blob: d77495ed098fd2117219afeb3a635c7aa8e31945 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package controller
import (
"context"
"fmt"
"strings"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
ufspb "infra/unifiedfleet/api/v1/models"
"infra/unifiedfleet/app/model/inventory"
"infra/unifiedfleet/app/model/registration"
"infra/unifiedfleet/app/util"
)
// AssetRegistration registers the given asset to the datastore after validation
func AssetRegistration(ctx context.Context, asset *ufspb.Asset) (*ufspb.Asset, error) {
hc := &HistoryClient{}
var err error
f := func(ctx context.Context) error {
if err := validateAssetRegistration(ctx, asset); err != nil {
return err
}
if asset.GetType() == ufspb.AssetType_DUT || asset.GetType() == ufspb.AssetType_LABSTATION {
//Create a new machine
if err := addMachineHelper(ctx, asset); err != nil {
return err
}
}
_, err := registration.BatchUpdateAssets(ctx, []*ufspb.Asset{asset})
if err != nil {
return err
}
hc.LogAssetChanges(nil, asset)
return hc.SaveChangeEvents(ctx)
}
if err = datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "AddAsset - unable to update asset %s", asset.GetName()).Err()
}
return asset, err
}
// UpdateAsset updates the asset record to the datastore after validation
func UpdateAsset(ctx context.Context, asset *ufspb.Asset, mask *field_mask.FieldMask) (*ufspb.Asset, error) {
var oldAsset *ufspb.Asset
var err error
hc := &HistoryClient{}
f := func(ctx context.Context) error {
// TODO(anshruth): Support validation of DUT/Labstation/Servo
// created using this asset. And update them accordingly or fail.
oldAsset, err = registration.GetAsset(ctx, asset.GetName())
if err != nil {
return err
}
err := validateUpdateAsset(ctx, oldAsset, asset, mask)
if err != nil {
return err
}
// Copy OUTPUT_ONLY fields
if asset.GetInfo() == nil {
asset.Info = &ufspb.AssetInfo{}
}
asset.GetInfo().SerialNumber = oldAsset.GetInfo().GetSerialNumber()
asset.GetInfo().Hwid = oldAsset.GetInfo().GetHwid()
asset.GetInfo().Sku = oldAsset.GetInfo().GetSku()
// updatableAsset will be used to update the asset
updatableAsset := asset
if mask != nil && mask.Paths != nil {
// Construct updatableAsset from mask if given
updatableAsset = proto.Clone(proto.MessageV1(oldAsset)).(*ufspb.Asset)
if asset, err = processAssetUpdateMask(asset, updatableAsset, mask); err != nil {
return err
}
}
a, err := registration.BatchUpdateAssets(ctx, []*ufspb.Asset{updatableAsset})
if err != nil {
return err
}
// Update the associated Machine from the updated asset
if err := updateMachineHelper(ctx, a[0]); err != nil {
return err
}
hc.LogAssetChanges(oldAsset, updatableAsset)
return hc.SaveChangeEvents(ctx)
}
if err = datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "UpdateAsset - unable to update asset %s", asset.GetName()).Err()
}
return asset, err
}
// GetAsset returns asset for the given name from datastore
func GetAsset(ctx context.Context, name string) (*ufspb.Asset, error) {
if name == "" {
return nil, status.Error(codes.InvalidArgument, "GetAsset - missing asset name")
}
asset, err := registration.GetAsset(ctx, name)
if err != nil {
return nil, errors.Annotate(err, "GetAsset - unable to get asset %s", name).Err()
}
return asset, nil
}
// ListAssets lists the assets
func ListAssets(ctx context.Context, pageSize int32, pageToken, filter string, keysOnly bool) ([]*ufspb.Asset, string, error) {
var filterMap map[string][]interface{}
var err error
if filter != "" {
filterMap, err = getFilterMap(filter, getAssetIndexedFieldName)
if err != nil {
return nil, "", errors.Annotate(err, "ListAssets - failed to read filter for listing assets").Err()
}
}
filterMap = resetZoneFilter(filterMap)
filterMap = resetAssetTypeFilter(filterMap)
return registration.ListAssets(ctx, pageSize, pageToken, filterMap, keysOnly)
}
// DeleteAsset deletes an asset from UFS
func DeleteAsset(ctx context.Context, name string) error {
hc := &HistoryClient{}
f := func(ctx context.Context) error {
asset, err := registration.GetAsset(ctx, name)
if err != nil {
return errors.Annotate(err, "DeleteAsset - cannot find asset %s", name).Err()
}
if err := validateDeleteAsset(ctx, asset); err != nil {
return errors.Annotate(err, "DeleteAsset - failed to delete %s", name).Err()
}
// delete associated machine
if err := deleteMachineHelper(ctx, asset.GetName()); err != nil {
return errors.Annotate(err, "DeleteAsset - failed to delete associated machine %s", asset.GetName()).Err()
}
// delete the asset
err = registration.DeleteAsset(ctx, name)
if err != nil {
return errors.Annotate(err, "DeleteAsset - failed to delete %s", name).Err()
}
hc.LogAssetChanges(asset, nil)
err = hc.SaveChangeEvents(ctx)
if err != nil {
return errors.Annotate(err, "DeleteAsset- unable to record delete history").Err()
}
return nil
}
return datastore.RunInTransaction(ctx, f, nil)
}
// addMachineHelper adds a machine for the newly added asset.
//
// asset should be a DUT or Labstation.
// This should be run inside a transaction.
func addMachineHelper(ctx context.Context, asset *ufspb.Asset) error {
// Check if machine already exists
if err := resourceAlreadyExists(ctx, []*Resource{GetMachineResource(asset.Name)}, nil); err != nil {
return err
}
machine := CreateMachineFromAsset(asset)
hc := getMachineHistoryClient(machine)
if _, err := registration.BatchUpdateMachines(ctx, []*ufspb.Machine{machine}); err != nil {
return errors.Annotate(err, "unable to create machine").Err()
}
hc.LogMachineChanges(nil, machine)
hc.stUdt.updateStateHelper(ctx, ufspb.State_STATE_REGISTERED)
return hc.SaveChangeEvents(ctx)
}
// updateMachineHelper updates a machine for the updated asset.
//
// This should be run inside a transaction.
func updateMachineHelper(ctx context.Context, asset *ufspb.Asset) error {
// Get the existing machine.
machine, err := registration.GetMachine(ctx, asset.GetName())
if err != nil {
if util.IsNotFoundError(err) {
// Create a new machine if the updated asset is a
// DUT or Labstation.
if asset.GetType() == ufspb.AssetType_DUT || asset.GetType() == ufspb.AssetType_LABSTATION {
return addMachineHelper(ctx, asset)
}
// Nothing to do if its a servo type.
// No machine is created for a servo asset.
return nil
}
return err
}
// If the machine exists and the updated asset is a servo
// then delete the associated machine.
if asset.GetType() == ufspb.AssetType_SERVO {
if err := validateDeleteAsset(ctx, asset); err != nil {
return errors.Annotate(err, "failed to update %s to SERVO type as there is a DUT associated with this asset.", asset.GetName()).Err()
}
return deleteMachineHelper(ctx, asset.GetName())
}
// Copy for logging
oldMachineCopy := proto.Clone(machine).(*ufspb.Machine)
hc := getMachineHistoryClient(machine)
//update the machine from the asset
updateMachineFromAsset(machine, asset)
if _, err := registration.BatchUpdateMachines(ctx, []*ufspb.Machine{machine}); err != nil {
return errors.Annotate(err, "unable to create machine").Err()
}
hc.LogMachineChanges(oldMachineCopy, machine)
return hc.SaveChangeEvents(ctx)
}
// deleteMachineHelper deletes a machine. If the machine is not found it return nil.
// This should be run inside a transaction.
func deleteMachineHelper(ctx context.Context, id string) error {
hc := getMachineHistoryClient(&ufspb.Machine{Name: id})
machine, err := registration.GetMachine(ctx, id)
if err != nil {
if status.Code(err) == codes.NotFound {
return nil
}
return err
}
// delete the machine
if err := registration.DeleteMachine(ctx, id); err != nil {
return err
}
hc.stUdt.deleteStateHelper(ctx)
hc.LogMachineChanges(machine, nil)
return hc.SaveChangeEvents(ctx)
}
// getAssetIndexedFieldName returns the same string as the mapping is 1:1
func getAssetIndexedFieldName(name string) (string, error) {
return name, nil
}
func validateUpdateAsset(ctx context.Context, oldAsset *ufspb.Asset, asset *ufspb.Asset, mask *field_mask.FieldMask) error {
if err := util.CheckPermission(ctx, util.RegistrationsUpdate, oldAsset.GetRealm()); err != nil {
return err
}
if asset.GetRealm() != "" && oldAsset.GetRealm() != asset.GetRealm() {
if err := util.CheckPermission(ctx, util.RegistrationsUpdate, asset.GetRealm()); err != nil {
return err
}
}
if mask == nil || mask.Paths == nil {
// If mask doesn't exist then validate the given asset
return validateAsset(ctx, asset)
}
// Validate AssetUpdate Mask if it exists
return validateAssetUpdateMask(ctx, asset, mask)
}
func validateAssetRegistration(ctx context.Context, asset *ufspb.Asset) error {
if err := util.CheckPermission(ctx, util.RegistrationsCreate, asset.GetRealm()); err != nil {
return err
}
if err := validateAsset(ctx, asset); err != nil {
return err
}
var errMsg strings.Builder
errMsg.WriteString("validateAsset - ")
if err := ResourceExist(ctx, []*Resource{GetAssetResource(asset.GetName())}, &errMsg); err == nil {
return status.Errorf(codes.FailedPrecondition, "validateAssetRegistration - Asset %s exists, cannot create another", asset.GetName())
}
return nil
}
func validateAsset(ctx context.Context, asset *ufspb.Asset) error {
if asset.GetName() == "" {
return status.Error(codes.InvalidArgument, "validateAsset - Missing name")
}
if asset.GetLocation() == nil {
return status.Error(codes.InvalidArgument, "validateAsset - Location unspecified")
}
if asset.GetLocation().GetZone() == ufspb.Zone_ZONE_UNSPECIFIED {
return status.Error(codes.InvalidArgument, "validateAsset - Zone unspecified")
}
// Check if the rack exists
if r := asset.GetLocation().GetRack(); r != "" {
var errMsg strings.Builder
errMsg.WriteString("validateAsset - ")
return ResourceExist(ctx, []*Resource{GetRackResource(r)}, &errMsg)
}
return status.Error(codes.InvalidArgument, "validateAsset - Rack unspecified")
}
func validateAssetUpdateMask(ctx context.Context, asset *ufspb.Asset, mask *field_mask.FieldMask) error {
if mask != nil {
for _, path := range mask.Paths {
switch path {
case "name":
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - name cannot be updated, delete and create new asset")
case "info.asset_tag":
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - asset_tag cannot be updated, delete and create new asset")
case "update_time":
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid update, cannot update update_time")
case "location":
fallthrough
case "location.zone":
if asset.GetLocation() == nil || asset.GetLocation().GetZone() == ufspb.Zone_ZONE_UNSPECIFIED {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid location")
}
case "location.rack":
if asset.GetLocation() == nil {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid location")
}
if asset.GetLocation().GetRack() == "" {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid update, cannot clear rack")
}
var errMsg strings.Builder
errMsg.WriteString("validateAssetUpdateMask - ")
return ResourceExist(ctx, []*Resource{GetRackResource(asset.GetLocation().GetRack())}, &errMsg)
case "location.aisle":
fallthrough
case "location.row":
fallthrough
case "location.rack_number":
fallthrough
case "location.shelf":
fallthrough
case "location.position":
fallthrough
case "location.barcode_name":
if asset.GetLocation() == nil {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid location")
}
case "type":
if asset.GetType() == ufspb.AssetType_UNDEFINED {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid update, no type given")
}
case "model":
if asset.GetModel() == "" {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid update, no model given")
}
case "info.cost_center":
fallthrough
case "info.google_code_name":
fallthrough
case "info.build_target":
fallthrough
case "info.reference_board":
fallthrough
case "info.ethernet_mac_address":
fallthrough
case "info.phase":
if asset.GetInfo() == nil {
return status.Error(codes.InvalidArgument, "validateAssetUpdateMask - Invalid asset info")
}
default:
return status.Errorf(codes.InvalidArgument, "validateAssetUpdateMask - unsupported mask %s", path)
}
}
}
return nil
}
func validateDeleteAsset(ctx context.Context, asset *ufspb.Asset) error {
if err := util.CheckPermission(ctx, util.RegistrationsDelete, asset.GetRealm()); err != nil {
return err
}
machinelses, err := inventory.QueryMachineLSEByPropertyName(ctx, "machine_ids", asset.GetName(), true)
if err != nil {
return err
}
if len(machinelses) > 0 {
return status.Errorf(codes.FailedPrecondition, fmt.Sprintf("Asset %s cannot be deleted because DUT %s is referring this Asset.", asset.GetName(), machinelses[0].GetName()))
}
// TODO(anushruth): Add validation for servo resources
return nil
}
func processAssetUpdateMask(updatedAsset, oldAsset *ufspb.Asset, mask *field_mask.FieldMask) (*ufspb.Asset, error) {
// Add empty asset info both messages to avoid segfaults
if oldAsset.GetInfo() == nil {
oldAsset.Info = &ufspb.AssetInfo{}
}
if updatedAsset.GetInfo() == nil {
updatedAsset.Info = &ufspb.AssetInfo{}
}
if mask != nil {
for _, path := range mask.Paths {
switch path {
case "type":
oldAsset.Type = updatedAsset.Type
case "model":
oldAsset.Model = updatedAsset.Model
oldAsset.Info.Model = updatedAsset.Model
case "location":
oldAsset.Location = updatedAsset.Location
oldAsset.Realm = updatedAsset.Realm
case "location.aisle":
oldAsset.Location.Aisle = updatedAsset.Location.Aisle
case "location.row":
oldAsset.Location.Row = updatedAsset.Location.Row
case "location.rack":
oldAsset.Location.Rack = updatedAsset.Location.Rack
case "location.rack_number":
oldAsset.Location.RackNumber = updatedAsset.Location.RackNumber
case "location.shelf":
oldAsset.Location.Shelf = updatedAsset.Location.Shelf
case "location.position":
oldAsset.Location.Position = updatedAsset.Location.Position
case "location.barcode_name":
oldAsset.Location.BarcodeName = updatedAsset.Location.BarcodeName
case "location.zone":
oldAsset.Location.Zone = updatedAsset.Location.Zone
oldAsset.Realm = updatedAsset.Realm
case "info.cost_center":
oldAsset.Info.CostCenter = updatedAsset.Info.CostCenter
case "info.google_code_name":
oldAsset.Info.GoogleCodeName = updatedAsset.Info.GoogleCodeName
case "info.build_target":
oldAsset.Info.BuildTarget = updatedAsset.Info.BuildTarget
case "info.reference_board":
oldAsset.Info.ReferenceBoard = updatedAsset.Info.ReferenceBoard
case "info.ethernet_mac_address":
oldAsset.Info.EthernetMacAddress = updatedAsset.Info.EthernetMacAddress
case "info.phase":
oldAsset.Info.Phase = updatedAsset.Info.Phase
}
}
return oldAsset, nil
}
return nil, status.Error(codes.InvalidArgument, "processAssetUpdateMask - Invalid Input, No mask found")
}
// UpdateAssetMeta updates only dut meta data portion of the Asset.
//
// It's a temporary method to correct Serial number, HWID and Sku.
// Will remove once HaRT could provide us the correct info.
func UpdateAssetMeta(ctx context.Context, meta *ufspb.DutMeta) error {
if meta == nil {
return nil
}
f := func(ctx context.Context) error {
machine, err := registration.GetMachine(ctx, meta.GetChromeosDeviceId())
if err != nil {
return errors.Annotate(err, "UpdateAssetMeta").Err()
}
if machine.GetChromeosMachine() == nil {
logging.Warningf(ctx, "UpdateAssetMeta - %s is not a valid Chromeos machine", meta.GetChromeosDeviceId())
return nil
}
asset, err := registration.GetAsset(ctx, meta.GetChromeosDeviceId())
if err != nil {
return errors.Annotate(err, "UpdateAssetMeta").Err()
}
hc := &HistoryClient{}
// Copy for logging
oldAsset := proto.Clone(asset).(*ufspb.Asset)
if asset.GetInfo() == nil {
asset.Info = &ufspb.AssetInfo{}
}
if asset.GetInfo().GetSerialNumber() == meta.GetSerialNumber() &&
asset.GetInfo().GetHwid() == meta.GetHwID() &&
asset.GetInfo().GetSku() == meta.GetDeviceSku() {
logging.Warningf(ctx, "nothing to update: old serial number %q, old hwid %q, old device-sku %q", meta.GetSerialNumber(), meta.GetHwID(), meta.GetDeviceSku())
return nil
}
asset.GetInfo().SerialNumber = meta.GetSerialNumber()
asset.GetInfo().Hwid = meta.GetHwID()
asset.GetInfo().Sku = meta.GetDeviceSku()
// Update the asset
if _, err := registration.BatchUpdateAssets(ctx, []*ufspb.Asset{asset}); err != nil {
return errors.Annotate(err, "Unable to update dut meta for %s", asset.Name).Err()
}
hc.LogAssetChanges(oldAsset, asset)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "UpdateAssetMeta - %s", err.Error())
return err
}
return nil
}
// CreateMachineFromAsset creates machine from asset
//
// If the asset is either a DUT or Labstation, machine is returned, nil otherwise.
func CreateMachineFromAsset(asset *ufspb.Asset) *ufspb.Machine {
if asset == nil {
return nil
}
device := &ufspb.ChromeOSMachine{
ReferenceBoard: asset.GetInfo().GetReferenceBoard(),
BuildTarget: asset.GetInfo().GetBuildTarget(),
Model: asset.GetInfo().GetModel(),
GoogleCodeName: asset.GetInfo().GetGoogleCodeName(),
MacAddress: asset.GetInfo().GetEthernetMacAddress(),
Sku: asset.GetInfo().GetSku(),
Phase: asset.GetInfo().GetPhase(),
CostCenter: asset.GetInfo().GetCostCenter(),
Hwid: asset.GetInfo().GetHwid(),
}
switch asset.GetType() {
case ufspb.AssetType_DUT:
device.DeviceType = ufspb.ChromeOSDeviceType_DEVICE_CHROMEBOOK
case ufspb.AssetType_LABSTATION:
device.DeviceType = ufspb.ChromeOSDeviceType_DEVICE_LABSTATION
default:
// Only DUTs and Labstations are stored as machines
return nil
}
machine := &ufspb.Machine{
Name: asset.GetName(),
SerialNumber: asset.GetInfo().GetSerialNumber(),
Location: asset.GetLocation(),
Device: &ufspb.Machine_ChromeosMachine{
ChromeosMachine: device,
},
Realm: asset.GetRealm(),
ResourceState: ufspb.State_STATE_REGISTERED,
}
return machine
}
// updateMachineFromAsset updates a machine from asset
//
// This must be used only for DUT or a Labstation.
func updateMachineFromAsset(machine *ufspb.Machine, asset *ufspb.Asset) {
if asset == nil {
return
}
if machine.GetChromeosMachine() == nil {
machine.Device = &ufspb.Machine_ChromeosMachine{
ChromeosMachine: &ufspb.ChromeOSMachine{},
}
}
// Serial number of UFS machine is updated by SSW in
// UpdateDutMeta https://source.corp.google.com/chromium_infra/go/src/infra/unifiedfleet/app/controller/machine.go;l=182
// Dont rely on Asset(user provided) for Serial number, Hwid, Sku.
// Leave them with original values and dont update them.
machine.Location = asset.GetLocation()
machine.Realm = asset.GetRealm()
machine.GetChromeosMachine().ReferenceBoard = asset.GetInfo().GetReferenceBoard()
machine.GetChromeosMachine().BuildTarget = asset.GetInfo().GetBuildTarget()
machine.GetChromeosMachine().Model = asset.GetInfo().GetModel()
machine.GetChromeosMachine().GoogleCodeName = asset.GetInfo().GetGoogleCodeName()
machine.GetChromeosMachine().MacAddress = asset.GetInfo().GetEthernetMacAddress()
machine.GetChromeosMachine().Phase = asset.GetInfo().GetPhase()
machine.GetChromeosMachine().CostCenter = asset.GetInfo().GetCostCenter()
switch asset.GetType() {
case ufspb.AssetType_DUT:
machine.GetChromeosMachine().DeviceType = ufspb.ChromeOSDeviceType_DEVICE_CHROMEBOOK
case ufspb.AssetType_LABSTATION:
machine.GetChromeosMachine().DeviceType = ufspb.ChromeOSDeviceType_DEVICE_LABSTATION
default:
// Only DUTs and Labstations are stored as machines
}
}