blob: 0bce753a81a892971128844107fd486c368df3b4 [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 changehistory
import (
"context"
"fmt"
"sort"
"time"
"go.chromium.org/chromiumos/infra/proto/go/device"
"go.chromium.org/chromiumos/infra/proto/go/lab"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/auth"
"infra/cros/lab_inventory/utils"
)
// LifeCycleEvent is the type for all life cycle events, e.g. deployment,
// decommission, etc.
type LifeCycleEvent string
const (
// DatastoreKind is the datastore kind.
DatastoreKind = "ChangeHistory"
lifeCycleEventLabel = "LIFE_CYCLE_EVENT"
// LifeCycleDeployment indicates the deployment of a device.
LifeCycleDeployment LifeCycleEvent = "DEPLOYMENT"
// LifeCycleDecomm indicates the decommission of a device.
LifeCycleDecomm LifeCycleEvent = "DECOMMISSION"
)
// Change tracks the change of a ChromeOSDevice.
type Change struct {
_kind string `gae:"$kind,ChangeHistory"`
ID int64 `gae:"$id"`
DeviceID string `gae:",noindex"`
Hostname string `gae:",noindex"`
Label string `gae:",noindex"`
OldValue string `gae:",noindex"`
NewValue string `gae:",noindex"`
Updated time.Time `gae:",noindex"`
ByWhomName string `gae:",noindex"`
ByWhomEmail string `gae:",noindex"`
Comment string `gae:",noindex"`
}
// Changes is a slice of Change.
type Changes []Change
type changeReasonKeyType struct{}
var changeReasonKey changeReasonKeyType
// LogDeployment logs the deployment of a ChromeOSDevice.
func (c *Changes) LogDeployment(id, hostname string) {
deployment := Change{
DeviceID: id,
Hostname: hostname,
Label: lifeCycleEventLabel,
// Set the event to both old and new value so queries might be more
// convenient.
OldValue: string(LifeCycleDeployment),
NewValue: string(LifeCycleDeployment),
}
*c = append(*c, deployment)
}
// LogDecommission logs the decommission of a ChromeOSDevice.
func (c *Changes) LogDecommission(id, hostname string) {
decomm := Change{
DeviceID: id,
Hostname: hostname,
Label: lifeCycleEventLabel,
// Set the event to both old and new value so queries might be more
// convenient.
OldValue: string(LifeCycleDecomm),
NewValue: string(LifeCycleDecomm),
}
*c = append(*c, decomm)
}
func (c *Changes) log(label string, oldValue interface{}, newValue interface{}) {
oldValueStr := fmt.Sprintf("%v", oldValue)
newValueStr := fmt.Sprintf("%v", newValue)
if oldValueStr == newValueStr {
return
}
change := Change{
Label: label,
OldValue: oldValueStr,
NewValue: newValueStr,
}
*c = append(*c, change)
}
// Use installs the value of change reason to context.
func Use(ctx context.Context, reason string) context.Context {
return context.WithValue(ctx, changeReasonKey, reason)
}
// SaveToDatastore saves the changes to datastore.
func (c Changes) SaveToDatastore(ctx context.Context) error {
reason, _ := ctx.Value(changeReasonKey).(string)
now := time.Now().UTC()
user := auth.CurrentUser(ctx)
for i := range c {
c[i].ByWhomName = user.Name
c[i].ByWhomEmail = user.Email
c[i].Comment = reason
if c[i].Updated.IsZero() {
c[i].Updated = now
}
}
if err := datastore.Put(ctx, c); err != nil {
return errors.Annotate(err, "failed to save changes to datastore").Err()
}
return nil
}
// LoadFromDatastore loads all Changes entities from datastore.
func LoadFromDatastore(ctx context.Context) (Changes, error) {
q := datastore.NewQuery(DatastoreKind)
var changes Changes
if err := datastore.GetAll(ctx, q, &changes); err != nil {
return nil, err
}
return changes, nil
}
// FlushDatastore deletes changes dumped to bigquery from datastore.
func FlushDatastore(ctx context.Context, changes Changes) error {
return datastore.Delete(ctx, changes)
}
// LogDutStateChanges logs the change of the given DutState.
func LogDutStateChanges(hostname string, old *lab.DutState, newData *lab.DutState) (changes Changes) {
changes.log("DutState.Servo", old.GetServo(), newData.GetServo())
changes.log("DutState.Chameleon", old.GetChameleon(), newData.GetChameleon())
changes.log("DutState.AudioLoopbackDongle", old.GetAudioLoopbackDongle(), newData.GetAudioLoopbackDongle())
changes.log("DutState.WorkingBluetoothBtpeer", old.GetWorkingBluetoothBtpeer(), newData.GetWorkingBluetoothBtpeer())
changes.log("DutState.Cr50Phase", old.GetCr50Phase(), newData.GetCr50Phase())
changes.log("DutState.Cr50KeyEnv", old.GetCr50KeyEnv(), newData.GetCr50KeyEnv())
// Set id and hostname for all changes.
id := old.GetId().GetValue()
for i := range changes {
changes[i].DeviceID = id
changes[i].Hostname = hostname
}
return
}
// LogChromeOSDeviceChanges logs the change of the given ChromeOSDevice.
func LogChromeOSDeviceChanges(old *lab.ChromeOSDevice, newData *lab.ChromeOSDevice) (changes Changes) {
changes.log("serial_number", old.GetSerialNumber(), newData.GetSerialNumber())
changes.log("manufacturing.config_id", old.GetManufacturingId().GetValue(), newData.GetManufacturingId().GetValue())
changes = append(changes, logDeviceConfigID(old.GetDeviceConfigId(), newData.GetDeviceConfigId())...)
changes = append(changes, logDutChange(old.GetDut(), newData.GetDut())...)
changes = append(changes, logLabstationChange(old.GetLabstation(), newData.GetLabstation())...)
// Set id and hostname for all changes.
id := old.GetId().GetValue()
hostname := utils.GetHostname(newData)
for i := range changes {
changes[i].DeviceID = id
changes[i].Hostname = hostname
}
return
}
// LogLabstationChange logs the labstation changes
func logLabstationChange(old *lab.Labstation, newData *lab.Labstation) (changes Changes) {
if old == nil || newData == nil {
changes.log("Labstation", old, newData)
return
}
changes.log("hostname", old.GetHostname(), newData.GetHostname())
changes = append(changes, logServosChange(old.GetServos(), newData.GetServos())...)
changes = append(changes, logRPMChange(old.GetRpm(), newData.GetRpm())...)
return
}
// LogChromeOSLabstationChange logs the change of the given labstation
func LogChromeOSLabstationChange(old *lab.ChromeOSDevice, newData *lab.ChromeOSDevice) (changes Changes) {
oldL := old.GetLabstation()
newL := newData.GetLabstation()
if oldL == nil || newL == nil {
return
}
changes.log("hostname", oldL.GetHostname(), newL.GetHostname())
changes = append(changes, logServosChange(oldL.GetServos(), newL.GetServos())...)
changes = append(changes, logRPMChange(oldL.GetRpm(), newL.GetRpm())...)
// Set id and hostname for all changes.
id := old.GetId().GetValue()
hostname := utils.GetHostname(newData)
for i := range changes {
changes[i].DeviceID = id
changes[i].Hostname = hostname
}
return
}
func logServosChange(old []*lab.Servo, newData []*lab.Servo) (changes Changes) {
// Sort oldValue and newValue by serial number in alphabet order and then
// compare.
sort.Slice(old, func(i, j int) bool { return old[i].ServoSerial < old[j].ServoSerial })
sort.Slice(newData, func(i, j int) bool { return newData[i].ServoSerial < newData[j].ServoSerial })
i, j := 0, 0
for i < len(old) && j < len(newData) {
switch {
case old[i].ServoSerial == newData[j].ServoSerial:
// Servo attribute change, e.g. servo port, etc.
changes = append(changes, logServoChange(old[i], newData[j])...)
i++
j++
case old[i].ServoSerial < newData[j].ServoSerial:
// removed an old servo.
changes = append(changes, logServoChange(old[i], nil)...)
i++
case old[i].ServoSerial > newData[j].ServoSerial:
// Added a new servo.
changes = append(changes, logServoChange(nil, newData[j])...)
j++
}
}
for ; i < len(old); i++ {
changes = append(changes, logServoChange(old[i], nil)...)
}
for ; j < len(newData); j++ {
changes = append(changes, logServoChange(nil, newData[j])...)
}
return
}
func logServoChange(old *lab.Servo, newData *lab.Servo) (changes Changes) {
if old == nil && newData == nil {
changes.log("servos", old, newData)
return
}
servo := old
if servo == nil {
servo = newData
}
changes.log(fmt.Sprintf("servo.%v", servo.ServoSerial), old, newData)
return
}
func logDeviceConfigID(old *device.ConfigId, newData *device.ConfigId) (changes Changes) {
if old == nil || newData == nil {
changes.log("DeviceConfigID", old, newData)
return
}
changes.log("platform_id", old.GetPlatformId().GetValue(), newData.GetPlatformId().GetValue())
changes.log("model_id", old.GetModelId().GetValue(), newData.GetModelId().GetValue())
changes.log("variant_id", old.GetVariantId().GetValue(), newData.GetVariantId().GetValue())
changes.log("brand_id", old.GetBrandId().GetValue(), newData.GetBrandId().GetValue())
return
}
func logDutChange(old *lab.DeviceUnderTest, newData *lab.DeviceUnderTest) (changes Changes) {
if old == nil || newData == nil {
changes.log("DeviceUnderTest", old, newData)
return
}
changes.log("hostname", old.GetHostname(), newData.GetHostname())
changes.log("critical_pools", old.GetCriticalPools(), newData.GetCriticalPools())
changes.log("pools", old.GetPools(), newData.GetPools())
changes = append(changes, logPeripheralsChange(old.GetPeripherals(), newData.GetPeripherals())...)
return
}
func logPeripheralsChange(old *lab.Peripherals, newData *lab.Peripherals) (changes Changes) {
if old == nil || newData == nil {
changes.log("peripherals", old, newData)
return
}
changes = append(changes, logServoChange(old.GetServo(), newData.GetServo())...)
changes = append(changes, logChameleonChange(old.GetChameleon(), newData.GetChameleon())...)
changes = append(changes, logRPMChange(old.GetRpm(), newData.GetRpm())...)
changes = append(changes, logConnectedCameraChange(old.GetConnectedCamera(), newData.GetConnectedCamera())...)
changes = append(changes, logAudioChange(old.GetAudio(), newData.GetAudio())...)
changes = append(changes, logWifiChange(old.GetWifi(), newData.GetWifi())...)
changes = append(changes, logTouchChange(old.GetTouch(), newData.GetTouch())...)
changes.log("carrier", old.GetCarrier(), newData.GetCarrier())
changes.log("camerabox", old.GetCamerabox(), newData.GetCamerabox())
changes.log("chaos", old.GetChaos(), newData.GetChaos())
changes.log("cable", old.GetCable(), newData.GetCable())
return
}
func logChameleonChange(old *lab.Chameleon, newData *lab.Chameleon) (changes Changes) {
changes.log("chameleon", old, newData)
return
}
func logRPMChange(old *lab.RPM, newData *lab.RPM) (changes Changes) {
changes.log("powerunit_name", old.GetPowerunitName(), newData.GetPowerunitName())
changes.log("powerunit_outlet", old.GetPowerunitOutlet(), newData.GetPowerunitOutlet())
return
}
func logConnectedCameraChange(old []*lab.Camera, newData []*lab.Camera) (changes Changes) {
changes.log("connected_camera", old, newData)
return
}
func logAudioChange(old *lab.Audio, newData *lab.Audio) (changes Changes) {
changes.log("audio_box", old.GetAudioBox(), newData.GetAudioBox())
changes.log("atrus", old.GetAtrus(), newData.GetAtrus())
return
}
func logWifiChange(old *lab.Wifi, newData *lab.Wifi) (changes Changes) {
changes.log("wificell", old.GetWificell(), newData.GetWificell())
changes.log("antenna_conn", old.GetAntennaConn(), newData.GetAntennaConn())
changes.log("router", old.GetRouter(), newData.GetRouter())
return
}
func logTouchChange(old *lab.Touch, newData *lab.Touch) (changes Changes) {
changes.log("mimo", old.GetMimo(), newData.GetMimo())
return
}