blob: e99a781d45ca7bd41ded76563e3eb7545d436b8c [file] [log] [blame]
// Copyright 2019 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 datastore contains datastore-related logic.
package datastore
import (
"context"
"fmt"
"strings"
"time"
"github.com/golang/protobuf/proto"
"github.com/google/uuid"
"go.chromium.org/chromiumos/infra/proto/go/device"
"go.chromium.org/chromiumos/infra/proto/go/lab"
"go.chromium.org/chromiumos/infra/proto/go/manufacturing"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"infra/cros/lab_inventory/changehistory"
"infra/cros/lab_inventory/utils"
)
const (
// UUIDPrefix is the prefix we used to identify the system generated ID.
UUIDPrefix = "UUID"
dutIDPlaceholder = "IGNORED"
)
// A query in transaction requires to have Ancestor filter, see
// https://cloud.google.com/appengine/docs/standard/python/datastore/query-restrictions#queries_inside_transactions_must_include_ancestor_filters
func fakeAcestorKey(ctx context.Context) *datastore.Key {
return datastore.MakeKey(ctx, DeviceKind, "key")
}
func addMissingID(devices []*lab.ChromeOSDevice) {
// Use uuid as the device ID if asset id is not present.
for _, d := range devices {
// TODO (guocb) Erase the id passed in as long as it's not asset id to
// ensure the ID is unique.
if d.GetId() == nil || d.GetId().GetValue() == "" || d.GetId().GetValue() == dutIDPlaceholder {
d.Id = &lab.ChromeOSDeviceID{
Value: fmt.Sprintf("%s:%s", UUIDPrefix, uuid.New().String()),
}
}
}
}
func sanityCheckForAdding(ctx context.Context, d *lab.ChromeOSDevice, q *datastore.Query) error {
id := d.GetId().GetValue()
hostname := utils.GetHostname(d)
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q.Eq("Hostname", hostname), &devs); err != nil {
return errors.Annotate(err, "failed to get host by hostname %s", hostname).Err()
}
if len(devs) > 0 {
return errors.Reason("fail to add device <%s:%s> due to hostname confliction", hostname, id).Err()
}
newEntity := DeviceEntity{
ID: DeviceEntityID(id),
Parent: fakeAcestorKey(ctx),
}
if !strings.HasPrefix(id, UUIDPrefix) {
if err := datastore.Get(ctx, &newEntity); err != datastore.ErrNoSuchEntity {
return errors.Reason("failed to add device %s due to ID confliction", newEntity).Err()
}
}
return nil
}
func getDutServo(ctx context.Context, d *lab.ChromeOSDevice) (*lab.Servo, error) {
id := d.GetId().GetValue()
entity := &DeviceEntity{
ID: DeviceEntityID(id),
Parent: fakeAcestorKey(ctx),
}
if err := datastore.Get(ctx, entity); err != nil {
if datastore.IsErrNoSuchEntity(err) {
// hiding error, device not exist and cannot provide old servo
logging.Errorf(ctx, "device with ID: %s not exits", id)
return nil, nil
}
logging.Errorf(ctx, "Failed to get entity from datastore: %s", err)
return nil, errors.Annotate(err, "Internal error when try to find the device with id: %s", id).Err()
}
var labConfig lab.ChromeOSDevice
if err := entity.GetCrosDeviceProto(&labConfig); err != nil {
return nil, errors.Annotate(err, "failed to unmarshal lab config data for %s", id).Err()
}
dut := labConfig.GetDut()
if dut == nil {
return nil, nil
}
return dut.GetPeripherals().GetServo(), nil
}
// AddDevices creates a new Device datastore entity with a unique ID.
func AddDevices(ctx context.Context, devices []*lab.ChromeOSDevice, assignServoPort bool) (*DeviceOpResults, error) {
updatedTime := time.Now().UTC()
addMissingID(devices)
addingResults := make(DeviceOpResults, len(devices))
for i, d := range devices {
addingResults[i].Data = d
}
r := newServoHostRegistryFromProtoMsgs(ctx, devices)
f := func(ctx context.Context) error {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
entities := make([]*DeviceEntity, 0, len(devices))
entityResults := make([]*DeviceOpResult, 0, len(devices))
// Don't use the value returned by `range`. It's a copied value,
// instead of a reference.
for i := range addingResults {
devToAdd := &addingResults[i]
message := devToAdd.Data.(*lab.ChromeOSDevice)
hostname := utils.GetHostname(message)
id := message.GetId().GetValue()
devToAdd.Entity = &DeviceEntity{
ID: DeviceEntityID(id),
Hostname: hostname,
Parent: fakeAcestorKey(ctx),
}
if err := sanityCheckForAdding(ctx, message, q); err != nil {
devToAdd.logError(err)
continue
}
if dut := message.GetDut(); dut != nil {
cleanPreDeployFields(dut)
// Update associated labstation if the DUT has a new servo. Also
// assign new servo port if specified.
if err := r.amendServoToLabstation(ctx, dut, nil, assignServoPort); err != nil {
devToAdd.logError(err)
continue
}
}
labConfig, err := proto.Marshal(message)
if err != nil {
devToAdd.logError(errors.Annotate(err, fmt.Sprintf("fail to marshal device <%s:%s>", hostname, id), err).Err())
continue
}
devToAdd.Entity.Updated = updatedTime
devToAdd.Entity.LabConfig = labConfig
entities = append(entities, devToAdd.Entity)
entityResults = append(entityResults, devToAdd)
}
if err := datastore.Put(ctx, entities); err != nil {
for i, e := range err.(errors.MultiError) {
entityResults[i].logError(e)
}
}
logLifeCycleEvent(ctx, changehistory.LifeCycleDeployment, addingResults)
return r.saveToDatastore(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return &addingResults, err
}
return &addingResults, nil
}
// DeleteDevicesByIds deletes entities by specified Ids.
// The datastore implementation doesn't raise error when deleting non-existing
// entities: https://github.com/googleapis/google-cloud-go/issues/501
//
// As additional deleting servo from labstation for deleted device.
func DeleteDevicesByIds(ctx context.Context, ids []string) DeviceOpResults {
removingResults := make(DeviceOpResults, len(ids))
r := newServoHostRegistryFromProtoMsgs(ctx, nil)
f := func(ctx context.Context) error {
removingResults = GetDevicesByIds(ctx, ids)
var entities []*DeviceEntity
for i := range removingResults {
deviceResult := &removingResults[i]
if deviceResult.Err != nil || deviceResult.Entity == nil {
continue
}
entity := deviceResult.Entity
var devProto lab.ChromeOSDevice
if err := entity.GetCrosDeviceProto(&devProto); err != nil {
deviceResult.logError(errors.Annotate(err, "failed to unmarshal lab config data for %s", entity.Hostname).Err())
continue
}
if dut := devProto.GetDut(); dut != nil {
err := r.removeDeviceFromLabstation(ctx, dut)
if err != nil {
deviceResult.logError(errors.Annotate(err, "failed to delete servo from labstation for: %s", entity.Hostname).Err())
continue
}
} else if labstation := devProto.GetLabstation(); labstation != nil {
if len(labstation.GetServos()) > 0 {
deviceResult.logError(errors.Reason("cannot delete labstation: %s used by the DUTs", entity.Hostname).Err())
continue
}
}
entities = append(entities, entity)
}
if err := datastore.Delete(ctx, entities); err != nil {
for i, e := range err.(errors.MultiError) {
removingResults[i].logError(e)
}
}
logLifeCycleEvent(ctx, changehistory.LifeCycleDecomm, removingResults)
return r.saveToDatastore(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
for _, result := range removingResults {
result.logError(err)
}
}
return removingResults
}
func logLifeCycleEvent(ctx context.Context, event changehistory.LifeCycleEvent, opResults DeviceOpResults) {
var changes changehistory.Changes
for _, r := range opResults.Passed() {
switch event {
case changehistory.LifeCycleDeployment:
changes.LogDeployment(string(r.Entity.ID), r.Entity.Hostname)
case changehistory.LifeCycleDecomm:
changes.LogDecommission(string(r.Entity.ID), r.Entity.Hostname)
}
logging.Infof(ctx, "LifeCycleEvent: %s %s", event, r.Entity)
}
if len(changes) == 0 {
return
}
if err := changes.SaveToDatastore(ctx); err != nil {
logging.Errorf(ctx, "%s: Failed to save change history to datastore: %s", event, err)
}
}
// DeleteDevicesByHostnames deletes entities by specified hostnames.
//
// As additional deleting servo from labstation for deleted device.
func DeleteDevicesByHostnames(ctx context.Context, hostnames []string) DeviceOpResults {
removingResults := make(DeviceOpResults, len(hostnames))
r := newServoHostRegistryFromProtoMsgs(ctx, nil)
f := func(ctx context.Context) error {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
entities := make([]*DeviceEntity, 0, len(hostnames))
entityResults := make([]*DeviceOpResult, 0, len(hostnames))
// Filter out invalid input hostnames.
for i, hostname := range hostnames {
removingResults[i].Entity = &DeviceEntity{Hostname: hostname}
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q.Eq("Hostname", hostname), &devs); err != nil {
removingResults[i].logError(errors.Annotate(err, "failed to get host by hostname %s", hostname).Err())
continue
}
if len(devs) == 0 {
removingResults[i].logError(errors.Reason("No such host: %s", hostname).Err())
continue
}
if len(devs) > 1 {
removingResults[i].logError(errors.Reason("multiple entities found with hostname %s: %v", hostname, devs).Err())
continue
}
entity := devs[0]
var devProto lab.ChromeOSDevice
if err := entity.GetCrosDeviceProto(&devProto); err != nil {
removingResults[i].logError(errors.Annotate(err, "failed to unmarshal lab config data for %s", hostname).Err())
continue
}
if dut := devProto.GetDut(); dut != nil {
err := r.removeDeviceFromLabstation(ctx, dut)
if err != nil {
removingResults[i].logError(errors.Annotate(err, "failed to delete servo from labstation for: %s", hostname).Err())
continue
}
} else if labstation := devProto.GetLabstation(); labstation != nil {
if len(labstation.GetServos()) > 0 {
removingResults[i].logError(errors.Reason("cannot delete labstation: %s used by the DUTs", hostname).Err())
continue
}
}
removingResults[i].Entity = entity
entities = append(entities, entity)
entityResults = append(entityResults, &removingResults[i])
}
if err := datastore.Delete(ctx, entities); err != nil {
for i, e := range err.(errors.MultiError) {
entityResults[i].logError(e)
}
}
logLifeCycleEvent(ctx, changehistory.LifeCycleDecomm, removingResults)
return r.saveToDatastore(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
for _, result := range removingResults {
result.logError(err)
}
}
return removingResults
}
// GetDevicesByIds returns entities by specified ids.
func GetDevicesByIds(ctx context.Context, ids []string) DeviceOpResults {
retrievingResults := make(DeviceOpResults, len(ids))
entities := make([]DeviceEntity, len(ids))
for i, id := range ids {
retrievingResults[i].Entity = &entities[i]
entities[i].ID = DeviceEntityID(id)
entities[i].Parent = fakeAcestorKey(ctx)
}
if err := datastore.Get(ctx, entities); err != nil {
for i, e := range err.(errors.MultiError) {
retrievingResults[i].logError(e)
}
}
return retrievingResults
}
// GetDevicesByHostnames returns entities by specified hostnames.
func GetDevicesByHostnames(ctx context.Context, hostnames []string) DeviceOpResults {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
retrievingResults := make(DeviceOpResults, len(hostnames))
// Filter out invalid input hostnames.
for i, hostname := range hostnames {
retrievingResults[i].Entity = &DeviceEntity{Hostname: hostname}
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q.Eq("Hostname", hostname), &devs); err != nil {
retrievingResults[i].logError(errors.Annotate(err, "failed to get host by hostname %s", hostname).Err())
continue
}
if len(devs) == 0 {
retrievingResults[i].logError(errors.Reason("No such host: %s", hostname).Err())
continue
}
if len(devs) > 1 {
retrievingResults[i].logError(errors.Reason("multiple hosts found with hostname %s: %v", hostname, devs).Err())
continue
}
retrievingResults[i].Entity = devs[0]
}
return retrievingResults
}
func getDUTServoByHostname(ctx context.Context, hostnames []string) ([]*lab.Servo, error) {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
var servos []*lab.Servo
for _, hostname := range hostnames {
if hostname == "" {
continue
}
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q.Eq("Hostname", hostname), &devs); err != nil {
return nil, errors.Annotate(err, "failed to get DUT by hostname %s", hostname).Err()
} else if len(devs) == 0 {
return nil, errors.Reason("No such host: %s", hostname).Err()
} else if len(devs) > 1 {
return nil, errors.Reason("multiple entities found with hostname %s: %v", hostname, devs).Err()
}
entity := devs[0]
var devProto lab.ChromeOSDevice
if err := entity.GetCrosDeviceProto(&devProto); err != nil {
return nil, errors.Annotate(err, "failed to unmarshal lab config data for %s", hostname).Err()
}
dut := devProto.GetDut()
if dut == nil {
continue
}
servo := dut.GetPeripherals().GetServo()
if servo == nil {
continue
}
servos = append(servos, servo)
}
return servos, nil
}
// GetAllDevices returns all device entities.
//
// TODO(guocb) optimize for performance if needed.
func GetAllDevices(ctx context.Context) (DeviceOpResults, error) {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q, &devs); err != nil {
return nil, errors.Annotate(err, "failed to get all devices").Err()
}
result := make([]DeviceOpResult, len(devs))
for i, d := range devs {
result[i].Entity = d
}
return DeviceOpResults(result), nil
}
// ListDevices lists the devices
//
// Does a query over device entities. Returns up to pageSize entities, plus non-nil cursor (if
// there are more results). pageSize must be positive.
func ListDevices(ctx context.Context, pageSize int32, pageToken string) (res []DeviceOpResult, nextPageToken string, err error) {
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx)).Limit(pageSize).FirestoreMode(true)
var cursor datastore.Cursor
if pageToken != "" {
cursor, err = datastore.DecodeCursor(ctx, pageToken)
if err != nil {
logging.Errorf(ctx, "Failed to DecodeCursor from pageToken: %s", err)
return nil, "", errors.Annotate(err, "ListDevices").Err()
}
}
if cursor != nil {
q = q.Start(cursor)
}
res = make([]DeviceOpResult, 0)
var nextCur datastore.Cursor
err = datastore.Run(ctx, q, func(ent *DeviceEntity, cb datastore.CursorCB) error {
res = append(res, DeviceOpResult{
Entity: ent,
})
if len(res) >= int(pageSize) {
if nextCur, err = cb(); err != nil {
return err
}
return datastore.Stop
}
return nil
})
if err != nil {
logging.Errorf(ctx, "Failed to List devices %s", err)
return nil, "", errors.Annotate(err, "ListDevices").Err()
}
if nextCur != nil {
nextPageToken = nextCur.String()
}
return
}
// GetDevicesByModels returns all device entities of models.
//
// TODO(guocb) optimize for performance if needed.
func GetDevicesByModels(ctx context.Context, models []string) (DeviceOpResults, error) {
if len(models) == 0 {
return nil, nil
}
q := datastore.NewQuery(DeviceKind).Ancestor(fakeAcestorKey(ctx))
var devs []*DeviceEntity
if err := datastore.GetAll(ctx, q, &devs); err != nil {
return nil, errors.Annotate(err, "failed to get all devices").Err()
}
modelSet := stringset.NewFromSlice(models...)
result := make([]DeviceOpResult, 0, len(devs))
for _, d := range devs {
var labConfig lab.ChromeOSDevice
if err := d.GetCrosDeviceProto(&labConfig); err != nil {
logging.Errorf(ctx, "failed to unmarshal lab config data for %s", d.Hostname)
continue
}
if !modelSet.Has(labConfig.GetDeviceConfigId().GetModelId().GetValue()) {
continue
}
result = append(result, DeviceOpResult{Entity: d})
}
return DeviceOpResults(result), nil
}
// UpdateDeviceID of the old device to the new device
//
// Changes the timestamp to reflect this change.
func UpdateDeviceID(ctx context.Context, oldDev, newDev string) error {
if oldDev == "" || newDev == "" {
return errors.Reason("UpdateDeviceID, Invalid input").Err()
}
updatedTime := time.Now().UTC()
oldEntity := &DeviceEntity{
ID: DeviceEntityID(oldDev),
Parent: fakeAcestorKey(ctx),
}
newEntity := &DeviceEntity{
ID: DeviceEntityID(newDev),
Parent: fakeAcestorKey(ctx),
Updated: updatedTime,
}
f := func(ctx context.Context) error {
if err := datastore.Get(ctx, oldEntity); err != nil {
return err
}
// Generate lab config
var labConfig lab.ChromeOSDevice
err := proto.Unmarshal(oldEntity.LabConfig, &labConfig)
if err != nil {
return err
}
labConfig.Id = &lab.ChromeOSDeviceID{Value: newDev}
l, err := proto.Marshal(&labConfig)
newEntity.LabConfig = l
// Update Dut State
var state lab.DutState
if err := oldEntity.GetDutStateProto(&state); err != nil {
return err
}
state.Id = &lab.ChromeOSDeviceID{Value: newDev}
mState, err := proto.Marshal(&state)
if err != nil {
return err
}
newEntity.DutState = mState
newEntity.Hostname = oldEntity.Hostname
if err := datastore.Delete(ctx, oldEntity); err != nil {
return err
}
if err := datastore.Put(ctx, newEntity); err != nil {
return err
}
return nil
}
return datastore.RunInTransaction(ctx, f, nil)
}
func updateEntities(ctx context.Context, opResults DeviceOpResults, additionalFilter func()) func(context.Context) error {
maxLen := len(opResults)
entities := make([]*DeviceEntity, maxLen)
for i := range opResults {
entities[i] = opResults[i].Entity
}
f := func(ctx context.Context) error {
if err := datastore.Get(ctx, entities); err != nil {
for i, e := range err.(errors.MultiError) {
opResults[i].logError(errors.Annotate(e, "failed to get entities").Err())
}
}
if additionalFilter != nil {
additionalFilter()
}
entities = []*DeviceEntity{}
entityIndexes := make([]int, 0, maxLen)
updatedTime := time.Now().UTC()
var changes changehistory.Changes
for i, r := range opResults {
if r.Err != nil {
continue
}
c, err := r.Entity.UpdatePayload(r.Data, updatedTime)
if err != nil {
r.logError(errors.Annotate(err, "failed to update payload").Err())
continue
}
changes = append(changes, c...)
entities = append(entities, r.Entity)
entityIndexes = append(entityIndexes, i)
}
if err := changes.SaveToDatastore(ctx); err != nil {
logging.Errorf(ctx, "UpdateEntities: Failed to save change history to datastore: %s", err)
}
if err := datastore.Put(ctx, entities); err != nil {
merr, ok := err.(errors.MultiError)
if !ok {
return err
}
for i, e := range merr {
opResults[entityIndexes[i]].logError(errors.Annotate(e, "failed to save entity to datastore").Err())
}
}
return nil
}
return f
}
// UpdateDeviceSetup updates the content of lab.ChromeOSDevice.
func UpdateDeviceSetup(ctx context.Context, devices []*lab.ChromeOSDevice, assignServoPort bool) (DeviceOpResults, error) {
updatingResults := make(DeviceOpResults, len(devices))
entities := make([]*DeviceEntity, len(devices))
r := newServoHostRegistryFromProtoMsgs(ctx, devices)
for i, d := range devices {
entities[i] = &DeviceEntity{
ID: DeviceEntityID(d.GetId().GetValue()),
Parent: fakeAcestorKey(ctx),
}
updatingResults[i].Data = devices[i]
updatingResults[i].Entity = entities[i]
if dut := devices[i].GetDut(); dut != nil {
oldServo, err := getDutServo(ctx, d)
if err != nil {
return nil, err
}
if err := r.amendServoToLabstation(ctx, dut, oldServo, assignServoPort); err != nil {
return nil, err
}
}
}
f := func(ctx context.Context) error {
err := updateEntities(ctx, updatingResults, nil)(ctx)
if err != nil {
return errors.Annotate(err, "save updated entities").Err()
}
if err := r.saveToDatastore(ctx); err != nil {
return errors.Annotate(err, "save changed labstations caused by updated DUT").Err()
}
return nil
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return updatingResults, err
}
return updatingResults, nil
}
// DutMeta refers to the metadata to be stored for a DUT.
type DutMeta struct {
SerialNumber string
HwID string
DeviceSku string
}
// UpdateDutMeta updates dut serial number and hwid for a given host.
func UpdateDutMeta(ctx context.Context, meta map[string]DutMeta) (DeviceOpResults, error) {
ids := make([]string, 0, len(meta))
for i := range meta {
ids = append(ids, i)
}
results := GetDevicesByIds(ctx, ids)
var updateResults DeviceOpResults
var failedResults DeviceOpResults
for _, r := range results {
if r.Err != nil {
failedResults = append(failedResults, r)
continue
}
var labData lab.ChromeOSDevice
if err := r.Entity.GetCrosDeviceProto(&labData); err != nil {
r.logError(err)
logging.Debugf(ctx, "fail to parse proto for entity: %#v", r.Entity)
failedResults = append(failedResults, r)
continue
}
hid := string(r.Entity.ID)
if labData.SerialNumber == meta[hid].SerialNumber && labData.ManufacturingId != nil &&
labData.GetManufacturingId().GetValue() == meta[hid].HwID &&
labData.GetDeviceConfigId().GetVariantId().GetValue() == meta[hid].DeviceSku {
r.logError(errors.New(fmt.Sprintf("meta is not changed. Old serial number %q, old hwid %q, old device-sku %q", meta[hid].SerialNumber, meta[hid].HwID, meta[hid].DeviceSku)))
failedResults = append(failedResults, r)
continue
}
labData.SerialNumber = meta[hid].SerialNumber
if labData.ManufacturingId == nil {
labData.ManufacturingId = &manufacturing.ConfigID{
Value: meta[hid].HwID,
}
} else {
labData.ManufacturingId.Value = meta[hid].HwID
}
if labData.GetDeviceConfigId() != nil {
if labData.GetDeviceConfigId().GetVariantId().GetValue() != "" && meta[hid].DeviceSku == "" {
// Will remove this check after device sku are populated.
logging.Debugf(ctx, "skip wiping device sku for DUT %s", hid)
} else {
labData.DeviceConfigId.VariantId = &device.VariantId{
Value: meta[hid].DeviceSku,
}
}
}
r := DeviceOpResult{
Entity: &DeviceEntity{
ID: r.Entity.ID,
Parent: fakeAcestorKey(ctx),
},
Data: &labData,
}
updateResults = append(updateResults, r)
}
f := updateEntities(ctx, updateResults, nil)
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return updateResults, err
}
return append(updateResults, failedResults...), nil
}
// LabMeta refers to the metadata to be stored for a DUT.
type LabMeta struct {
ServoType string
SmartUsbhub bool
ServoTopology *lab.ServoTopology
}
// UpdateLabMeta updates servo_type and smart_usbhub flag for a given host.
func UpdateLabMeta(ctx context.Context, meta map[string]LabMeta) (DeviceOpResults, error) {
ids := make([]string, 0, len(meta))
for i := range meta {
ids = append(ids, i)
}
results := GetDevicesByIds(ctx, ids)
var updateResults DeviceOpResults
var failedResults DeviceOpResults
for _, r := range results {
if r.Err != nil {
failedResults = append(failedResults, r)
continue
}
var labData lab.ChromeOSDevice
if err := r.Entity.GetCrosDeviceProto(&labData); err != nil {
r.logError(err)
logging.Debugf(ctx, "fail to parse proto for entity: %#v", r.Entity)
failedResults = append(failedResults, r)
continue
}
hid := string(r.Entity.ID)
if dut := labData.GetDut(); dut != nil {
p := dut.GetPeripherals()
if servo := p.GetServo(); servo != nil {
servo.ServoType = meta[hid].ServoType
servo.ServoTopology = meta[hid].ServoTopology
}
p.SmartUsbhub = meta[hid].SmartUsbhub
}
r := DeviceOpResult{
Entity: &DeviceEntity{
ID: r.Entity.ID,
Parent: fakeAcestorKey(ctx),
},
Data: &labData,
}
updateResults = append(updateResults, r)
}
f := updateEntities(ctx, updateResults, nil)
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return updateResults, err
}
return append(updateResults, failedResults...), nil
}
// UpdateDutsStatus updates dut status of testing related.
func UpdateDutsStatus(ctx context.Context, states []*lab.DutState) (DeviceOpResults, error) {
maxLen := len(states)
updatingResults := make(DeviceOpResults, maxLen)
entities := make([]*DeviceEntity, maxLen)
// The Id must be a valid Id of DeviceUnderTest.
for i, s := range states {
entities[i] = &DeviceEntity{
ID: DeviceEntityID(s.GetId().GetValue()),
Parent: fakeAcestorKey(ctx),
}
updatingResults[i].Data = states[i]
updatingResults[i].Entity = entities[i]
}
filter := func() {
// The returned device must be DeviceUnderTest.
var d lab.ChromeOSDevice
for i, e := range entities {
if err := e.GetCrosDeviceProto(&d); err != nil {
updatingResults[i].logError(errors.Annotate(err, "failed to get proto of entity %v", e).Err())
continue
}
if d.GetDut() == nil {
updatingResults[i].logError(errors.Reason("entity %v isn't a DUT", e).Err())
continue
}
}
}
// We cannot filter entities inside `updateEntities` as the filtering is
// after on the entity retrieving.
f := updateEntities(ctx, updatingResults, filter)
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return updatingResults, err
}
return updatingResults, nil
}
// DeviceProperty specifies some device property.
type DeviceProperty struct {
Hostname string
Pool string
PowerunitName string
PowerunitOutlet string
}
// BatchUpdateDevices updates devices of some specific properties in a batch.
func BatchUpdateDevices(ctx context.Context, duts []*DeviceProperty) error {
var hostnames []string
propertyMap := map[string]*DeviceProperty{}
for _, d := range duts {
hostnames = append(hostnames, d.Hostname)
propertyMap[d.Hostname] = d
}
now := time.Now().UTC()
setRpm := func(rpm *lab.RPM, p *DeviceProperty) {
if p.PowerunitName != "" {
rpm.PowerunitName = p.PowerunitName
}
if p.PowerunitOutlet != "" {
rpm.PowerunitOutlet = p.PowerunitOutlet
}
}
f := func(ctx context.Context) error {
var changes changehistory.Changes
entities := make([]*DeviceEntity, 0, len(duts))
for _, r := range GetDevicesByHostnames(ctx, hostnames).Passed() {
var labConfig lab.ChromeOSDevice
if err := r.Entity.GetCrosDeviceProto(&labConfig); err != nil {
logging.Errorf(ctx, "Cannot get lab config from entity %v: %s", r.Entity, err.Error())
continue
}
p := propertyMap[r.Entity.Hostname]
if dut := labConfig.GetDut(); dut != nil {
if p.Pool != "" {
dut.Pools = []string{p.Pool}
}
if peri := dut.GetPeripherals(); peri == nil {
dut.Peripherals = &lab.Peripherals{
Rpm: &lab.RPM{},
}
} else if peri.GetRpm() == nil {
peri.Rpm = &lab.RPM{}
}
setRpm(dut.GetPeripherals().GetRpm(), p)
}
if labstation := labConfig.GetLabstation(); labstation != nil {
if p.Pool != "" {
labstation.Pools = []string{p.Pool}
}
if labstation.GetRpm() == nil {
labstation.Rpm = &lab.RPM{}
}
setRpm(labstation.GetRpm(), p)
}
c, err := r.Entity.UpdatePayload(&labConfig, now)
if err != nil {
r.logError(errors.Annotate(err, "failed to update payload").Err())
continue
}
changes = append(changes, c...)
entities = append(entities, r.Entity)
}
if err := changes.SaveToDatastore(ctx); err != nil {
logging.Errorf(ctx, "BatchUpdateDevices: Failed to save change history to datastore: %s", err)
}
return datastore.Put(ctx, entities)
}
return datastore.RunInTransaction(ctx, f, nil)
}
// cleanPreDeployFields resets values for the fields re-generated during deployment.
func cleanPreDeployFields(d *lab.DeviceUnderTest) {
servo := d.GetPeripherals().GetServo()
if servo != nil {
servo.ServoType = ""
servo.ServoTopology = nil
}
}