blob: 20a3af959e87fafbf929fbd8aec003b9cea94160 [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
import (
"context"
"fmt"
"time"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"go.chromium.org/chromiumos/infra/proto/go/lab"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/gae/service/datastore"
"infra/cros/lab_inventory/changehistory"
inv "infra/cros/lab_inventory/protos"
"infra/cros/lab_inventory/utils"
fleet "infra/libs/fleet/protos"
ufs "infra/libs/fleet/protos/go"
)
// DeviceEntityID represents the ID of a device. We prefer use asset id as the id.
type DeviceEntityID string
// DeviceKind is the datastore entity kind for Device entities.
const DeviceKind string = "Device"
// DeviceEntity is a datastore entity that tracks a device.
type DeviceEntity struct {
_kind string `gae:"$kind,Device"`
ID DeviceEntityID `gae:"$id"`
Hostname string
LabConfig []byte `gae:",noindex"`
DutState []byte `gae:",noindex"`
Updated time.Time
Parent *datastore.Key `gae:"$parent"`
}
// GetCrosDeviceProto gets the unmarshaled proto message data.
func (e *DeviceEntity) GetCrosDeviceProto(p *lab.ChromeOSDevice) error {
if err := proto.Unmarshal(e.LabConfig, p); err != nil {
return err
}
return nil
}
// GetDutStateProto gets the unmarshaled proto message data.
func (e *DeviceEntity) GetDutStateProto(p *lab.DutState) error {
if err := proto.Unmarshal(e.DutState, p); err != nil {
return err
}
return nil
}
func (e *DeviceEntity) updateLabConfig(p *lab.ChromeOSDevice) (changehistory.Changes, error) {
var oldMsg lab.ChromeOSDevice
if err := proto.Unmarshal(e.LabConfig, &oldMsg); err != nil {
return nil, err
}
if proto.Equal(p, &oldMsg) {
// Do nothing if the proto message is identical.
return nil, nil
}
data, err := proto.Marshal(p)
if err != nil {
return nil, err
}
changes := changehistory.LogChromeOSDeviceChanges(&oldMsg, p)
e.LabConfig = data
e.Hostname = utils.GetHostname(p)
return changes, nil
}
func (e *DeviceEntity) updateDutState(p *lab.DutState) (changehistory.Changes, error) {
var oldMsg lab.DutState
if err := proto.Unmarshal(e.DutState, &oldMsg); err != nil {
return nil, err
}
if proto.Equal(p, &oldMsg) {
// Do nothing if the proto message is identical.
return nil, nil
}
data, err := proto.Marshal(p)
if err != nil {
return nil, err
}
changes := changehistory.LogDutStateChanges(e.Hostname, &oldMsg, p)
e.DutState = data
return changes, nil
}
// UpdatePayload sets the proto data to the entity.
func (e *DeviceEntity) UpdatePayload(p proto.Message, t time.Time) (changes changehistory.Changes, err error) {
if v, ok := p.(*lab.ChromeOSDevice); ok {
changes, err = e.updateLabConfig(v)
} else if v, ok := p.(*lab.DutState); ok {
changes, err = e.updateDutState(v)
}
e.Updated = t
return
}
func (e *DeviceEntity) String() string {
return fmt.Sprintf("<%s:%s>", e.Hostname, e.ID)
}
/* Asset Entity and helper functions*/
// AssetEntityName is the datastore entity kind for Asset entities.
const AssetEntityName string = "Asset"
// AssetEntity is a datastore entity that tracks the asset.
type AssetEntity struct {
_kind string `gae:"$kind,Asset"`
ID string `gae:"$id"`
Lab string
Location []byte `gae:",noindex"`
Parent *datastore.Key `gae:"$parent"`
}
// AssetStateEntityName is the datastore entity kind for Asset state entities.
const AssetStateEntityName string = "AssetState"
// AssetStateEntity is the datastore that tracks the asset state.
type AssetStateEntity struct {
_kind string `gae:"$kind,AssetState"`
ID string `gae:"$id"`
State fleet.AssetState `gae:",noindex"`
Updated time.Time
Parent *datastore.Key `gae:"$parent"`
}
func (e *AssetEntity) String() string {
return fmt.Sprintf("<%s>:%s", e.ID, e.Lab)
}
// NewAssetEntity creates an AssetEntity object from ChopsAsset object
func NewAssetEntity(a *fleet.ChopsAsset, parent *datastore.Key) (*AssetEntity, error) {
if a.GetId() == "" {
return nil, errors.Reason("Missing asset tag").Err()
}
location, err := proto.Marshal(a.GetLocation())
if err != nil {
return nil, err
}
return &AssetEntity{
ID: a.GetId(),
Lab: a.GetLocation().GetLab(),
Location: location,
Parent: parent,
}, nil
}
// NewAssetStateEntity creates an AssetStateEntity object based on input.
func NewAssetStateEntity(a *fleet.ChopsAsset, state fleet.State, updated time.Time, parent *datastore.Key) (*AssetStateEntity, error) {
if a.GetId() == "" {
return nil, errors.Reason("Missing asset tag").Err()
}
return &AssetStateEntity{
ID: a.GetId(),
State: fleet.AssetState{
Id: a.GetId(),
State: state,
},
Updated: updated,
Parent: parent,
}, nil
}
// ToChopsAsset returns a ChopsAsset object
func (e *AssetEntity) ToChopsAsset() (*fleet.ChopsAsset, error) {
var location ufs.Location
err := proto.Unmarshal(e.Location, &location)
return &fleet.ChopsAsset{
Id: e.ID,
Location: &location,
}, err
}
/* Asset Entity and helper functions end */
/* Asset Info and helper funtions */
// AssetInfoEntity is a datastore entity that tracks the asset info from HaRT.
type AssetInfoEntity struct {
_kind string `gae:"$kind,AssetInfo"`
AssetTag string `gae:"$id"`
Info ufs.AssetInfo `gae:",noindex"`
}
// AssetInfoEntityKind is the datastore entity kind for AssetInfo entities.
const AssetInfoEntityKind = "AssetInfo"
// NewAssetInfo creates an AssetInfoEntity object from AssetInfo object
func NewAssetInfo(a *ufs.AssetInfo) (*AssetInfoEntity, error) {
if a.GetAssetTag() == "" {
return nil, errors.Reason("Missing asset tag").Err()
}
return &AssetInfoEntity{
AssetTag: a.GetAssetTag(),
Info: *a,
}, nil
}
/* Asset Info and helper functions end */
/* Device Manual Repair Record Entity and helper functions */
// DeviceManualRepairRecordEntity is a datastore entity that tracks a manual
// repair record of a device.
//
// Possible RepairState based on proto enum:
// STATE_INVALID = 0;
// STATE_NOT_STARTED = 1;
// STATE_IN_PROGRESS = 2;
// STATE_COMPLETED = 3;
type DeviceManualRepairRecordEntity struct {
_kind string `gae:"$kind,DeviceManualRepairRecord"`
ID string `gae:"$id"`
Hostname string `gae:"hostname"`
AssetTag string `gae:"asset_tag"`
RepairState string `gae:"repair_state"`
UserLdap string `gae:"user_ldap"`
UpdatedTime time.Time `gae:"updated_time"`
Content []byte `gae:",noindex"`
}
// DeviceManualRepairRecordEntityKind is the datastore entity kind for
// DeviceManualRepairRecord entities.
const DeviceManualRepairRecordEntityKind = "DeviceManualRepairRecord"
// NewDeviceManualRepairRecordEntity creates a new
// DeviceManualRepairRecordEntity from a DeviceManualRepairRecord object.
func NewDeviceManualRepairRecordEntity(r *inv.DeviceManualRepairRecord) (*DeviceManualRepairRecordEntity, error) {
hostname := r.GetHostname()
assetTag := r.GetAssetTag()
createdTime := ptypes.TimestampString(r.GetCreatedTime())
// Set default updatedTime to createdTime.
updatedTime, err := ptypes.Timestamp(r.GetCreatedTime())
if err != nil {
return nil, err
}
if hostname == "" {
return nil, errors.Reason("Hostname cannot be empty").Err()
} else if assetTag == "" {
return nil, errors.Reason("Asset Tag cannot be empty").Err()
} else if createdTime == "" {
return nil, errors.Reason("Created Time cannot be empty").Err()
}
content, err := proto.Marshal(r)
if err != nil {
return nil, err
}
id, err := GenerateRepairRecordID(hostname, assetTag, createdTime)
if err != nil {
return nil, err
}
return &DeviceManualRepairRecordEntity{
ID: id,
Hostname: hostname,
AssetTag: assetTag,
RepairState: r.GetRepairState().String(),
UserLdap: r.GetUserLdap(),
UpdatedTime: updatedTime,
Content: content,
}, nil
}
// UpdateDeviceManualRepairRecordEntity sets the proto data to the entity.
func (e *DeviceManualRepairRecordEntity) UpdateDeviceManualRepairRecordEntity(r *inv.DeviceManualRepairRecord) error {
var oldMsg inv.DeviceManualRepairRecord
if err := proto.Unmarshal(e.Content, &oldMsg); err != nil {
return err
}
if r.GetHostname() == "" {
return errors.Reason("Hostname cannot be empty").Err()
} else if r.GetAssetTag() == "" {
return errors.Reason("Asset Tag cannot be empty").Err()
} else if ptypes.TimestampString(r.GetCreatedTime()) == "" {
return errors.Reason("Created Time cannot be empty").Err()
}
// Update record if the message is different from the old one.
if !proto.Equal(r, &oldMsg) {
data, err := proto.Marshal(r)
if err != nil {
return err
}
e.RepairState = r.GetRepairState().String()
e.Content = data
e.UpdatedTime, err = ptypes.Timestamp(r.GetUpdatedTime())
if err != nil {
return err
}
}
return nil
}
// GenerateRepairRecordID returns the predefined ID format of
// $hostname-$assetTag-$createdTime
func GenerateRepairRecordID(hostname string, assetTag string, createdTime string) (string, error) {
var err error
if hostname == "" {
err = errors.Reason("Hostname cannot be empty").Err()
} else if assetTag == "" {
err = errors.Reason("Asset Tag cannot be empty").Err()
} else if createdTime == "" {
err = errors.Reason("Created Time cannot be empty").Err()
}
return hostname + "-" + assetTag + "-" + createdTime, err
}
/* Device Manual Repair Record Entity and helper functions end */
/* Data layer entities and helpers for Manual Repair start */
// MRMetadataEntity is a datastore entity that tracks miscellaneous information
// used for data operations for a device repair record.
type MRMetadataEntity struct {
_kind string `gae:"$kind,MRMetadata"`
ID string `gae:"$id"`
LastScanned time.Time
}
// MRMetadataEntityKind is the datastore entity kind for
// MRMetadata entities.
const MRMetadataEntityKind = "MRMetadata"
// MRLastScannedID is the ID for the entity that will always store the last
// scanned time.
const MRLastScannedID = "ManualRepairLastScanned"
// GetLastScannedTime returns a list of 1 with the entity with the latest
// LastScanned time.
func GetLastScannedTime(ctx context.Context) (*MRMetadataEntity, error) {
e := MRMetadataEntity{
ID: MRLastScannedID,
}
if err := datastore.Get(ctx, &e); err != nil {
return nil, err
}
return &e, nil
}
// SaveLastScannedTime saves the newest LastScanned time into an entity. If one
// does not exist, it will create a new entity in the Datastore.
func SaveLastScannedTime(ctx context.Context, lastScannedTime time.Time) error {
e := &MRMetadataEntity{
ID: MRLastScannedID,
LastScanned: lastScannedTime,
}
if err := datastore.Put(ctx, e); err != nil {
return err
}
return nil
}
/* Data layer entities and helpers for Manual Repair end */