From dff3ee0cef6224817a8ca4069155956884369b4d Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 16 Nov 2022 14:10:40 +0100 Subject: [PATCH] internal/inventory: provides an interface to inventory backends This includes a functional serverservice implementation the YAML implementation is a WIP. --- internal/inventory/interface.go | 59 +++ internal/inventory/mock.go | 65 ++++ internal/inventory/serverservice.go | 431 ++++++++++++++++++++++ internal/inventory/serverservice_attrs.go | 297 +++++++++++++++ internal/inventory/yaml.go | 74 ++++ 5 files changed, 926 insertions(+) create mode 100644 internal/inventory/interface.go create mode 100644 internal/inventory/mock.go create mode 100644 internal/inventory/serverservice.go create mode 100644 internal/inventory/serverservice_attrs.go create mode 100644 internal/inventory/yaml.go diff --git a/internal/inventory/interface.go b/internal/inventory/interface.go new file mode 100644 index 00000000..bbef7d12 --- /dev/null +++ b/internal/inventory/interface.go @@ -0,0 +1,59 @@ +package inventory + +import ( + "context" + + "github.com/metal-toolbox/flasher/internal/model" +) + +type Inventory interface { + // DevicesForFwInstall returns a list of devices eligible for firmware installation. + DevicesForFwInstall(ctx context.Context, limit int) ([]DeviceInventory, error) + + // DeviceByID returns device attributes by its identifier + DeviceByID(ctx context.Context, id string) (*DeviceInventory, error) + + // AcquireDevice looks up a device by its identifier and flags or locks it for an update. + // + // - The implementation is to check if the device is a eligible based its status or other non-firmware inventory attributes. + // - The locking mechnism is left to the implementation. + AcquireDevice(ctx context.Context, deviceID, workerID string) (DeviceInventory, error) + + // ReleaseDevice looks up a device by its identifier and releases any locks held on the device. + // The lock release mechnism is left to the implementation. + ReleaseDevice(ctx context.Context, id string) error + + // SetFlasherAttributes - sets the firmware install attributes for a device. + SetFlasherAttributes(ctx context.Context, deviceID string, attrs *FwInstallAttributes) error + + // FlasherAttributes - gets the firmware install attributes to the given value for a device. + FlasherAttributes(ctx context.Context, deviceID string) (FwInstallAttributes, error) + + // DeleteFlasherAttributes - removes the firmware install attributes from a device. + DeleteFlasherAttributes(ctx context.Context, deviceID string) error + + // FirmwareInstalled returns the component installed firmware versions + FirmwareInstalled(ctx context.Context, deviceID string) (model.Components, error) + + // FirmwareByDeviceVendorModel returns the firmware for the device vendor, model. + FirmwareByDeviceVendorModel(ctx context.Context, deviceVendor, deviceModel string) ([]model.Firmware, error) +} + +// FwInstallAttributes is the flasher server service attribute stored in serverservice +type FwInstallAttributes struct { + model.TaskParameters `json:"parameters,omitempty"` + + FlasherTaskID string `json:"flasher_task_id,omitempty"` + Status string `json:"status,omitempty"` + Info string `json:"info,omitempty"` + Requester string `json:"requester,omitempty"` + WorkerID string `json:"worker_id,omitempty"` +} + +// DeviceInventory objects are returned by the inventory package +type DeviceInventory struct { + Device model.Device `json:"device"` + Components model.Components `json:"components"` + FwInstallAttributes FwInstallAttributes `json:"fw_install_attributes,omitempty"` + Firmware []model.Firmware `json:"firmware,omitempty"` +} diff --git a/internal/inventory/mock.go b/internal/inventory/mock.go new file mode 100644 index 00000000..086834df --- /dev/null +++ b/internal/inventory/mock.go @@ -0,0 +1,65 @@ +package inventory + +import ( + "context" + + "github.com/metal-toolbox/flasher/internal/fixtures" + "github.com/metal-toolbox/flasher/internal/model" +) + +type Mock struct{} + +func NewMockInventory() (Inventory, error) { + return &Mock{}, nil +} + +// DeviceByID returns device attributes by its identifier +func (s *Mock) DeviceByID(ctx context.Context, id string) (*DeviceInventory, error) { + return nil, nil +} + +func (s *Mock) DevicesForFwInstall(ctx context.Context, limit int) ([]DeviceInventory, error) { + devices := []DeviceInventory{ + {Device: fixtures.Devices[fixtures.Device1.String()]}, + {Device: fixtures.Devices[fixtures.Device2.String()]}, + } + + return devices, nil +} + +func (s *Mock) AcquireDevice(ctx context.Context, deviceID, workerID string) (DeviceInventory, error) { + // updates the server service attribute + // - the device should not have any active flasher tasks + // - the device state should be maintenance + + return DeviceInventory{Device: fixtures.Devices[fixtures.Device1.String()]}, nil +} + +func (s *Mock) FirmwareByDeviceVendorModel(ctx context.Context, deviceVendor, deviceModel string) ([]model.Firmware, error) { + return fixtures.Firmware, nil +} + +// FlasherAttributes - gets the firmware install attributes for the device. +func (s *Mock) FlasherAttributes(ctx context.Context, deviceID string) (FwInstallAttributes, error) { + return FwInstallAttributes{}, nil +} + +// SetFlasherAttributes - sets the firmware install attributes to the given values on a device. +func (s *Mock) SetFlasherAttributes(ctx context.Context, deviceID string, attrs *FwInstallAttributes) error { + return nil +} + +// DeleteFlasherAttributes - removes the firmware install attributes from a device. +func (s *Mock) DeleteFlasherAttributes(ctx context.Context, deviceID string) error { + return nil +} + +// ReleaseDevice looks up a device by its identifier and releases any locks held on the device. +func (s *Mock) ReleaseDevice(ctx context.Context, id string) error { + return nil +} + +// FirmwareInstalled returns the component installed firmware versions +func (s *Mock) FirmwareInstalled(ctx context.Context, deviceID string) (model.Components, error) { + return nil, nil +} diff --git a/internal/inventory/serverservice.go b/internal/inventory/serverservice.go new file mode 100644 index 00000000..91b8bf76 --- /dev/null +++ b/internal/inventory/serverservice.go @@ -0,0 +1,431 @@ +package inventory + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + sservice "go.hollow.sh/serverservice/pkg/api/v1" + "golang.org/x/exp/slices" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "github.com/metal-toolbox/flasher/internal/model" + "github.com/pkg/errors" +) + +const ( + // serverservice attribute namespace for flasher task information + serverAttributeNSFlasherTask = "sh.hollow.flasher.task" + + // serverservice attribute namespace for device vendor, model, serial attributes + serverAttributeNSVendor = "sh.hollow.alloy.server_vendor_attributes" + + // serverservice attribute namespace for the BMC address + serverAttributeNSBmcAddress = "sh.hollow.bmc_info" + + // serverservice attribute namespace for firmware set labels + firmwareAttributeNSFirmwareSetLabels = "sh.hollow.firmware_set.labels" + + component = "inventory.serverservice" +) + +var ( + ErrNoAttributes = errors.New("no flasher attribute found") + ErrAttributeList = errors.New("error in serverservice flasher attribute list") + ErrAttributeCreate = errors.New("error in serverservice flasher attribute create") + ErrAttributeUpdate = errors.New("error in serverservice flasher attribute update") + ErrVendorModelAttributes = errors.New("device vendor, model attributes not found in serverservice") + ErrDeviceStatus = errors.New("error serverservice device status") + + ErrDeviceID = errors.New("device UUID error") + + // ErrBMCAddress is returned when an error occurs in the BMC address lookup. + ErrBMCAddress = errors.New("error in server BMC Address") + + // ErrDeviceState is returned when an error occurs in the device state lookup. + ErrDeviceState = errors.New("error in device state") + + // ErrServerserviceAttrObj is retuned when an error occurred in unpacking the attribute. + ErrServerserviceAttrObj = errors.New("serverservice attribute error") + + // ErrServerserviceVersionedAttrObj is retuned when an error occurred in unpacking the versioned attribute. + ErrServerserviceVersionedAttrObj = errors.New("serverservice versioned attribute error") + + // ErrServerserviceQuery is returned when a server service query fails. + ErrServerserviceQuery = errors.New("serverservice query returned error") + + ErrFirmwareSetLookup = errors.New("firmware set error") +) + +type Serverservice struct { + config *model.Config + // componentSlugs map[string]string + client *sservice.Client + logger *logrus.Logger +} + +func NewServerserviceInventory(ctx context.Context, config *model.Config, logger *logrus.Logger) (Inventory, error) { + // TODO: add helper method for OIDC auth + client, err := sservice.NewClientWithToken("fake", config.Endpoint, nil) + if err != nil { + return nil, err + } + + serverservice := &Serverservice{ + client: client, + config: config, + // componentSlugs: map[string]string{}, + logger: logger, + } + + // cache component type slugs + // componentTypes, _, err := client.ListServerComponentTypes(ctx, nil) + // if err != nil { + // return nil, errors.Wrap(ErrServerserviceQuery, err.Error()) + // } + // + // for _, ct := range componentTypes { + // serverservice.componentSlugs[ct.ID] = ct.Slug + // } + + return serverservice, nil +} + +// DeviceByID returns device attributes by its identifier +func (s *Serverservice) DeviceByID(ctx context.Context, id string) (*DeviceInventory, error) { + deviceUUID, err := uuid.Parse(id) + if err != nil { + return nil, errors.Wrap(ErrDeviceID, err.Error()+id) + } + + device, installAttributes, err := s.deviceWithFwInstallAttributes(ctx, id) + if err != nil && !errors.Is(err, ErrVendorModelAttributes) { + return nil, err + } + + if device == nil { + return nil, errors.Wrap(ErrServerserviceQuery, "got nil device object") + } + + components, _, err := s.client.GetComponents(ctx, deviceUUID, nil) + if err != nil { + return nil, errors.Wrap(ErrServerserviceQuery, "device component query error: "+err.Error()) + } + + inventoryDevice := &DeviceInventory{ + Device: *device, + Components: s.fromServerserviceComponents(device.Vendor, device.Model, components), + } + + if installAttributes != nil { + inventoryDevice.FwInstallAttributes = *installAttributes + } + + return inventoryDevice, nil +} + +func (s *Serverservice) DevicesForFwInstall(ctx context.Context, limit int) ([]DeviceInventory, error) { + params := &sservice.ServerListParams{ + FacilityCode: s.config.FacilityCode, + AttributeListParams: []sservice.AttributeListParams{ + { + Namespace: serverAttributeNSFlasherTask, + Keys: []string{"status"}, + Operator: sservice.OperatorEqual, + Value: string(model.StateRequested), + }, + }, + } + + found, _, err := s.client.List(ctx, params) + if err != nil { + return nil, err + } + + if len(found) == 0 { + return nil, nil + } + + return s.convertServersToInventoryDeviceObjs(ctx, found) +} + +func (s *Serverservice) convertServersToInventoryDeviceObjs(ctx context.Context, servers []sservice.Server) ([]DeviceInventory, error) { + devices := make([]DeviceInventory, 0, len(servers)) + + for _, server := range servers { + device, fwInstallAttributes, err := s.deviceWithFwInstallAttributes(ctx, server.UUID.String()) + if err != nil && !errors.Is(err, ErrVendorModelAttributes) { + s.logger.WithFields( + logrus.Fields{ + "component": component, + "deviceID": server.UUID.String(), + "err": err.Error(), + }, + ).Warn("error in device attribute lookup") + + continue + } + + // check device state is acceptable for firmware install + if device.State != "" && !slices.Contains(s.config.DeviceStates, device.State) { + s.logger.WithFields( + logrus.Fields{ + "component": component, + "deviceID": server.UUID.String(), + }, + ).Trace("device skipped, inventory state is not one of: ", strings.Join(s.config.DeviceStates, ", ")) + + continue + } + + device.ID = server.UUID + + devices = append( + devices, + DeviceInventory{Device: *device, FwInstallAttributes: *fwInstallAttributes}, + ) + } + + return devices, nil +} + +func (s *Serverservice) AcquireDevice(ctx context.Context, deviceID, workerID string) (DeviceInventory, error) { + // updates the server service attribute + // - the device should not have any active flasher tasks + // - the device state should be maintenance + device, fwInstallAttributes, err := s.deviceWithFwInstallAttributes(ctx, deviceID) + if err != nil && !errors.Is(err, ErrVendorModelAttributes) { + return DeviceInventory{}, errors.Wrap(ErrAttributeList, err.Error()) + } + + fwInstallAttributes.Status = string(model.StateQueued) + fwInstallAttributes.WorkerID = workerID + + if err := s.SetFlasherAttributes(ctx, deviceID, fwInstallAttributes); err != nil { + return DeviceInventory{}, errors.Wrap(ErrAttributeUpdate, err.Error()) + } + + return DeviceInventory{Device: *device, FwInstallAttributes: *fwInstallAttributes}, nil +} + +// ReleaseDevice looks up a device by its identifier and releases any locks held on the device. +// The lock release mechnism is left to the implementation. +func (s *Serverservice) ReleaseDevice(ctx context.Context, id string) error { + return nil +} + +func (s *Serverservice) FirmwareSetByDeviceVendorModel(ctx context.Context, deviceVendor, deviceModel string) ([]model.Firmware, error) { + // looks up device inventory + // looks up firmware + + return nil, nil +} + +// FlasherAttributes - gets the firmware install attributes for the device. +func (s *Serverservice) FlasherAttributes(ctx context.Context, deviceID string) (FwInstallAttributes, error) { + params := FwInstallAttributes{} + + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return params, errors.Wrap(ErrDeviceID, err.Error()+deviceID) + } + + // lookup flasher task attribute + attributes, _, err := s.client.ListAttributes(ctx, deviceUUID, nil) + if err != nil { + return params, errors.Wrap(err, ErrAttributeList.Error()) + } + + // update existing task attribute + foundAttributes := findAttribute(serverAttributeNSFlasherTask, attributes) + if foundAttributes == nil { + return params, ErrNoAttributes + } + + installAttributes := FwInstallAttributes{} + + if err := json.Unmarshal(foundAttributes.Data, &installAttributes); err != nil { + return params, errors.Wrap(ErrAttributeList, err.Error()) + } + + return installAttributes, nil +} + +// SetDeviceFwInstallTaskAttributes - sets the firmware install attributes to the given values on a device. +func (s *Serverservice) SetFlasherAttributes(ctx context.Context, deviceID string, newTaskAttrs *FwInstallAttributes) error { + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return err + } + + // lookup flasher task attribute + attributes, _, err := s.client.ListAttributes(ctx, deviceUUID, nil) + if err != nil { + return err + } + + taskAttrs, err := s.flasherTaskAttribute(attributes) + if err != nil { + return err + } + + // create firmware install attributes when theres none. + if taskAttrs == nil { + // create new task attribute + return s.createFlasherAttributes(ctx, deviceUUID, newTaskAttrs) + } + + // device already flagged for install + if taskAttrs.Status == string(model.StateRequested) && newTaskAttrs.Status == string(model.StateRequested) { + return errors.Wrap( + ErrAttributeUpdate, + "device already flagged for firmware install", + ) + } + + // update firmware install attributes + return s.updateFlasherAttributes(ctx, deviceUUID, taskAttrs, newTaskAttrs) +} + +// DeleteFlasherAttributes - removes the firmware install attributes from a device. +func (s *Serverservice) DeleteFlasherAttributes(ctx context.Context, deviceID string) error { + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return err + } + + _, err = s.client.DeleteAttributes(ctx, deviceUUID, serverAttributeNSFlasherTask) + return err +} + +// FirmwareInstalled returns the component installed firmware versions +func (s *Serverservice) FirmwareInstalled(ctx context.Context, deviceID string) (model.Components, error) { + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return nil, errors.Wrap(ErrDeviceID, err.Error()+": "+deviceID) + } + + components, _, err := s.client.GetComponents(ctx, deviceUUID, nil) + if err != nil { + return nil, err + } + + converted := model.Components{} + + for _, component := range components { + if len(component.VersionedAttributes) == 0 { + s.logger.WithFields(logrus.Fields{ + "slug": component.ComponentTypeSlug, + "deviceID": deviceID, + }).Trace("component skipped - no versioned attributes") + + continue + } + + installed, err := installedFirmwareFromVA(component.VersionedAttributes[1]) + if err != nil { + s.logger.WithFields(logrus.Fields{ + "slug": component.ComponentTypeSlug, + "deviceID": deviceID, + "err": err.Error(), + }).Trace("component skipped - versioned attribute error") + } + + c := &model.Component{ + Slug: component.ComponentTypeSlug, + Vendor: component.Vendor, + Model: component.Model, + Serial: component.Serial, + FirmwareInstalled: installed, + } + + converted = append(converted, c) + } + + return converted, nil +} + +//func cacheServerComponentTypes(ctx context.Context) error { +// s.componentSlugs = map[string]string{} +// +// return nil +//} +// + +// FirmwareByDeviceVendorModel returns the firmware for the device vendor, model. +func (s *Serverservice) FirmwareByDeviceVendorModel(ctx context.Context, deviceVendor, deviceModel string) ([]model.Firmware, error) { + // lookup flasher task attribute + params := &sservice.ComponentFirmwareSetListParams{ + AttributeListParams: []sservice.AttributeListParams{ + { + Namespace: firmwareAttributeNSFirmwareSetLabels, + Keys: []string{"model"}, + Operator: "eq", + Value: deviceModel, + }, + { + Namespace: firmwareAttributeNSFirmwareSetLabels, + Keys: []string{"vendor"}, + Operator: "eq", + Value: deviceVendor, + }, + }, + } + + firmwaresets, _, err := s.client.ListServerComponentFirmwareSet(ctx, params) + if err != nil { + return nil, errors.Wrap(ErrServerserviceQuery, err.Error()) + } + + if len(firmwaresets) == 0 { + return nil, errors.Wrap( + ErrFirmwareSetLookup, + fmt.Sprintf( + "lookup by device vendor: %s, model: %s returned no firmware set", + deviceVendor, + deviceModel, + ), + ) + } + + if len(firmwaresets) > 1 { + return nil, errors.Wrap( + ErrFirmwareSetLookup, + fmt.Sprintf( + "lookup by device vendor: %s, model: %s returned multiple firmware sets, expected one", + deviceVendor, + deviceModel, + ), + ) + } + + if len(firmwaresets[0].ComponentFirmware) == 0 { + return nil, errors.Wrap( + ErrFirmwareSetLookup, + fmt.Sprintf( + "lookup by device vendor: %s, model: %s returned firmware set with no component firmware", + deviceVendor, + deviceModel, + ), + ) + } + + found := []model.Firmware{} + for _, set := range firmwaresets { + for _, firmware := range set.ComponentFirmware { + found = append(found, model.Firmware{ + Vendor: strings.ToLower(firmware.Vendor), + Model: strings.ToLower(firmware.Model), + Version: firmware.Version, + FileName: firmware.Filename, + ComponentSlug: strings.ToLower(firmware.Component), + Checksum: firmware.Checksum, + }) + } + } + + return found, nil +} diff --git a/internal/inventory/serverservice_attrs.go b/internal/inventory/serverservice_attrs.go new file mode 100644 index 00000000..0ef00349 --- /dev/null +++ b/internal/inventory/serverservice_attrs.go @@ -0,0 +1,297 @@ +package inventory + +import ( + "context" + "encoding/json" + "net" + + "github.com/bmc-toolbox/common" + "github.com/metal-toolbox/flasher/internal/model" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + sservice "go.hollow.sh/serverservice/pkg/api/v1" + + "github.com/google/uuid" +) + +// versionedAttributeFirmware is the format in which the firmware data is present in serverservice. +type versionedAttributeFirmware struct { + Firmware *common.Firmware `json:"firmware,omitempty"` +} + +func installedFirmwareFromVA(va sservice.VersionedAttributes) (string, error) { + data := &common.Firmware{} + + if err := json.Unmarshal(va.Data, data); err != nil { + return "", errors.Wrap(ErrServerserviceVersionedAttrObj, "failed to unpack Firmware data: "+err.Error()) + } + + if data.Installed == "" { + return "", errors.Wrap(ErrServerserviceVersionedAttrObj, "installed firmware version unknown") + } + + return data.Installed, nil +} + +func findAttribute(ns string, attributes []sservice.Attributes) *sservice.Attributes { + for _, attribute := range attributes { + if attribute.Namespace == ns { + return &attribute + } + } + + return nil +} + +func findVersionedAttribute(ns string, attributes []sservice.VersionedAttributes) *sservice.VersionedAttributes { + for _, attribute := range attributes { + if attribute.Namespace == ns { + return &attribute + } + } + + return nil +} + +func (s *Serverservice) flasherTaskAttribute(attributes []sservice.Attributes) (*FwInstallAttributes, error) { + // update existing task attribute + found := findAttribute(serverAttributeNSFlasherTask, attributes) + if found == nil { + return nil, nil + } + + taskAttrs := &FwInstallAttributes{} + if err := json.Unmarshal(found.Data, taskAttrs); err != nil { + return nil, err + } + + return taskAttrs, nil +} + +// deviceState returns the server state attribute value from the configured DeviceStateAttributeNS +func (s *Serverservice) deviceStateAttribute(attributes []sservice.Attributes) (string, error) { + var deviceState string + + deviceStateAttribute := findAttribute(s.config.DeviceStateAttributeNS, attributes) + if deviceStateAttribute == nil { + return deviceState, nil + } + + data := map[string]string{} + if err := json.Unmarshal(deviceStateAttribute.Data, &data); err != nil { + return deviceState, errors.Wrap(ErrDeviceState, err.Error()) + } + + if data[s.config.DeviceStateAttributeKey] == "" { + return deviceState, errors.Wrap(ErrDeviceState, "device state attribute is not set") + } + + return data[s.config.DeviceStateAttributeKey], nil +} + +func (s *Serverservice) bmcAddressFromAttributes(attributes []sservice.Attributes) (net.IP, error) { + ip := net.IP{} + + bmcAttribute := findAttribute(serverAttributeNSBmcAddress, attributes) + if bmcAttribute == nil { + return ip, errors.Wrap(ErrBMCAddress, "not found: "+serverAttributeNSBmcAddress) + } + + data := map[string]string{} + if err := json.Unmarshal(bmcAttribute.Data, &data); err != nil { + return ip, errors.Wrap(ErrBMCAddress, err.Error()) + } + + if data["address"] == "" { + return ip, errors.Wrap(ErrBMCAddress, "value undefined: "+serverAttributeNSBmcAddress) + } + + return net.ParseIP(data["address"]), nil +} +func (s *Serverservice) vendorModelFromAttributes(attributes []sservice.Attributes) (deviceVendor, deviceModel, deviceSerial string, err error) { + vendorAttrs := map[string]string{} + + vendorAttribute := findAttribute(serverAttributeNSVendor, attributes) + if vendorAttribute == nil { + return deviceVendor, + deviceModel, + deviceSerial, + ErrVendorModelAttributes + } + + if err := json.Unmarshal(vendorAttribute.Data, &vendorAttrs); err != nil { + return deviceVendor, + deviceModel, + deviceSerial, + errors.Wrap(ErrVendorModelAttributes, "server vendor attribute: "+err.Error()) + } + + deviceVendor = common.FormatVendorName(vendorAttrs["vendor"]) + deviceModel = common.FormatProductName(vendorAttrs["model"]) + deviceSerial = vendorAttrs["serial"] + + if deviceVendor == "" { + return deviceVendor, + deviceModel, + deviceSerial, + errors.Wrap(ErrVendorModelAttributes, "device vendor unknown") + } + + if deviceModel == "" { + return deviceVendor, + deviceModel, + deviceSerial, + errors.Wrap(ErrVendorModelAttributes, "device model unknown") + } + + return +} + +func (s *Serverservice) updateFlasherAttributes(ctx context.Context, deviceUUID uuid.UUID, currentTaskAttrs, newTaskAttrs *FwInstallAttributes) error { + if newTaskAttrs.Requester == "" && currentTaskAttrs.Requester != "" { + newTaskAttrs.Requester = currentTaskAttrs.Requester + } + + if newTaskAttrs.FlasherTaskID == "" && currentTaskAttrs.FlasherTaskID != "" { + newTaskAttrs.FlasherTaskID = currentTaskAttrs.FlasherTaskID + } + + payload, err := json.Marshal(newTaskAttrs) + if err != nil { + return errors.Wrap(ErrAttributeUpdate, err.Error()) + } + + _, err = s.client.UpdateAttributes(ctx, deviceUUID, serverAttributeNSFlasherTask, payload) + if err != nil { + return errors.Wrap(ErrAttributeUpdate, err.Error()) + } + + return nil +} + +func (s *Serverservice) createFlasherAttributes(ctx context.Context, deviceUUID uuid.UUID, attrs *FwInstallAttributes) error { + payload, err := json.Marshal(attrs) + if err != nil { + return errors.Wrap(ErrAttributeCreate, err.Error()) + } + + data := sservice.Attributes{Namespace: serverAttributeNSFlasherTask, Data: payload} + + _, err = s.client.CreateAttributes(ctx, deviceUUID, data) + + return err +} + +// deviceWithFwInstallAttributes returns a device, firmware install parameters object with its fields populated with data from server service. +// +// The attributes looked up are, +// - BMC address +// - BMC credentials +// - Device vendor, model, serial attributes +// - Device state +// - Flasher install attributes +func (s *Serverservice) deviceWithFwInstallAttributes(ctx context.Context, deviceID string) (*model.Device, *FwInstallAttributes, error) { + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return nil, nil, err + } + + device := &model.Device{ID: deviceUUID} + + // lookup attributes + attributes, _, err := s.client.ListAttributes(ctx, deviceUUID, nil) + if err != nil { + return nil, nil, errors.Wrap(ErrServerserviceQuery, err.Error()) + } + + // bmc address from attribute + device.BmcAddress, err = s.bmcAddressFromAttributes(attributes) + if err != nil { + return nil, nil, err + } + + // credentials from credential store + credential, _, err := s.client.GetCredential(ctx, deviceUUID, sservice.ServerCredentialTypeBMC) + if err != nil { + return nil, nil, errors.Wrap(ErrServerserviceQuery, err.Error()) + } + + device.BmcUsername = credential.Username + device.BmcPassword = credential.Password + + // device state attribute + device.State, err = s.deviceStateAttribute(attributes) + if err != nil { + return nil, nil, err + } + + installParams, err := s.flasherTaskAttribute(attributes) + if err != nil { + return nil, nil, err + } + + // vendor attributes + device.Vendor, device.Model, device.Serial, err = s.vendorModelFromAttributes(attributes) + if err != nil { + if errors.Is(err, ErrVendorModelAttributes) { + s.logger.WithFields( + logrus.Fields{ + "component": component, + "deviceID": deviceID, + "err": err.Error(), + }, + ).Debug("device vendor/model is unknown") + } + + return device, installParams, err + } + + return device, installParams, nil +} + +func (s *Serverservice) fromServerserviceComponents(deviceVendor, deviceModel string, scomponents sservice.ServerComponentSlice) model.Components { + components := make(model.Components, 0, len(scomponents)) + + for _, sc := range scomponents { + if sc.Vendor == "" { + sc.Vendor = deviceVendor + } + + if sc.Model == "" { + sc.Model = deviceModel + } + + components = append(components, &model.Component{ + Slug: sc.ComponentTypeSlug, + Serial: sc.Serial, + Vendor: sc.Vendor, + Model: sc.Model, + FirmwareInstalled: s.firmwareFromVersionedAttributes(sc.VersionedAttributes), + }) + } + + return components +} + +func (s *Serverservice) firmwareFromVersionedAttributes(va []sservice.VersionedAttributes) string { + if len(va) == 0 { + return "" + } + + found := findVersionedAttribute(s.config.OutofbandFirmwareNS, va) + if found == nil { + return "" + } + + vaData := &versionedAttributeFirmware{} + if err := json.Unmarshal(found.Data, vaData); err != nil { + s.logger.Warn("failed to unmarshal firmware data") + return "" + } + + if vaData.Firmware == nil { + return "" + } + + return vaData.Firmware.Installed +} diff --git a/internal/inventory/yaml.go b/internal/inventory/yaml.go new file mode 100644 index 00000000..c3182996 --- /dev/null +++ b/internal/inventory/yaml.go @@ -0,0 +1,74 @@ +package inventory + +import ( + "context" + "errors" + + "github.com/metal-toolbox/flasher/internal/model" +) + +const ( + InventorySourceYAML = "inventorySourceYAML" +) + +var ( + ErrYamlSource = errors.New("error in Yaml inventory") +) + +// Yaml type implements the inventory interface +type Yaml struct { + YamlFile string +} + +// NewYamlInventory returns a Yaml type that implements the inventory interface. +func NewYamlInventory(yamlFile string) (Inventory, error) { + return &Yaml{YamlFile: yamlFile}, nil +} + +// DeviceByID returns device attributes by its identifier +func (c *Yaml) DeviceByID(ctx context.Context, id string) (*DeviceInventory, error) { + return nil, nil +} + +func (c *Yaml) DevicesForFwInstall(ctx context.Context, limit int) ([]DeviceInventory, error) { + return nil, nil +} + +// AcquireDevice looks up a device by its identifier and flags or locks it for an update. +// +// - The implementation is to check if the device is a eligible based its status or other non-firmware inventory attributes. +// - The locking mechnism is left to the implementation. +func (c *Yaml) AcquireDevice(ctx context.Context, deviceID, workerID string) (DeviceInventory, error) { + return DeviceInventory{}, nil +} + +// ReleaseDevice looks up a device by its identifier and releases any locks held on the device. +// The lock release mechnism is left to the implementation. +func (c *Yaml) ReleaseDevice(ctx context.Context, id string) error { + return nil +} + +// SetFlasherAttributes - sets the firmware install attributes to the given value on a device. +func (c *Yaml) SetFlasherAttributes(ctx context.Context, deviceID string, attrs *FwInstallAttributes) error { + return nil +} + +// FlasherAttributes - gets the firmware install attributes to the given value for a device. +func (c *Yaml) FlasherAttributes(ctx context.Context, deviceID string) (FwInstallAttributes, error) { + return FwInstallAttributes{}, nil +} + +// DeleteFlasherAttributes - removes the firmware install attributes from a device. +func (c *Yaml) DeleteFlasherAttributes(ctx context.Context, deviceID string) error { + return nil +} + +// FirmwareByDeviceVendorModel returns the firmware for the device vendor, model. +func (c *Yaml) FirmwareByDeviceVendorModel(ctx context.Context, deviceVendor, deviceModel string) ([]model.Firmware, error) { + return nil, nil +} + +// FirmwareInstalled returns the component installed firmware versions +func (c *Yaml) FirmwareInstalled(ctx context.Context, deviceID string) (model.Components, error) { + return nil, nil +}