blob: 9c61d65d4bee75632a962269da0ab50e8c64c0d4 [file]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package inventory implements the fleet.Inventory service end-points of
// corsskylabadmin.
package inventory
import (
"context"
"strings"
"time"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
fleet "infra/appengine/crosskylabadmin/api/fleet/v1"
"infra/appengine/crosskylabadmin/app/frontend/internal/datastore/dronecfg"
"infra/appengine/crosskylabadmin/app/frontend/internal/datastore/freeduts"
dsinventory "infra/appengine/crosskylabadmin/app/frontend/internal/datastore/inventory"
dssv "infra/appengine/crosskylabadmin/app/frontend/internal/datastore/stableversion"
"infra/appengine/crosskylabadmin/app/gitstore"
"infra/libs/skylab/inventory"
)
const beagleboneServo = "beaglebone_servo"
// ListServers implements the method from fleet.InventoryServer interface.
func (is *ServerImpl) ListServers(ctx context.Context, req *fleet.ListServersRequest) (resp *fleet.ListServersResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(ctx, err)
}()
return nil, status.Error(codes.Unimplemented, "ListServers not yet implemented")
}
// GetDutInfo implements the method from fleet.InventoryServer interface.
// Deprecated: Do not use.
func (is *ServerImpl) GetDutInfo(ctx context.Context, req *fleet.GetDutInfoRequest) (resp *fleet.GetDutInfoResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(ctx, err)
}()
if err = req.Validate(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
ic, err := is.newInventoryClient(ctx)
if err != nil {
return nil, err
}
data, updated, err := ic.getDutInfo(ctx, req)
if err != nil {
return nil, err
}
return &fleet.GetDutInfoResponse{
Spec: data,
Updated: timestamppb.New(updated),
}, nil
}
// GetDroneConfig implements the method from fleet.InventoryServer interface.
func (is *ServerImpl) GetDroneConfig(ctx context.Context, req *fleet.GetDroneConfigRequest) (resp *fleet.GetDroneConfigResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(ctx, err)
}()
e, err := dronecfg.Get(ctx, req.Hostname)
if err != nil {
if datastore.IsErrNoSuchEntity(err) {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, err
}
resp = &fleet.GetDroneConfigResponse{}
for _, d := range e.DUTs {
resp.Duts = append(resp.Duts, &fleet.GetDroneConfigResponse_Dut{
Id: d.ID,
Hostname: d.Hostname,
})
}
return resp, nil
}
// ListRemovedDuts implements the method from fleet.InventoryServer interface.
func (is *ServerImpl) ListRemovedDuts(ctx context.Context, req *fleet.ListRemovedDutsRequest) (resp *fleet.ListRemovedDutsResponse, err error) {
return nil, nil
}
// GetStableVersion implements the method from fleet.InventoryServer interface
func (is *ServerImpl) GetStableVersion(ctx context.Context, req *fleet.GetStableVersionRequest) (resp *fleet.GetStableVersionResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(ctx, err)
}()
ic, err := is.newInventoryClient(ctx)
if err != nil {
logging.Errorf(ctx, "Failed to create inventory client: %s", err.Error())
logging.Infof(ctx, "Fall back to legacy flow")
ic = nil
}
return getStableVersionImpl(ctx, ic, req.BuildTarget, req.Model, req.Hostname)
}
// ReportInventory reports metrics of duts in inventory.
//
// This method is deprecated. UFS reports the inventory metrics.
func (is *ServerImpl) ReportInventory(ctx context.Context, req *fleet.ReportInventoryRequest) (resp *fleet.ReportInventoryResponse, err error) {
return &fleet.ReportInventoryResponse{}, nil
}
// UpdateCachedInventory implements the method from fleet.InventoryServer interface.
func (is *ServerImpl) UpdateCachedInventory(ctx context.Context, req *fleet.UpdateCachedInventoryRequest) (resp *fleet.UpdateCachedInventoryResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(ctx, err)
}()
store, err := is.newStore(ctx)
if err != nil {
return nil, err
}
if err := store.Refresh(ctx); err != nil {
return nil, err
}
duts := dutsInCurrentEnvironment(ctx, store.Lab.GetDuts())
if err := dsinventory.UpdateDUTs(ctx, duts); err != nil {
return nil, err
}
es := makeDroneConfigs(ctx, store.Infrastructure, store.Lab)
if err := dronecfg.Update(ctx, es); err != nil {
return nil, err
}
if err := updateFreeDUTs(ctx, store); err != nil {
return nil, err
}
return &fleet.UpdateCachedInventoryResponse{}, nil
}
func dutsInCurrentEnvironment(ctx context.Context, duts []*inventory.DeviceUnderTest) []*inventory.DeviceUnderTest {
// TODO(crbug.com/947322): Disable this temporarily until it
// can be implemented properly. This updates the cache of
// DUTs which can only be queried by hostname or ID, so it is
// not problematic to also cache DUTs in the wrong environment
// (prod vs dev).
return duts
}
func makeDroneConfigs(ctx context.Context, inf *inventory.Infrastructure, lab *inventory.Lab) []dronecfg.Entity {
dutHostnames := makeDUTHostnameMap(lab.GetDuts())
var entities []dronecfg.Entity
for _, s := range inf.GetServers() {
if !isDrone(s) {
continue
}
e := dronecfg.Entity{
Hostname: s.GetHostname(),
}
for _, d := range s.GetDutUids() {
h, ok := dutHostnames[d]
if !ok {
logging.Infof(ctx, "DUT ID %s doesn't match any hostname", d)
continue
}
e.DUTs = append(e.DUTs, dronecfg.DUT{
ID: d,
Hostname: h,
})
}
entities = append(entities, e)
}
return entities
}
// makeDUTHostnameMap makes a mapping from DUT IDs to DUT hostnames.
func makeDUTHostnameMap(duts []*inventory.DeviceUnderTest) map[string]string {
m := make(map[string]string)
for _, d := range duts {
c := d.GetCommon()
m[c.GetId()] = c.GetHostname()
}
return m
}
func isDrone(s *inventory.Server) bool {
for _, r := range s.GetRoles() {
if r == inventory.Server_ROLE_SKYLAB_DRONE {
return true
}
}
return false
}
func updateFreeDUTs(ctx context.Context, s *gitstore.InventoryStore) error {
ic := newGlobalInvCache(ctx, s)
var free []freeduts.DUT
for dutID, d := range ic.idToDUT {
if _, ok := ic.droneForDUT[dutID]; ok {
continue
}
free = append(free, freeDUTInfo(d))
}
stale, err := getStaleFreeDUTs(ctx, free)
if err != nil {
return errors.Annotate(err, "update free duts").Err()
}
if err := freeduts.Remove(ctx, stale); err != nil {
return errors.Annotate(err, "update free duts").Err()
}
if err := freeduts.Add(ctx, free); err != nil {
return errors.Annotate(err, "update free duts").Err()
}
return nil
}
// getStaleFreeDUTs returns the free DUTs in datastore that are no longer
// free, given the currently free DUTs passed as an argument.
func getStaleFreeDUTs(ctx context.Context, free []freeduts.DUT) ([]freeduts.DUT, error) {
freeMap := make(map[string]bool, len(free))
for _, d := range free {
freeMap[d.ID] = true
}
all, err := freeduts.GetAll(ctx)
if err != nil {
return nil, errors.Annotate(err, "get stale free duts").Err()
}
stale := make([]freeduts.DUT, 0, len(all))
for _, d := range all {
if _, ok := freeMap[d.ID]; !ok {
stale = append(stale, d)
}
}
return stale, nil
}
// freeDUTInfo returns the free DUT info to store for a DUT.
func freeDUTInfo(d *inventory.DeviceUnderTest) freeduts.DUT {
c := d.GetCommon()
rr := d.GetRemovalReason()
var t time.Time
if ts := rr.GetExpireTime(); ts != nil {
t = time.Unix(ts.GetSeconds(), int64(ts.GetNanos())).UTC()
}
return freeduts.DUT{
ID: c.GetId(),
Hostname: c.GetHostname(),
Bug: rr.GetBug(),
Comment: rr.GetComment(),
ExpireTime: t,
Model: c.GetLabels().GetModel(),
}
}
// getStableVersionImpl returns all the stable versions associated with a given buildTarget and model
// NOTE: hostname is explicitly allowed to be "". If hostname is "", then no hostname was provided in the GetStableVersion RPC call
func getStableVersionImpl(ctx context.Context, ic inventoryClient, buildTarget string, model string, hostname string) (*fleet.GetStableVersionResponse, error) {
logging.Infof(ctx, "getting stable version for buildTarget: %s and model: %s", buildTarget, model)
if hostname == "" {
logging.Infof(ctx, "hostname not provided, using buildTarget (%s) and model (%s)", buildTarget, model)
return getStableVersionImplNoHostname(ctx, buildTarget, model)
}
logging.Infof(ctx, "hostname (%s) provided, ignoring user-provided buildTarget (%s) and model (%s)", hostname, buildTarget, model)
return getStableVersionImplWithHostname(ctx, ic, hostname)
}
// getStableVersionImplNoHostname returns stableversion information given a buildTarget and model
// TODO(gregorynisbet): Consider under what circumstances an error leaving this function
// should be considered transient or non-transient.
// If the dut in question is a beaglebone servo, then failing to get the firmware version
// is non-fatal.
func getStableVersionImplNoHostname(ctx context.Context, buildTarget string, model string) (*fleet.GetStableVersionResponse, error) {
logging.Infof(ctx, "getting stable version for buildTarget: (%s) and model: (%s)", buildTarget, model)
var err error
merr := errors.NewMultiError()
out := &fleet.GetStableVersionResponse{}
out.CrosVersion, err = dssv.GetCrosStableVersion(ctx, buildTarget, model)
if err != nil {
merr = append(merr, err)
}
out.FaftVersion, err = dssv.GetFaftStableVersion(ctx, buildTarget, model)
if err != nil {
logging.Infof(ctx, "faft stable version does not exist: %#v", err)
}
// successful early exit if we have a beaglebone servo
if buildTarget == beagleboneServo || model == beagleboneServo {
return out, nil
}
out.FirmwareVersion, err = dssv.GetFirmwareStableVersion(ctx, buildTarget, model)
if err != nil {
logging.Errorf(ctx, "getStableVersionImplNoHostname: failed to get firmware version %q %q", buildTarget, model)
merr = append(merr, err)
}
if len(merr) != 0 {
return nil, errors.Annotate(merr, "getStableVersionImplNoHostname").Err()
}
return out, nil
}
// getStableVersionImplWithHostname return stable version information given just a hostname
// TODO(gregorynisbet): Consider under what circumstances an error leaving this function
// should be considered transient or non-transient.
func getStableVersionImplWithHostname(ctx context.Context, ic inventoryClient, hostname string) (*fleet.GetStableVersionResponse, error) {
var err error
// If the DUT in question is a labstation or a servo (i.e. is a servo host), then it does not have
// its own servo host.
if looksLikeServo(hostname) {
return getStableVersionImplNoHostname(ctx, beagleboneServo, "")
}
dut, err := getDUT(ctx, ic, hostname)
if err != nil {
return nil, errors.Annotate(err, "failed to get DUT %q", dut).Err()
}
buildTarget := dut.GetCommon().GetLabels().GetBoard()
model := dut.GetCommon().GetLabels().GetModel()
out, err := getStableVersionImplNoHostname(ctx, buildTarget, model)
if err != nil {
return nil, errors.Annotate(err, "failed to get stable version info").Err()
}
if looksLikeLabstation(hostname) {
return out, nil
}
servoHostHostname, err := getServoHostHostname(dut)
if err != nil {
// Some DUTs, particularly High Touch Lab DUTs legitimately do not have servos.
// See b/162030132 for context.
logging.Infof(ctx, "failed to get servo host for %q", hostname)
return out, nil
}
if looksLikeFakeServo(servoHostHostname) {
logging.Infof(ctx, "concluded servo hostname is fake %q", servoHostHostname)
return out, nil
}
servoStableVersion, err := getCrosVersionFromServoHost(ctx, ic, servoHostHostname)
if err != nil {
return nil, errors.Annotate(err, "getting cros version from servo host %q", servoHostHostname).Err()
}
out.ServoCrosVersion = servoStableVersion
return out, nil
}
// getServoHostHostname gets the servo host hostname associated with a dut
// for instance, a labstation is a servo host.
func getServoHostHostname(dut *inventory.DeviceUnderTest) (string, error) {
attrs := dut.GetCommon().GetAttributes()
if len(attrs) == 0 {
return "", errors.Reason("attributes for dut with hostname %q is unexpectedly empty", dut.GetCommon().GetHostname()).Err()
}
for _, item := range attrs {
key := item.GetKey()
value := item.GetValue()
if key == "servo_host" {
if value == "" {
return "", errors.Reason("\"servo_host\" attribute unexpectedly has value \"\" for hostname %q", dut.GetCommon().GetHostname()).Err()
}
return value, nil
}
}
return "", errors.Reason("no \"servo_host\" attribute for hostname %q", dut.GetCommon().GetHostname()).Err()
}
// getDUT returns the DUT associated with a particular hostname from datastore
func getDUT(ctx context.Context, ic inventoryClient, hostname string) (*inventory.DeviceUnderTest, error) {
if ic == nil {
return nil, errors.Reason("Inventory Client cannot be nil").Err()
}
resp, _, err := ic.getDutInfo(ctx, &fleet.GetDutInfoRequest{
Hostname: hostname,
})
if err != nil {
return nil, errors.Annotate(err, "getting serialized DUT by hostname for %q", hostname).Err()
}
dut := &inventory.DeviceUnderTest{}
if err := proto.Unmarshal(resp, dut); err != nil {
return nil, errors.Annotate(err, "unserializing DUT for hostname %q", hostname).Err()
}
return dut, nil
}
// This is a heuristic to check if something is a labstation and might be wrong.
func looksLikeLabstation(hostname string) bool {
return strings.Contains(hostname, "labstation")
}
// This is a heuristic to check if something is a servo and might be wrong.
func looksLikeServo(hostname string) bool {
return strings.Contains(hostname, "servo")
}
// looksLikeFakeServo is a heuristic to check if a given hostname is an obviously
// fake entry such as an empty string or dummy_host or FAKE_SERVO_HOST or similar
func looksLikeFakeServo(hostname string) bool {
h := strings.ToLower(hostname)
return h == "" || strings.Contains(h, "dummy") || strings.Contains(h, "fake")
}
// getCrosVersionFromServoHost returns the cros version associated with a particular servo host
// hostname : hostname of the servo host (e.g. labstation)
// NOTE: If hostname is "localhost", task is for Satlab Containerized servod.
// NOTE: If hostname is "", this indicates the absence of a relevant servo host. This can happen if the DUT in question is already a labstation, for instance.
// NOTE: The cros version will be empty "" if the labstation does not exist. Because we don't re-image labstations as part of repair, the absence of a stable CrOS version for a labstation is not an error.
func getCrosVersionFromServoHost(ctx context.Context, ic inventoryClient, hostname string) (string, error) {
if hostname == "" || hostname == "localhost" {
logging.Infof(ctx, "Skipping getting cros version. Servo host hostname is %q", hostname)
return "", nil
}
if looksLikeLabstation(hostname) {
dut, err := getDUT(ctx, ic, hostname)
if err != nil {
logging.Infof(ctx, "get labstation dut info; %s", err)
return "", nil
}
buildTarget := dut.GetCommon().GetLabels().GetBoard()
model := dut.GetCommon().GetLabels().GetModel()
if buildTarget == "" {
return "", errors.Reason("no buildTarget for hostname %q", hostname).Err()
}
out, err := dssv.GetCrosStableVersion(ctx, buildTarget, model)
if err != nil {
return "", errors.Annotate(err, "getting labstation stable version").Err()
}
return out, nil
}
if looksLikeServo(hostname) {
// TODO(gregorynisbet): getting the stable version of a beaglebone servo is dependent on the fallback
// behavior
out, err := dssv.GetCrosStableVersion(ctx, beagleboneServo, beagleboneServo)
if err != nil {
return "", errors.Annotate(err, "getting beaglebone servo stable version").Err()
}
return out, nil
}
return "", errors.Reason("unrecognized hostname %q is not a labstation or beaglebone servo", hostname).Err()
}