From 430ebdf50516f99e2f9224797d37868dcd2aaea9 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:09:27 +0200 Subject: [PATCH 01/25] define various consts, errors, providers, device types --- devices/constants.go | 86 +++++++++++++++ devices/device.go | 233 +++++++++++++++++++++++++++++++++++++++++ errors/errors.go | 42 ++++++++ providers/providers.go | 16 +-- 4 files changed, 369 insertions(+), 8 deletions(-) create mode 100644 devices/device.go diff --git a/devices/constants.go b/devices/constants.go index 1e754928..4bb7bbbe 100644 --- a/devices/constants.go +++ b/devices/constants.go @@ -1,5 +1,7 @@ package devices +import "strings" + const ( // Unknown is the constant that defines unknown things Unknown = "Unknown" @@ -18,6 +20,8 @@ const ( Quanta = "Quanta" // Common is the constant of thinks we could use across multiple vendors Common = "Common" + // Quanta is the contant to identify Intel hardware + Intel = "Intel" // Power Constants @@ -38,9 +42,91 @@ const ( DiscreteHwType = "discrete" // ChassisHwType is the constant defining the chassis hw type ChassisHwType = "chassis" + + // Redfish firmware apply at constants + // FirmwareApplyImmediate sets the firmware to be installed immediately after upload + FirmwareApplyImmediate = "Immediate" + //FirmwareApplyOnReset sets the firmware to be install on device power cycle/reset + FirmwareApplyOnReset = "OnReset" + + // Firmware install states returned by bmclib provider FirmwareInstallStatus implementations + // + // The redfish from the redfish spec are exposed as a smaller set of bmclib states for callers + // https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2020.3.pdf + + // FirmwareInstallInitializing indicates the device is performing init actions to install the update + // this covers the redfish states - 'starting', 'downloading' + // no action is required from the callers part in this state + FirmwareInstallInitializing = "initializing" + + // FirmwareInstallQueued indicates the device has queued the update, but has not started the update task yet + // this covers the redfish states - 'pending', 'new' + // no action is required from the callers part in this state + FirmwareInstallQueued = "queued" + + // FirmwareInstallRunner indicates the device is installing the update + // this covers the redfish states - 'running', 'stopping', 'cancelling' + // no action is required from the callers part in this state + FirmwareInstallRunning = "running" + + // FirmwareInstallComplete indicates the device completed the firmware install + // this covers the redfish state - 'complete' + FirmwareInstallComplete = "complete" + + // FirmwareInstallFailed indicates the firmware install failed + // this covers the redfish states - 'interrupted', 'killed', 'exception', 'cancelled', 'suspended' + FirmwareInstallFailed = "failed" + + // FirmwareInstallPowerCycleHost indicates the firmware install requires a host power cycle + // this covers the dell redfish state - 'scheduled' + FirmwareInstallPowerCyleHost = "powercycle-host" + + FirmwareInstallUnknown = "unknown" + + // device BIOS/UEFI POST code bmclib identifiers + POSTStateBootINIT = "boot-init/pxe" + POSTStateUEFI = "uefi" + POSTStateOS = "grub/os" + POSTCodeUnknown = "unknown" + + // Generic component slugs + // Slugs are set on Device types to identify the type of component + SlugBackplaneExpander = "Backplane Expander" + SlugChassis = "Chassis" + SlugTPM = "TPM" + SlugGPU = "GPU" + SlugCPU = "CPU" + SlugPhysicalMem = "PhysicalMemory" + SlugStorageController = "StorageController" + SlugStorageControllers = "StorageControllers" + SlugBMC = "BMC" + SlugBIOS = "BIOS" + SlugDrive = "Drive" + SlugDrives = "Drives" + SlugDriveTypePCIeNVMEeSSD = "NVMe PCIe SSD" + SlugDriveTypeSATASSD = "Sata SSD" + SlugDriveTypeSATAHDD = "Sata HDD" + SlugNIC = "NIC" + SlugNICs = "NICs" + SlugPSU = "Power Supply" + SlugPSUs = "Power Supplies" + SlugCPLD = "CPLD" + SlugEnclosure = "ENCLOSURE" + SlugUnknown = "unknown" ) // ListSupportedVendors returns a list of supported vendors func ListSupportedVendors() []string { return []string{HP, Dell, Supermicro} } + +// VendorFromProductName attempts to identify the vendor from the given productname +func VendorFromProductName(productName string) string { + n := strings.ToLower(productName) + switch { + case strings.Contains(n, "intel"): + return Intel + default: + return productName + } +} diff --git a/devices/device.go b/devices/device.go new file mode 100644 index 00000000..aeefba26 --- /dev/null +++ b/devices/device.go @@ -0,0 +1,233 @@ +package devices + +// Device type is composed of various components +type Device struct { + Oem bool `json:"oem"` + HardwareType string `json:"hardware_type,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + Chassis string `json:"chassis,omitempty"` + BIOS *BIOS `json:"bios,omitempty"` + BMC *BMC `json:"bmc,omitempty"` + Mainboard *Mainboard `json:"mainboard,omitempty"` + CPLDs []*CPLD `json:"cplds"` + TPMs []*TPM `json:"tpms,omitempty"` + GPUs []*GPU `json:"gpus,omitempty"` + CPUs []*CPU `json:"cpus,omitempty"` + Memory []*Memory `json:"memory,omitempty"` + NICs []*NIC `json:"nics,omitempty"` + Drives []*Drive `json:"drives,omitempty"` + StorageControllers []*StorageController `json:"storage_controller,omitempty"` + PSUs []*PSU `json:"power_supplies,omitempty"` + Enclosures []*Enclosure `json:"enclosures,omitempty"` + Status *Status `json:"status,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// NewDevice returns a pointer to an initialized Device type +func NewDevice() *Device { + return &Device{ + BMC: &BMC{NIC: &NIC{}}, + BIOS: &BIOS{}, + Mainboard: &Mainboard{}, + TPMs: []*TPM{}, + CPLDs: []*CPLD{}, + PSUs: []*PSU{}, + NICs: []*NIC{}, + GPUs: []*GPU{}, + CPUs: []*CPU{}, + Memory: []*Memory{}, + Drives: []*Drive{}, + StorageControllers: []*StorageController{}, + Enclosures: []*Enclosure{}, + Status: &Status{}, + } +} + +// Firmware struct holds firmware attributes of a device component +type Firmware struct { + Installed string `json:"installed,omitempty"` + SoftwareID string `json:"software_id,omitempty"` + Previous []*Firmware `json:"previous,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// NewFirmwareObj returns a *Firmware object +func NewFirmwareObj() *Firmware { + return &Firmware{Metadata: make(map[string]string)} +} + +// Status is the health status of a component +type Status struct { + Health string + State string + PostCode int `json:"post_code,omitempty"` + PostCodeStatus string `json:"post_code_status,omitempty"` +} + +// GPU component +type GPU struct { +} + +// Enclosure component +type Enclosure struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + ChassisType string `json:"chassis_type,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` + Status *Status `json:"status,omitempty"` +} + +// TPM component +type TPM struct { + InterfaceType string `json:"interface_type,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` + Status *Status `json:"status,omitempty"` +} + +// CPLD component +type CPLD struct { + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// PSU component +type PSU struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + PowerCapacityWatts int64 `json:"power_capacity_watts,omitempty"` + Oem bool `json:"oem"` + Status *Status `json:"status,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// BIOS component +type BIOS struct { + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + SizeBytes int64 `json:"size_bytes,omitempty"` + CapacityBytes int64 `json:"capacity_bytes,omitempty" diff:"immutable"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// BMC component +type BMC struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + NIC *NIC `json:"nic,omitempty"` + Status *Status `json:"status,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// CPU component +type CPU struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + Slot string `json:"slot,omitempty"` + Architecture string `json:"architecture,omitempty"` + ClockSpeedHz int64 `json:"clock_speeed_hz,omitempty"` + Cores int `json:"cores,omitempty"` + Threads int `json:"threads,omitempty"` + Status *Status `json:"status,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// Memory component +type Memory struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Slot string `json:"slot,omitempty"` + Type string `json:"type,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + SizeBytes int64 `json:"size_bytes,omitempty"` + FormFactor string `json:"form_factor,omitempty"` + PartNumber string `json:"part_number,omitempty"` + ClockSpeedHz int64 `json:"clock_speed_hz,omitempty"` + Status *Status `json:"status,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// NIC component +type NIC struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty" diff:"identifier"` + SpeedBits int64 `json:"speed_bits,omitempty"` + PhysicalID string `json:"physid,omitempty"` + MacAddress string `json:"macaddress,omitempty"` + Oem bool `json:"oem"` + Metadata map[string]string `json:"metadata"` + Status *Status `json:"status,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// StorageController component +type StorageController struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + SupportedControllerProtocols string `json:"supported_controller_protocol,omitempty"` // PCIe + SupportedDeviceProtocols string `json:"supported_device_protocol,omitempty"` // Attached device protocols - SAS, SATA + SupportedRAIDTypes string `json:"supported_raid_types,omitempty"` + PhysicalID string `json:"physid,omitempty"` + SpeedGbps int64 `json:"speed_gbps,omitempty"` + Oem bool `json:"oem"` + Status *Status `json:"status,omitempty"` + Metadata map[string]string `json:"metadata"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// Mainboard component +type Mainboard struct { + ProductName string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + PhysicalID string `json:"physid,omitempty"` + Firmware *Firmware `json:"firmware,omitempty"` +} + +// Drive component +type Drive struct { + ID string `json:"id,omitempty"` + ProductName string `json:"name,omitempty"` + Type string `json:"drive_type,omitempty"` + Description string `json:"description,omitempty"` + Serial string `json:"serial,omitempty" diff:"identifier"` + StorageController string `json:"storage_controller,omitempty"` + Vendor string `json:"vendor,omitempty"` + Model string `json:"model,omitempty"` + WWN string `json:"wwn,omitempty"` + Protocol string `json:"protocol,omitempty"` + CapacityBytes int64 `json:"capacity_bytes,omitempty"` + BlockSizeBytes int64 `json:"block_size_bytes,omitempty"` + CapableSpeedGbps int64 `json:"capable_speed_gbps,omitempty"` + NegotiatedSpeedGbps int64 `json:"negotiated_speed_gbps,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` // Additional metadata if any + Oem bool `json:"oem,omitempty"` // Component is an OEM component + Firmware *Firmware `json:"firmware,omitempty"` + Status *Status `json:"status,omitempty"` +} diff --git a/errors/errors.go b/errors/errors.go index 59bccd54..fe4af1f1 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -36,6 +36,9 @@ var ( // Err500 is returned when we receive a 500 response from an endpoint. Err500 = errors.New("we've received 500 calling this endpoint") + // ErrNon200Response is returned when bmclib recieves an unexpected non-200 status code for a query + ErrNon200Response = errors.New("non-200 response returned for the endpoint") + // ErrNotImplemented is returned for not implemented methods called ErrNotImplemented = errors.New("this feature hasn't been implemented yet") @@ -71,6 +74,45 @@ var ( // ErrUserAccountUpdate is returned when the user account failed to be updated ErrUserAccountUpdate = errors.New("user account attributes could not be updated") + + // ErrRedfishChassisOdataID is returned when no compatible Chassis Odata IDs were identified + ErrRedfishChassisOdataID = errors.New("no compatible Chassis Odata IDs identified") + + // ErrRedfishSystemOdataID is returned when no compatible System Odata IDs were identified + ErrRedfishSystemOdataID = errors.New("no compatible System Odata IDs identified") + + // ErrRedfishManagerOdataID is returned when no compatible Manager Odata IDs were identified + ErrRedfishManagerOdataID = errors.New("no compatible Manager Odata IDs identified") + + // ErrRedfishServiceNil is returned when a redfish method is invoked on a nil redfish (gofish) Service object + ErrRedfishServiceNil = errors.New("redfish connection returned a nil redfish Service object") + + // ErrRedfishSoftwareInventory is returned when software inventory could not be collected over redfish + ErrRedfishSoftwareInventory = errors.New("error collecting redfish software inventory") + + // ErrFirmwareUpload is returned when a firmware upload method fails + ErrFirmwareUpload = errors.New("error uploading firmware") + + // ErrFirmwareInstall is returned for firmware update failures + ErrFirmwareInstall = errors.New("error updating firmware") + + // ErrRedfishUpdateService is returned on redfish update service errors + ErrRedfishUpdateService = errors.New("redfish update service error") + + // ErrTaskNotFound is returned when the (redfish) task could not be found + ErrTaskNotFound = errors.New("task not found") + + // ErrTaskPurge is returned when a (redfish) task could not be purged + ErrTaskPurge = errors.New("unable to purge task") + + // ErrPowerStatusRead is returned when a power status read query fails + ErrPowerStatusRead = errors.New("error returning power status") + + // ErrPowerStatusSet is returned when a power status set query fails + ErrPowerStatusSet = errors.New("error setting power status") + + // ErrProviderImplementation is returned when theres an error in the BMC provider implementation + ErrProviderImplementation = errors.New("error in provider implementation") ) type ErrUnsupportedHardware struct { diff --git a/providers/providers.go b/providers/providers.go index 887fa01a..ae3363b0 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -21,12 +21,12 @@ const ( FeatureBmcReset registrar.Feature = "bmcreset" // FeatureBootDeviceSet means an implementation the next boot device FeatureBootDeviceSet registrar.Feature = "bootdeviceset" - // FeatureBmcVersionRead means an implementation that returns the BMC firmware version - FeatureBmcVersionRead registrar.Feature = "bmcversionread" - // FeatureBiosVersionRead means an implementation that returns the BIOS firmware version - FeatureBiosVersionRead registrar.Feature = "biosversionread" - // FeatureBmcFirmwareUpdate means an implementation that updates the BMC firmware - FeatureBmcFirmwareUpdate registrar.Feature = "bmcfirwareupdate" - // FeatureBiosFirmwareUpdate means an implementation that updates the BIOS firmware - FeatureBiosFirmwareUpdate registrar.Feature = "biosfirwareupdate" + // FeatureFirmwareInstall means an implementation that initiates the firmware install process + FeatureFirmwareInstall registrar.Feature = "firmwareinstall" + // FeatureFirmwareInstallSatus means an implementation that returns the firmware install status + FeatureFirmwareInstallStatus registrar.Feature = "firmwareinstallstatus" + // FeatureInventoryRead means an implementation that returns the hardware and firmware inventory + FeatureInventoryRead registrar.Feature = "inventoryread" + // FeaturePostCodeRead means an implmentation that returns the boot BIOS/UEFI post code status and value + FeaturePostCodeRead registrar.Feature = "postcoderead" ) From 683e135d886de22357988fbc2da97fca3d058053 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:10:52 +0200 Subject: [PATCH 02/25] bmc/inventory: define inventory getter interface and method --- bmc/inventory.go | 78 ++++++++++++++++++++++++++++++++ bmc/inventory_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 bmc/inventory.go create mode 100644 bmc/inventory_test.go diff --git a/bmc/inventory.go b/bmc/inventory.go new file mode 100644 index 00000000..d6f0d55f --- /dev/null +++ b/bmc/inventory.go @@ -0,0 +1,78 @@ +package bmc + +import ( + "context" + "fmt" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" +) + +// InventoryGetter defines methods to retrieve device hardware and firmware inventory +type InventoryGetter interface { + GetInventory(ctx context.Context) (device *devices.Device, err error) +} + +type inventoryGetterProvider struct { + name string + InventoryGetter +} + +// GetInventory returns hardware and firmware inventory +func GetInventory(ctx context.Context, generic []inventoryGetterProvider) (device *devices.Device, metadata Metadata, err error) { + var metadataLocal Metadata +Loop: + for _, elem := range generic { + if elem.InventoryGetter == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + device, vErr := elem.GetInventory(ctx) + if vErr != nil { + err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) + err = multierror.Append(err, vErr) + continue + + } + metadataLocal.SuccessfulProvider = elem.name + return device, metadataLocal, nil + } + } + + return device, metadataLocal, multierror.Append(err, errors.New("failure to get device inventory")) +} + +// GetInventoryFromInterfaces is a pass through to library function +func GetInventoryFromInterfaces(ctx context.Context, generic []interface{}) (device *devices.Device, metadata Metadata, err error) { + implementations := make([]inventoryGetterProvider, 0) + for _, elem := range generic { + temp := inventoryGetterProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case InventoryGetter: + temp.InventoryGetter = p + implementations = append(implementations, temp) + default: + e := fmt.Sprintf("not a InventoryGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(implementations) == 0 { + return device, metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + ("no InventoryGetter implementations found"), + ), + ) + } + + return GetInventory(ctx, implementations) +} diff --git a/bmc/inventory_test.go b/bmc/inventory_test.go new file mode 100644 index 00000000..c8a43403 --- /dev/null +++ b/bmc/inventory_test.go @@ -0,0 +1,103 @@ +package bmc + +import ( + "context" + "testing" + "time" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/bmc-toolbox/bmclib/errors" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/stretchr/testify/assert" +) + +type inventoryGetterTester struct { + returnDevice *devices.Device + returnError error +} + +func (f *inventoryGetterTester) GetInventory(ctx context.Context) (device *devices.Device, err error) { + return f.returnDevice, f.returnError +} + +func (f *inventoryGetterTester) Name() string { + return "foo" +} + +func TestGetInventory(t *testing.T) { + testCases := []struct { + testName string + returnDevice *devices.Device + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + }{ + {"success with metadata", &devices.Device{Vendor: "foo"}, nil, 5 * time.Second, "foo", 1}, + {"failure with metadata", nil, errors.ErrNon200Response, 5 * time.Second, "foo", 1}, + {"failure with context timeout", nil, context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + testImplementation := inventoryGetterTester{returnDevice: tc.returnDevice, returnError: tc.returnError} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + device, metadata, err := GetInventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tc.returnDevice, device) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) + }) + } +} + +func TestGetInventoryFromInterfaces(t *testing.T) { + testCases := []struct { + testName string + returnDevice *devices.Device + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + badImplementation bool + }{ + {"success with metadata", &devices.Device{Vendor: "foo"}, nil, 5 * time.Second, "foo", 1, false}, + {"failure with bad implementation", nil, bmclibErrs.ErrProviderImplementation, 5 * time.Second, "foo", 1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := &inventoryGetterTester{returnDevice: tc.returnDevice, returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + device, metadata, err := GetInventoryFromInterfaces(context.Background(), generic) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, tc.returnDevice, device) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + }) + } +} From 7e7ce130120e68bbd3894662ff420728a3e80cc6 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:15:35 +0200 Subject: [PATCH 03/25] providers/asrockrack: implement InventoryGetter interface - include example --- examples/v1/inventory/inventory.go | 54 ++ examples/v1/inventory/output.json | 486 ++++++++++++++ .../fixtures/E3C246D4I-NL/sensors.json | 596 ++++++++++++++++++ providers/asrockrack/helpers_test.go | 114 ++++ providers/asrockrack/inventory.go | 181 ++++++ providers/asrockrack/inventory_test.go | 27 + 6 files changed, 1458 insertions(+) create mode 100644 examples/v1/inventory/inventory.go create mode 100644 examples/v1/inventory/output.json create mode 100644 providers/asrockrack/fixtures/E3C246D4I-NL/sensors.json create mode 100644 providers/asrockrack/helpers_test.go create mode 100644 providers/asrockrack/inventory.go create mode 100644 providers/asrockrack/inventory_test.go diff --git a/examples/v1/inventory/inventory.go b/examples/v1/inventory/inventory.go new file mode 100644 index 00000000..137b414d --- /dev/null +++ b/examples/v1/inventory/inventory.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/bmc-toolbox/bmclib" + "github.com/bombsimon/logrusr/v2" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + // set BMC parameters here + host := "" + port := "" + user := "" + pass := "" + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.New(l) + + if host == "" || user == "" || pass == "" { + log.Fatal("required host/user/pass parameters not defined") + } + + cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + + err := cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + defer cl.Close(ctx) + + inv, err := cl.GetInventory(ctx) + if err != nil { + l.Error(err) + } + + b, err := json.MarshalIndent(inv, " ", " ") + if err != nil { + l.Error(err) + } + + fmt.Println(string(b)) + +} diff --git a/examples/v1/inventory/output.json b/examples/v1/inventory/output.json new file mode 100644 index 00000000..4061fe9a --- /dev/null +++ b/examples/v1/inventory/output.json @@ -0,0 +1,486 @@ +{ + "oem": false, + "vendor": "Dell Inc.", + "model": "PowerEdge R6515", + "serial": "FOOBAR", + "bios": { + "description": "BIOS Configuration Current Settings", + "firmware": { + "installed": "2.2.4", + "software_id": "159", + "previous": [ + { + "installed": "1.3.1", + "software_id": "159" + } + ], + "metadata": { + "name": "BIOS" + } + } + }, + "bmc": { + "id": "iDRAC.Embedded.1", + "description": "BMC", + "vendor": "Dell Inc.", + "model": "PowerEdge R6515", + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "5.00.00.00", + "software_id": "25227", + "previous": [ + { + "installed": "4.10.10.10", + "software_id": "25227" + } + ], + "metadata": { + "name": "Integrated Dell Remote Access Controller" + } + } + }, + "mainboard": {}, + "cplds": [ + { + "description": "System CPLD", + "vendor": "Dell Inc.", + "model": "PowerEdge R6515", + "firmware": { + "installed": "1.0.3", + "software_id": "27763", + "metadata": { + "name": "System CPLD" + } + } + } + ], + "tpms": [ + { + "interface_type": "TPM2_0", + "firmware": { + "installed": "1.3.1.0", + "software_id": "109673", + "metadata": { + "name": "TPM" + } + }, + "status": { + "Health": "", + "State": "Enabled" + } + } + ], + "cpus": [ + { + "id": "CPU.Socket.1", + "description": "Represents the properties of a Processor attached to this System", + "vendor": "AMD", + "model": "AMD EPYC 7402P 24-Core Processor", + "slot": "CPU.Socket.1", + "architecture": "x86", + "clock_speeed_hz": 3900, + "cores": 24, + "threads": 48, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "0x830104D" + } + } + ], + "memory": [ + { + "description": "DIMM A7", + "slot": "DIMM.Socket.A7", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "537385EF", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A6", + "slot": "DIMM.Socket.A6", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "53738538", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A3", + "slot": "DIMM.Socket.A3", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "53738536", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A8", + "slot": "DIMM.Socket.A8", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "5373856E", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A2", + "slot": "DIMM.Socket.A2", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "5373858E", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A4", + "slot": "DIMM.Socket.A4", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "537385CA", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A5", + "slot": "DIMM.Socket.A5", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "53738537", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "description": "DIMM A1", + "slot": "DIMM.Socket.A1", + "type": "DRAM", + "vendor": "Hynix Semiconductor", + "serial": "53738568", + "size_bytes": 8192, + "part_number": "HMA81GR7CJR8N-XN", + "clock_speed_hz": 3200, + "status": { + "Health": "OK", + "State": "Enabled" + } + } + ], + "nics": [ + { + "id": "NIC.Embedded.1", + "description": "Embedded NIC 1 Port 1 Partition 1", + "speed_bits": 6, + "oem": false, + "metadata": null, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "1.5.12" + } + }, + { + "id": "NIC.Slot.3", + "description": "NIC in Slot 3 Port 2 Partition 1", + "vendor": "Intel Corporation", + "model": "Intel(R) 10GbE 2P X710 Adapter", + "serial": "MYFLMIT06404GD", + "speed_bits": 100006, + "macaddress": "F8:F2:1E:A6:89:A1", + "oem": false, + "metadata": null, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "20.0.17", + "software_id": "102302", + "previous": [ + { + "installed": "19.5.12", + "software_id": "102301" + }, + { + "installed": "19.5.12", + "software_id": "102302" + } + ], + "metadata": { + "name": "Intel(R) Ethernet Converged Network Adapter X710 - F8:F2:1E:A6:89:A1" + } + } + } + ], + "drives": [ + { + "id": "Disk.Bay.1:Enclosure.Internal.0-1:NonRAID.Integrated.1-1", + "name": "MZ7LH480HBHQ0D3", + "drive_type": "SSD", + "description": "Disk 1 in Backplane 1 of Integrated Storage Controller 1", + "serial": "S5YJNE0N503923", + "storage_controller": "NonRAID.Integrated.1-1", + "vendor": "SAMSUNG", + "model": "MZ7LH480HBHQ0D3", + "protocol": "SATA", + "capacity_bytes": 480103980544, + "block_size_bytes": 512, + "capable_speed_gbps": 6, + "negotiated_speed_gbps": 6, + "firmware": { + "installed": "HG58", + "software_id": "108622", + "metadata": { + "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "id": "Disk.Bay.0:Enclosure.Internal.0-1:NonRAID.Integrated.1-1", + "name": "MZ7LH480HBHQ0D3", + "drive_type": "SSD", + "description": "Disk 0 in Backplane 1 of Integrated Storage Controller 1", + "serial": "S5YJNE0N503924", + "storage_controller": "NonRAID.Integrated.1-1", + "vendor": "SAMSUNG", + "model": "MZ7LH480HBHQ0D3", + "protocol": "SATA", + "capacity_bytes": 480103980544, + "block_size_bytes": 512, + "capable_speed_gbps": 6, + "negotiated_speed_gbps": 6, + "firmware": { + "installed": "HG58", + "software_id": "108622", + "metadata": { + "name": "Disk 0 in Backplane 1 of Integrated Storage Controller 1" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "id": "Disk.Direct.0-0:AHCI.Slot.2-1", + "name": "SSDSCKKB240G8R", + "drive_type": "SSD", + "description": "Disk 0 on AHCI Controller in slot 2", + "serial": "PHYH011400V5240J", + "storage_controller": "AHCI.Slot.2-1", + "vendor": "INTEL", + "model": "SSDSCKKB240G8R", + "protocol": "SATA", + "capacity_bytes": 240057409536, + "block_size_bytes": 512, + "capable_speed_gbps": 6, + "negotiated_speed_gbps": 6, + "firmware": { + "installed": "DL6R", + "software_id": "108313", + "metadata": { + "name": "Disk 0 on AHCI Controller in slot 2" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "id": "Disk.Direct.1-1:AHCI.Slot.2-1", + "name": "SSDSCKKB240G8R", + "drive_type": "SSD", + "description": "Disk 1 on AHCI Controller in slot 2", + "serial": "PHYH011303RM240J", + "storage_controller": "AHCI.Slot.2-1", + "vendor": "INTEL", + "model": "SSDSCKKB240G8R", + "protocol": "SATA", + "capacity_bytes": 240057409536, + "block_size_bytes": 512, + "capable_speed_gbps": 6, + "negotiated_speed_gbps": 6, + "firmware": { + "installed": "DL6R", + "software_id": "108313", + "metadata": { + "name": "Disk 1 on AHCI Controller in slot 2" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + } + ], + "storage_controller": [ + { + "description": "Dell HBA330 Mini", + "vendor": "DELL", + "speed_gbps": 12, + "oem": false, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "metadata": null, + "firmware": { + "installed": "16.17.01.00", + "software_id": "104298", + "metadata": { + "name": "Dell HBA330 Mini" + } + } + }, + { + "description": "BOSS-S1", + "vendor": "DELL", + "speed_gbps": 6, + "oem": false, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "metadata": null, + "firmware": { + "installed": "2.5.13.3024", + "software_id": "106883", + "metadata": { + "name": "BOSS-S1" + } + } + } + ], + "power_supplies": [ + { + "description": "PS1 Status", + "vendor": "DELL", + "model": "PWR SPLY,550W,RDNT,DELTA", + "serial": "CNDED000330FBL", + "power_capacity_watts": 550, + "oem": false, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "00.0C.7D" + } + }, + { + "description": "PS2 Status", + "vendor": "DELL", + "model": "PWR SPLY,550W,RDNT,DELTA", + "serial": "CNDED000330FFI", + "power_capacity_watts": 550, + "oem": false, + "status": { + "Health": "OK", + "State": "Enabled" + }, + "firmware": { + "installed": "00.0C.7D" + } + } + ], + "enclosures": [ + { + "id": "System.Embedded.1", + "description": "It represents the properties for physical components for any system.It represent racks, rackmount servers, blades, standalone, modular systems,enclosures, and all other containers.The non-cpu/device centric parts of the schema are all accessed either directly or indirectly through this resource.", + "chassis_type": "RackMount", + "vendor": "Dell Inc.", + "model": "PowerEdge R6515", + "firmware": {}, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "id": "Enclosure.Internal.0-1:NonRAID.Integrated.1-1", + "description": "Backplane 1 on Connector 0 of Integrated Storage Controller 1", + "chassis_type": "StorageEnclosure", + "model": "BP14G+ 0:1", + "firmware": { + "installed": "HG58", + "software_id": "108622", + "metadata": { + "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + }, + { + "id": "Enclosure.Internal.0-1", + "description": "PCIe SSD Backplane 1", + "chassis_type": "StorageEnclosure", + "model": "PCIe SSD Backplane 1", + "firmware": { + "installed": "HG58", + "software_id": "108622", + "metadata": { + "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" + } + }, + "status": { + "Health": "OK", + "State": "Enabled" + } + } + ], + "status": { + "Health": "", + "State": "" + } +} \ No newline at end of file diff --git a/providers/asrockrack/fixtures/E3C246D4I-NL/sensors.json b/providers/asrockrack/fixtures/E3C246D4I-NL/sensors.json new file mode 100644 index 00000000..9a239593 --- /dev/null +++ b/providers/asrockrack/fixtures/E3C246D4I-NL/sensors.json @@ -0,0 +1,596 @@ +[ + { + "id": 1, + "sensor_number": 1, + "name": "3VSB", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 112.000000, + "type": "voltage", + "type_number": 2, + "reading": 3.360000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 2.820000, + "lower_critical_threshold": 2.970000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 3.630000, + "higher_non_recoverable_threshold": 3.780000, + "accessible": 0, + "unit": "V" + }, + { + "id": 2, + "sensor_number": 2, + "name": "5VSB", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 101.000000, + "type": "voltage", + "type_number": 2, + "reading": 5.050000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 4.250000, + "lower_critical_threshold": 4.500000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 5.500000, + "higher_non_recoverable_threshold": 5.750000, + "accessible": 0, + "unit": "V" + }, + { + "id": 3, + "sensor_number": 3, + "name": "VCORE", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 64.000000, + "type": "voltage", + "type_number": 2, + "reading": 0.640000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 12336, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 1.890000, + "higher_non_recoverable_threshold": 1.980000, + "accessible": 0, + "unit": "V" + }, + { + "id": 4, + "sensor_number": 4, + "name": "VCCSA", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 105.000000, + "type": "voltage", + "type_number": 2, + "reading": 1.050000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 0.890000, + "lower_critical_threshold": 0.950000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 1.160000, + "higher_non_recoverable_threshold": 1.210000, + "accessible": 0, + "unit": "V" + }, + { + "id": 5, + "sensor_number": 5, + "name": "VCCM", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 120.000000, + "type": "voltage", + "type_number": 2, + "reading": 1.200000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 1.020000, + "lower_critical_threshold": 1.080000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 1.320000, + "higher_non_recoverable_threshold": 1.380000, + "accessible": 0, + "unit": "V" + }, + { + "id": 6, + "sensor_number": 6, + "name": "1.05V_PCH", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 105.000000, + "type": "voltage", + "type_number": 2, + "reading": 1.050000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 0.890000, + "lower_critical_threshold": 0.950000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 1.160000, + "higher_non_recoverable_threshold": 1.210000, + "accessible": 0, + "unit": "V" + }, + { + "id": 7, + "sensor_number": 7, + "name": "VCCIO", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 95.000000, + "type": "voltage", + "type_number": 2, + "reading": 0.950000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 0.810000, + "lower_critical_threshold": 0.860000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 1.050000, + "higher_non_recoverable_threshold": 1.090000, + "accessible": 0, + "unit": "V" + }, + { + "id": 8, + "sensor_number": 9, + "name": "VPPM", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 125.000000, + "type": "voltage", + "type_number": 2, + "reading": 2.500000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 2.200000, + "lower_critical_threshold": 2.320000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 2.840000, + "higher_non_recoverable_threshold": 2.960000, + "accessible": 0, + "unit": "V" + }, + { + "id": 9, + "sensor_number": 12, + "name": "BAT", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 96.000000, + "type": "voltage", + "type_number": 2, + "reading": 2.880000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 2.550000, + "lower_critical_threshold": 2.700000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 3.300000, + "higher_non_recoverable_threshold": 3.450000, + "accessible": 0, + "unit": "V" + }, + { + "id": 10, + "sensor_number": 13, + "name": "3V", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 111.000000, + "type": "voltage", + "type_number": 2, + "reading": 3.330000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 2.820000, + "lower_critical_threshold": 2.970000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 3.630000, + "higher_non_recoverable_threshold": 3.780000, + "accessible": 0, + "unit": "V" + }, + { + "id": 11, + "sensor_number": 14, + "name": "5V", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 101.000000, + "type": "voltage", + "type_number": 2, + "reading": 5.050000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 4.250000, + "lower_critical_threshold": 4.500000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 5.500000, + "higher_non_recoverable_threshold": 5.750000, + "accessible": 0, + "unit": "V" + }, + { + "id": 12, + "sensor_number": 15, + "name": "12V", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 122.000000, + "type": "voltage", + "type_number": 2, + "reading": 12.200000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 13878, + "lower_non_recoverable_threshold": 10.200000, + "lower_critical_threshold": 10.800000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 13.200000, + "higher_non_recoverable_threshold": 13.800000, + "accessible": 0, + "unit": "V" + }, + { + "id": 13, + "sensor_number": 48, + "name": "MB Temp", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 30.000000, + "type": "temperature", + "type_number": 1, + "reading": 30.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 6168, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 54.000000, + "higher_critical_threshold": 55.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "°C" + }, + { + "id": 14, + "sensor_number": 50, + "name": "TR1 Temp", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 0.000000, + "type": "temperature", + "type_number": 1, + "reading": 0.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 2056, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 65.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 213, + "unit": "°C" + }, + { + "id": 15, + "sensor_number": 51, + "name": "CPU Temp", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 28.000000, + "type": "temperature", + "type_number": 1, + "reading": 28.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 6168, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 99.000000, + "higher_critical_threshold": 100.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "°C" + }, + { + "id": 16, + "sensor_number": 53, + "name": "PCH Temp", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 36.000000, + "type": "temperature", + "type_number": 1, + "reading": 36.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 6168, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 99.000000, + "higher_critical_threshold": 100.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "°C" + }, + { + "id": 17, + "sensor_number": 96, + "name": "IPB FAN1", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 18, + "sensor_number": 97, + "name": "IPB FAN2", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 19, + "sensor_number": 98, + "name": "IPB FAN3", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 20, + "sensor_number": 99, + "name": "IPB FAN4", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 21, + "sensor_number": 100, + "name": "IPB FAN5", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 22, + "sensor_number": 101, + "name": "IPB FAN6", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 23, + "sensor_number": 102, + "name": "IPB FAN7", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 24, + "sensor_number": 103, + "name": "IPB FAN8", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 26.000000, + "type": "fan", + "type_number": 4, + "reading": 5200.000000, + "sensor_state": 1, + "discrete_state": 0, + "settable_readable_threshMask": 257, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 200.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "RPM" + }, + { + "id": 25, + "sensor_number": 145, + "name": "CPU_PROCHOT", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 0.000000, + "type": "processor", + "type_number": 7, + "reading": 32768.000000, + "sensor_state": 0, + "discrete_state": 3, + "settable_readable_threshMask": 0, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "unknown" + }, + { + "id": 26, + "sensor_number": 147, + "name": "CPU_THERMTRIP", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 0.000000, + "type": "processor", + "type_number": 7, + "reading": 32768.000000, + "sensor_state": 0, + "discrete_state": 111, + "settable_readable_threshMask": 0, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "unknown" + }, + { + "id": 27, + "sensor_number": 153, + "name": "CPU_CATERR", + "owner_id": 32, + "owner_lun": 0, + "raw_reading": 0.000000, + "type": "processor", + "type_number": 7, + "reading": 32768.000000, + "sensor_state": 0, + "discrete_state": 3, + "settable_readable_threshMask": 0, + "lower_non_recoverable_threshold": 0.000000, + "lower_critical_threshold": 0.000000, + "lower_non_critical_threshold": 0.000000, + "higher_non_critical_threshold": 0.000000, + "higher_critical_threshold": 0.000000, + "higher_non_recoverable_threshold": 0.000000, + "accessible": 0, + "unit": "unknown" + } +] \ No newline at end of file diff --git a/providers/asrockrack/helpers_test.go b/providers/asrockrack/helpers_test.go new file mode 100644 index 00000000..a1b18fc7 --- /dev/null +++ b/providers/asrockrack/helpers_test.go @@ -0,0 +1,114 @@ +package asrockrack + +import ( + "context" + "testing" + + "gopkg.in/go-playground/assert.v1" +) + +func Test_FirmwareInfo(t *testing.T) { + expected := firmwareInfo{ + BMCVersion: "0.01.00", + BIOSVersion: "L2.07B", + MEVersion: "5.1.3.78", + MicrocodeVersion: "000000ca", + CPLDVersion: "N/A", + CMVersion: "0.13.01", + BPBVersion: "0.0.002.0", + NodeID: "2", + } + + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + fwInfo, err := aClient.firmwareInfo(context.TODO()) + if err != nil { + t.Error(err.Error()) + } + + assert.Equal(t, expected, fwInfo) +} + +func Test_inventoryInfo(t *testing.T) { + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + inventory, err := aClient.inventoryInfo(context.TODO()) + if err != nil { + t.Fatal(err.Error()) + } + + assert.Equal(t, 6, len(inventory)) + assert.Equal(t, "CPU", inventory[0].DeviceType) + assert.Equal(t, "Storage device", inventory[5].DeviceType) +} +func Test_fruInfo(t *testing.T) { + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + frus, err := aClient.fruInfo(context.TODO()) + if err != nil { + t.Fatal(err.Error()) + } + + assert.Equal(t, 3, len(frus)) +} + +func Test_sensors(t *testing.T) { + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + sensors, err := aClient.sensors(context.TODO()) + if err != nil { + t.Fatal(err.Error()) + } + + assert.Equal(t, 27, len(sensors)) +} + +func Test_biosPOSTCode(t *testing.T) { + expected := biosPOSTCode{ + PostStatus: 1, + PostData: 160, + } + + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + info, err := aClient.postCodeInfo(context.TODO()) + if err != nil { + t.Error(err.Error()) + } + + assert.Equal(t, expected, info) +} + +func Test_chassisStatus(t *testing.T) { + expected := chassisStatus{ + PowerStatus: 1, + LEDStatus: 0, + } + + err := aClient.httpsLogin(context.TODO()) + if err != nil { + t.Errorf(err.Error()) + } + + info, err := aClient.chassisStatusInfo(context.TODO()) + if err != nil { + t.Error(err.Error()) + } + + assert.Equal(t, expected, info) +} diff --git a/providers/asrockrack/inventory.go b/providers/asrockrack/inventory.go new file mode 100644 index 00000000..fd21fda7 --- /dev/null +++ b/providers/asrockrack/inventory.go @@ -0,0 +1,181 @@ +package asrockrack + +import ( + "context" + + "github.com/bmc-toolbox/bmclib/devices" +) + +func (a *ASRockRack) GetInventory(ctx context.Context) (device *devices.Device, err error) { + // initialize device to be populated with inventory + device = devices.NewDevice() + device.Metadata = map[string]string{} + + // populate device BMC, BIOS component attributes + err = a.fruAttributes(ctx, device) + if err != nil { + return nil, err + } + + // populate device System components attributes + err = a.systemAttributes(ctx, device) + if err != nil { + return nil, err + } + + // populate device health based on sensor readings + err = a.systemHealth(ctx, device) + if err != nil { + return nil, err + } + + return device, nil +} + +// systemHealth collects system health information based on the sensors data +func (a *ASRockRack) systemHealth(ctx context.Context, device *devices.Device) error { + sensors, err := a.sensors(ctx) + if err != nil { + return err + } + + ok := true + device.Status.Health = "OK" + for _, sensor := range sensors { + switch sensor.Name { + case "CPU_CATERR", "CPU_THERMTRIP", "CPU_PROCHOT": + if sensor.SensorState != 0 { + ok = false + device.Status.State = sensor.Name + break + } + default: + if sensor.SensorState != 1 { + ok = false + device.Status.State = sensor.Name + break + } + } + } + + if !ok { + device.Status.Health = "CRITICAL" + } + + // we don't want to fail inventory collection hence ignore POST code collection error + device.Status.PostCodeStatus, device.Status.PostCode, _ = a.GetPostCode(ctx) + + return nil +} + +// fruAttributes collects chassis information +func (a *ASRockRack) fruAttributes(ctx context.Context, device *devices.Device) error { + components, err := a.fruInfo(ctx) + if err != nil { + return err + } + + for _, component := range components { + switch component.Component { + case "board": + device.Vendor = component.Manufacturer + device.Model = component.ProductName + device.Serial = component.SerialNumber + case "chassis": + device.Enclosures = append(device.Enclosures, &devices.Enclosure{ + Serial: component.SerialNumber, + Description: component.Type, + }) + case "product": + device.Metadata["product.manufacturer"] = component.Manufacturer + device.Metadata["product.name"] = component.ProductName + device.Metadata["product.part_number"] = component.PartNumber + device.Metadata["product.version"] = component.ProductVersion + device.Metadata["product.serialnumber"] = component.SerialNumber + } + } + + return nil +} + +// systemAttributes collects system component attributes +func (a *ASRockRack) systemAttributes(ctx context.Context, device *devices.Device) error { + fwInfo, err := a.firmwareInfo(ctx) + if err != nil { + return err + } + + device.BIOS = &devices.BIOS{ + Vendor: device.Vendor, + Model: device.Model, + Firmware: &devices.Firmware{Installed: fwInfo.BIOSVersion}, + } + + device.BMC = &devices.BMC{ + Vendor: device.Vendor, + Model: device.Model, + Firmware: &devices.Firmware{Installed: fwInfo.BMCVersion}, + } + + if fwInfo.CPLDVersion != "N/A" { + device.CPLDs = append(device.CPLDs, &devices.CPLD{ + Vendor: device.Vendor, + Model: device.Model, + Firmware: &devices.Firmware{Installed: fwInfo.CPLDVersion}, + }) + } + + device.Metadata["node_id"] = fwInfo.NodeID + + components, err := a.inventoryInfo(ctx) + if err != nil { + return err + } + + for _, component := range components { + switch component.DeviceType { + case "CPU": + device.CPUs = append(device.CPUs, + &devices.CPU{ + Vendor: component.ProductManufacturerName, + Model: component.ProductName, + Firmware: &devices.Firmware{ + Installed: fwInfo.MicrocodeVersion, + Metadata: map[string]string{ + "Intel_ME_version": fwInfo.MEVersion, + }, + }, + }, + ) + case "Memory": + device.Memory = append(device.Memory, + &devices.Memory{ + Vendor: component.ProductManufacturerName, + Serial: component.ProductSerialNumber, + PartNumber: component.ProductPartNumber, + Type: component.DeviceName, + Description: component.ProductExtra, + }, + ) + + case "Storage device": + var vendor string + + if component.ProductManufacturerName == "N/A" && + component.ProductPartNumber != "N/A" { + vendor = devices.VendorFromProductName(component.ProductPartNumber) + } + + device.Drives = append(device.Drives, + &devices.Drive{ + Vendor: vendor, + Serial: component.ProductSerialNumber, + ProductName: component.ProductPartNumber, + }, + ) + } + + } + + return nil +} diff --git a/providers/asrockrack/inventory_test.go b/providers/asrockrack/inventory_test.go new file mode 100644 index 00000000..50de7913 --- /dev/null +++ b/providers/asrockrack/inventory_test.go @@ -0,0 +1,27 @@ +package asrockrack + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_GetInventory(t *testing.T) { + device, err := aClient.GetInventory(context.TODO()) + if err != nil { + t.Fatal(err) + } + + assert.NotNil(t, device) + assert.Equal(t, "ASRockRack", device.Vendor) + assert.Equal(t, "E3C246D4I-NL", device.Model) + + assert.Equal(t, "L2.07B", device.BIOS.Firmware.Installed) + assert.Equal(t, "0.01.00", device.BMC.Firmware.Installed) + assert.Equal(t, "000000ca", device.CPUs[0].Firmware.Installed) + assert.Equal(t, "Intel(R) Xeon(R) E-2278G CPU @ 3.40GHz", device.CPUs[0].Model) + assert.Equal(t, 2, len(device.Memory)) + assert.Equal(t, 2, len(device.Drives)) + assert.Equal(t, "OK", device.Status.Health) +} From 3de88c218048ed3199d135474ccdf18404086ce6 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:21:14 +0200 Subject: [PATCH 04/25] providers/redfish: implement InventoryGetter interface, update gofish dependency --- go.mod | 7 +- go.sum | 4 +- providers/redfish/inventory.go | 677 +++++++++++++++++++++++++++++++++ providers/redfish/redfish.go | 7 + 4 files changed, 691 insertions(+), 4 deletions(-) create mode 100644 providers/redfish/inventory.go diff --git a/go.mod b/go.mod index 824f9834..d4227f44 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,11 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect + github.com/spf13/viper v1.7.1 + github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d + github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 + golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 golang.org/x/text v0.3.5 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/go.sum b/go.sum index 069331ed..3e9ab8f5 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stmcginnis/gofish v0.12.0 h1:6UbNePjA++XkHtCKKLr7envKENxljJ1YyD8f4vS3Zeo= -github.com/stmcginnis/gofish v0.12.0/go.mod h1:BGtQsY16q48M2K6KDAs38QXtNoHrkXaY/WZ/mmyMgNc= +github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d h1:cqKAdqKgyLY3CbHNViBPALcUbbYSXOciB5lppURZ3lE= +github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/providers/redfish/inventory.go b/providers/redfish/inventory.go new file mode 100644 index 00000000..a1f1fa4a --- /dev/null +++ b/providers/redfish/inventory.go @@ -0,0 +1,677 @@ +package redfish + +import ( + "context" + "strings" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/pkg/errors" + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" + + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" +) + +var ( + + // Supported Chassis Odata IDs + chassisOdataIDs = []string{ + // Dells + "/redfish/v1/Chassis/Enclosure.Internal.0-1", + "/redfish/v1/Chassis/System.Embedded.1", + "/redfish/v1/Chassis/Enclosure.Internal.0-1:NonRAID.Integrated.1-1", + // Supermicro + "/redfish/v1/Chassis/1", + // MegaRAC + "/redfish/v1/Chassis/Self", + } + + // Supported System Odata IDs + systemsOdataIDs = []string{ + // Dells + "/redfish/v1/Systems/System.Embedded.1", + } + + // Supported Manager Odata IDs (BMCs) + managerOdataIDs = []string{ + "/redfish/v1/Managers/iDRAC.Embedded.1", + } +) + +// inventory struct wraps redfish connection +type inventory struct { + conn *gofish.APIClient + softwareInventory []*redfish.SoftwareInventory +} + +// DeviceVendorModel returns the device vendor and model attributes +func (c *Conn) DeviceVendorModel(ctx context.Context) (vendor, model string, err error) { + systems, err := c.conn.Service.Systems() + if err != nil { + return vendor, model, err + } + + for _, sys := range systems { + if !compatibleOdataID(sys.ODataID, systemsOdataIDs) { + continue + } + + return sys.Manufacturer, sys.Model, nil + } + + return vendor, model, bmclibErrs.ErrRedfishSystemOdataID +} + +func (c *Conn) GetInventory(ctx context.Context) (device *devices.Device, err error) { + // initialize inventory object + inv := &inventory{conn: c.conn} + // TODO: this can soft fail + inv.softwareInventory, err = inv.collectSoftwareInventory() + if err != nil { + return nil, errors.Wrap(bmclibErrs.ErrRedfishSoftwareInventory, err.Error()) + } + + // initialize device to be populated with inventory + device = devices.NewDevice() + + // populate device Chassis components attributes + err = inv.chassisAttributes(device) + if err != nil { + return nil, err + } + + // populate device System components attributes + err = inv.systemAttributes(device) + if err != nil { + return nil, err + } + + // populate device BMC component attributes + err = inv.bmcAttributes(device) + if err != nil { + return nil, err + } + + return device, nil +} + +// collectSoftwareInventory returns redfish software inventory +func (i *inventory) collectSoftwareInventory() ([]*redfish.SoftwareInventory, error) { + service := i.conn.Service + if service == nil { + return nil, bmclibErrs.ErrRedfishServiceNil + } + + updateService, err := service.UpdateService() + if err != nil { + return nil, err + } + + return updateService.FirmwareInventories() +} + +// bmcAttributes collects BMC component attributes +func (i *inventory) bmcAttributes(device *devices.Device) (err error) { + service := i.conn.Service + if service == nil { + return bmclibErrs.ErrRedfishServiceNil + } + + managers, err := service.Managers() + if err != nil { + return err + } + + var compatible int + for _, manager := range managers { + if !compatibleOdataID(manager.ODataID, managerOdataIDs) { + continue + } + + compatible++ + + if manager.ManagerType != "BMC" { + continue + } + + device.BMC = &devices.BMC{ + ID: manager.ID, + Description: manager.Description, + Vendor: device.Vendor, + Model: device.Model, + Status: &devices.Status{ + Health: string(manager.Status.Health), + State: string(manager.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: manager.FirmwareVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes("", device.BMC.ID, device.BMC.Firmware) + } + + if compatible == 0 { + return bmclibErrs.ErrRedfishManagerOdataID + } + + return nil +} + +// chassisAttributes populates the device chassis attributes +func (i *inventory) chassisAttributes(device *devices.Device) (err error) { + service := i.conn.Service + if service == nil { + return bmclibErrs.ErrRedfishServiceNil + } + + chassis, err := service.Chassis() + if err != nil { + return err + } + + compatible := 0 + for _, ch := range chassis { + if !compatibleOdataID(ch.ODataID, chassisOdataIDs) { + continue + } + + compatible++ + + err = i.collectEnclosure(ch, device) + if err != nil { + return err + } + + err = i.collectPSUs(ch, device) + if err != nil { + return err + } + + } + + err = i.collectCPLDs(device) + if err != nil { + return err + } + + if compatible == 0 { + return bmclibErrs.ErrRedfishChassisOdataID + } + + return nil + +} + +func (i *inventory) systemAttributes(device *devices.Device) (err error) { + service := i.conn.Service + if service == nil { + return bmclibErrs.ErrRedfishServiceNil + } + + systems, err := service.Systems() + if err != nil { + return err + } + + compatible := 0 + for _, sys := range systems { + if !compatibleOdataID(sys.ODataID, systemsOdataIDs) { + continue + } + + compatible++ + + if sys.Manufacturer != "" && sys.Model != "" && sys.SerialNumber != "" { + device.Vendor = sys.Manufacturer + device.Model = sys.Model + device.Serial = sys.SerialNumber + } + + // slice of collector methods + funcs := []func(sys *redfish.ComputerSystem, device *devices.Device) error{ + i.collectCPUs, + i.collectDIMMs, + i.collectDrives, + i.collectBIOS, + i.collectNICs, + i.collectTPMs, + i.collectStorageControllers, + } + + // execute collector methods + for _, f := range funcs { + err := f(sys, device) + if err != nil { + return err + } + } + } + + if compatible == 0 { + return bmclibErrs.ErrRedfishSystemOdataID + } + + return nil +} + +// collectEnclosure collects Enclosure information +func (i *inventory) collectEnclosure(ch *redfish.Chassis, device *devices.Device) (err error) { + + e := &devices.Enclosure{ + ID: ch.ID, + Description: ch.Description, + Vendor: ch.Manufacturer, + Model: ch.Model, + ChassisType: string(ch.ChassisType), + Status: &devices.Status{ + Health: string(ch.Status.Health), + State: string(ch.Status.State), + }, + Firmware: &devices.Firmware{}, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugEnclosure, e.ID, e.Firmware) + + device.Enclosures = append(device.Enclosures, e) + + return nil +} + +// collectPSUs collects Power Supply Unit component information +func (i *inventory) collectPSUs(ch *redfish.Chassis, device *devices.Device) (err error) { + power, err := ch.Power() + if err != nil { + return err + } + + if power == nil { + return nil + } + + for _, psu := range power.PowerSupplies { + p := &devices.PSU{ + ID: psu.ID, + Description: psu.Name, + Vendor: psu.Manufacturer, + Model: psu.Model, + Serial: psu.SerialNumber, + PowerCapacityWatts: int64(psu.PowerCapacityWatts), + Status: &devices.Status{ + Health: string(psu.Status.Health), + State: string(psu.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: psu.FirmwareVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugPSU, psu.ID, p.Firmware) + + device.PSUs = append(device.PSUs, p) + + } + return nil +} + +// collectTPMs collects Trusted Platform Module component information +func (i *inventory) collectTPMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + for _, module := range sys.TrustedModules { + + tpm := &devices.TPM{ + InterfaceType: string(module.InterfaceType), + Firmware: &devices.Firmware{ + Installed: module.FirmwareVersion, + }, + Status: &devices.Status{ + State: string(module.Status.State), + Health: string(module.Status.Health), + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugTPM, "TPM", tpm.Firmware) + + device.TPMs = append(device.TPMs, tpm) + } + + return nil +} + +// collectNICs collects network interface component information +func (i *inventory) collectNICs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + // collect network interface information + nics, err := sys.NetworkInterfaces() + if err != nil { + return err + } + + // collect network ethernet interface information, these attributes are not available in NetworkAdapter, NetworkInterfaces + ethernetInterfaces, err := sys.EthernetInterfaces() + if err != nil { + return err + } + + for _, nic := range nics { + + // collect network interface adaptor information + adapter, err := nic.NetworkAdapter() + if err != nil { + return err + } + + n := &devices.NIC{ + ID: nic.ID, // "Id": "NIC.Slot.3", + Vendor: adapter.Manufacturer, + Model: adapter.Model, + Serial: adapter.SerialNumber, + Status: &devices.Status{ + State: string(nic.Status.State), + Health: string(nic.Status.Health), + }, + } + + if len(adapter.Controllers) > 0 { + n.Firmware = &devices.Firmware{ + Installed: adapter.Controllers[0].FirmwarePackageVersion, + } + } + + // populate mac addresses from ethernet interfaces + for _, ethInterface := range ethernetInterfaces { + // the ethernet interface includes the port and position number NIC.Slot.3-1-1 + if !strings.HasPrefix(ethInterface.ID, adapter.ID) { + continue + } + + // The ethernet interface description is + n.Description = ethInterface.Description + n.MacAddress = ethInterface.MACAddress + n.SpeedBits = int64(ethInterface.SpeedMbps*10 ^ 6) + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugNIC, n.ID, n.Firmware) + + device.NICs = append(device.NICs, n) + } + + return nil +} + +func (i *inventory) collectBIOS(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + bios, err := sys.Bios() + if err != nil { + return err + } + + //bios.Attributes.String("SystemCpldVersion") + device.BIOS = &devices.BIOS{ + Description: bios.Description, + Firmware: &devices.Firmware{ + Installed: sys.BIOSVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugBIOS, "BIOS", device.BIOS.Firmware) + + return nil +} + +// collectDrives collects drive component information +func (i *inventory) collectDrives(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + storage, err := sys.Storage() + if err != nil { + return err + } + + for _, member := range storage { + if member.DrivesCount == 0 { + continue + } + + drives, err := member.Drives() + if err != nil { + return err + } + + for _, drive := range drives { + d := &devices.Drive{ + ID: drive.ID, + ProductName: drive.Model, + Type: string(drive.MediaType), + Description: drive.Description, + Serial: drive.SerialNumber, + StorageController: member.ID, + Vendor: drive.Manufacturer, + Model: drive.Model, + Protocol: string(drive.Protocol), + CapacityBytes: drive.CapacityBytes, + CapableSpeedGbps: int64(drive.CapableSpeedGbs), + NegotiatedSpeedGbps: int64(drive.NegotiatedSpeedGbs), + BlockSizeBytes: int64(drive.BlockSizeBytes), + Firmware: &devices.Firmware{ + Installed: drive.Revision, + }, + Status: &devices.Status{ + Health: string(drive.Status.Health), + State: string(drive.Status.State), + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes("Disk", drive.ID, d.Firmware) + + device.Drives = append(device.Drives, d) + + } + + } + + return nil +} + +// collectStorageControllers populates the device with Storage controller component attributes +func (i *inventory) collectStorageControllers(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + storage, err := sys.Storage() + if err != nil { + return err + } + + for _, member := range storage { + for _, controller := range member.StorageControllers { + + c := &devices.StorageController{ + ID: controller.ID, + Description: controller.Name, + Vendor: controller.Manufacturer, + Model: controller.PartNumber, + Serial: controller.SerialNumber, + SpeedGbps: int64(controller.SpeedGbps), + // SupportedControllerProtocols: join(controller.SupportedControllerProtocols), + // SupportedDeviceProtocols: join(controller.SupportedDeviceProtocols), + // SupportedRAIDTypes: join(controller.SupportedRAIDTypes), + Status: &devices.Status{ + Health: string(controller.Status.Health), + State: string(controller.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: controller.FirmwareVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(c.Description, c.ID, c.Firmware) + + device.StorageControllers = append(device.StorageControllers, c) + } + + } + + return nil +} + +// collectCPUs populates the device with CPU component attributes +func (i *inventory) collectCPUs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + procs, err := sys.Processors() + if err != nil { + return err + } + + for _, proc := range procs { + if proc.ProcessorType != "CPU" { + // TODO: handle this case + continue + } + + device.CPUs = append(device.CPUs, &devices.CPU{ + ID: proc.ID, + Description: proc.Description, + Vendor: proc.Manufacturer, + Model: proc.Model, + Architecture: string(proc.ProcessorArchitecture), + Serial: "", + Slot: proc.Socket, + ClockSpeedHz: int64(proc.MaxSpeedMHz), + Cores: proc.TotalCores, + Threads: proc.TotalThreads, + Status: &devices.Status{ + Health: string(proc.Status.Health), + State: string(proc.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: proc.ProcessorID.MicrocodeInfo, + }, + }) + } + + return nil +} + +// collectDIMMs populates the device with memory component attributes +func (i *inventory) collectDIMMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + dimms, err := sys.Memory() + if err != nil { + return err + } + + for _, dimm := range dimms { + device.Memory = append(device.Memory, &devices.Memory{ + Description: dimm.Description, + Slot: dimm.ID, + Type: string(dimm.MemoryType), + Vendor: dimm.Manufacturer, + Model: "", + Serial: dimm.SerialNumber, + SizeBytes: int64(dimm.VolatileSizeMiB), + FormFactor: "", + PartNumber: dimm.PartNumber, + ClockSpeedHz: int64(dimm.OperatingSpeedMhz), + Status: &devices.Status{ + Health: string(dimm.Status.Health), + State: string(dimm.Status.State), + }, + }) + } + + return nil +} + +// collecCPLDs populates the device with CPLD component attributes +func (i *inventory) collectCPLDs(device *devices.Device) (err error) { + + cpld := &devices.CPLD{ + Vendor: device.Vendor, + Model: device.Model, + Firmware: &devices.Firmware{Metadata: make(map[string]string)}, + } + + i.firmwareAttributes(devices.SlugCPLD, "", cpld.Firmware) + name, exists := cpld.Firmware.Metadata["name"] + if exists { + cpld.Description = name + } + + device.CPLDs = []*devices.CPLD{cpld} + + return nil +} + +// firmwareInventory looks up the redfish inventory for objects that +// match - 1. slug, 2. id +// and returns the intalled or previous firmware for objects that matched +// +// slug - the component slug constant +// id - the component ID +// previous - when true returns previously installed firmware, else returns the current +func (i *inventory) firmwareAttributes(slug, id string, firmwareObj *devices.Firmware) { + if len(i.softwareInventory) == 0 { + return + } + + if id == "" { + id = slug + } + + for _, inv := range i.softwareInventory { + // include previously installed firmware attributes + if strings.HasPrefix(inv.ID, "Previous") { + if strings.Contains(inv.ID, id) || strings.EqualFold(slug, inv.Name) { + + if firmwareObj == nil { + firmwareObj = &devices.Firmware{} + } + + if firmwareObj.Installed == inv.Version { + continue + } + + firmwareObj.Previous = append(firmwareObj.Previous, &devices.Firmware{ + Installed: inv.Version, + SoftwareID: inv.SoftwareID, + }) + } + } + + // update firmwareObj with installed firmware attributes + if strings.HasPrefix(inv.ID, "Installed") { + if strings.Contains(inv.ID, id) || strings.EqualFold(slug, inv.Name) { + + if firmwareObj == nil { + firmwareObj = &devices.Firmware{} + } + + if firmwareObj.Installed == "" || firmwareObj.Installed != inv.Version { + firmwareObj.Installed = inv.Version + } + + firmwareObj.Metadata = map[string]string{"name": inv.Name} + firmwareObj.SoftwareID = inv.SoftwareID + } + } + } +} + +func compatibleOdataID(OdataID string, knownOdataIDs []string) bool { + for _, url := range knownOdataIDs { + if url == OdataID { + return true + } + } + + return false +} + +func stringInSlice(s string, sl []string) bool { + for _, elem := range sl { + if elem == s { + return true + } + } + + return false +} diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 9c05edba..abbb8073 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -33,6 +33,9 @@ var ( providers.FeatureUserCreate, providers.FeatureUserUpdate, providers.FeatureUserDelete, + providers.FeatureInventoryRead, + providers.FeatureFirmwareInstall, + providers.FeatureFirmwareInstallStatus, } ) @@ -82,6 +85,10 @@ func New(host, port, user, pass string, log logr.Logger, opts ...Option) *Conn { // Open a connection to a BMC via redfish func (c *Conn) Open(ctx context.Context) (err error) { + if !strings.HasPrefix(c.Host, "https://") && !strings.HasPrefix(c.Host, "http://") { + c.Host = "https://" + c.Host + } + config := gofish.ClientConfig{ Endpoint: "https://" + c.Host, Username: c.User, From 440f699fb5b814e1ff62b5efb4de8de966a50532 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:24:53 +0200 Subject: [PATCH 05/25] bmc/firmware: replace previous interface with FirmwareInstaller, FirmwareInstallVerifier --- bmc/firmware.go | 222 +++++++++++--------------- bmc/firmware_test.go | 369 +++++++++++++------------------------------ 2 files changed, 206 insertions(+), 385 deletions(-) diff --git a/bmc/firmware.go b/bmc/firmware.go index 59034f72..d36ed6cb 100644 --- a/bmc/firmware.go +++ b/bmc/firmware.go @@ -2,38 +2,43 @@ package bmc import ( "context" - "errors" "fmt" "io" - "github.com/hashicorp/go-multierror" -) + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" -// BMCVersionGetter retrieves the current BMC firmware version information -type BMCVersionGetter interface { - GetBMCVersion(ctx context.Context) (version string, err error) -} + "github.com/pkg/errors" -// BMCFirmwareUpdater upgrades the BMC firmware -type BMCFirmwareUpdater interface { - FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) -} + "github.com/hashicorp/go-multierror" +) -// BIOSVersionGetter retrieves the current BIOS firmware version information -type BIOSVersionGetter interface { - GetBIOSVersion(ctx context.Context) (version string, err error) +// FirmwareInstaller defines an interface to install firmware updates +type FirmwareInstaller interface { + // FirmwareInstall uploads firmware update payload to the BMC returning the task ID + FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) } -// BIOSFirmwareUpdater upgrades the BIOS firmware -type BIOSFirmwareUpdater interface { - FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) +// firmwareInstallerProvider is an internal struct to correlate an implementation/provider and its name +type firmwareInstallerProvider struct { + name string + FirmwareInstaller } -// GetBMCVersion returns the BMC firmware version, trying all interface implementations passed in -func GetBMCVersion(ctx context.Context, p []BMCVersionGetter) (version string, err error) { +// FirmwareInstall uploads and initiates firmware update for the component +// +// parameters: +// component - the component slug for the component update being installed +// applyAt - one of "Immediate", "OnReset" +// forceInstall - purge the install task queued/scheduled firmware install BMC task (if any) +// reader - the io.reader to the firmware update file +// +// return values: +// taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process +func FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader, generic []firmwareInstallerProvider) (taskID string, metadata Metadata, err error) { + var metadataLocal Metadata Loop: - for _, elem := range p { - if elem == nil { + for _, elem := range generic { + if elem.FirmwareInstaller == nil { continue } select { @@ -41,85 +46,73 @@ Loop: err = multierror.Append(err, ctx.Err()) break Loop default: - version, vErr := elem.GetBMCVersion(ctx) + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + taskID, vErr := elem.FirmwareInstall(ctx, component, applyAt, forceInstall, reader) if vErr != nil { + err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue + } - return version, nil + metadataLocal.SuccessfulProvider = elem.name + return taskID, metadataLocal, nil } } - return version, multierror.Append(err, errors.New("failed to get BMC version")) + return taskID, metadataLocal, multierror.Append(err, errors.New("failure in FirmwareInstall")) } -// GetBMCVersionFromInterfaces pass through to library function -func GetBMCVersionFromInterfaces(ctx context.Context, generic []interface{}) (version string, err error) { - bmcVersionGetter := make([]BMCVersionGetter, 0) +// FirmwareInstallFromInterfaces pass through to library function +func FirmwareInstallFromInterfaces(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader, generic []interface{}) (taskID string, metadata Metadata, err error) { + implementations := make([]firmwareInstallerProvider, 0) for _, elem := range generic { + temp := firmwareInstallerProvider{name: getProviderName(elem)} switch p := elem.(type) { - case BMCVersionGetter: - bmcVersionGetter = append(bmcVersionGetter, p) + case FirmwareInstaller: + temp.FirmwareInstaller = p + implementations = append(implementations, temp) default: - e := fmt.Sprintf("not a BMCVersionGetter implementation: %T", p) + e := fmt.Sprintf("not a FirmwareInstaller implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } - if len(bmcVersionGetter) == 0 { - return version, multierror.Append(err, errors.New("no BMCVersionGetter implementations found")) + if len(implementations) == 0 { + return taskID, metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + ("no FirmwareInstaller implementations found"), + ), + ) } - return GetBMCVersion(ctx, bmcVersionGetter) + return FirmwareInstall(ctx, component, applyAt, forceInstall, reader, implementations) } -// UpdateBMCFirmware upgrades the BMC firmware, trying all interface implementations passed ini -func UpdateBMCFirmware(ctx context.Context, fileReader io.Reader, fileSize int64, p []BMCFirmwareUpdater) (err error) { -Loop: - for _, elem := range p { - if elem == nil { - continue - } - select { - case <-ctx.Done(): - err = multierror.Append(err, ctx.Err()) - break Loop - default: - uErr := elem.FirmwareUpdateBMC(ctx, fileReader, fileSize) - if uErr != nil { - err = multierror.Append(err, uErr) - continue - } - return nil - } - } - - return multierror.Append(err, errors.New("failed to update BMC firmware")) +// FirmwareInstallVerifier defines an interface to check firmware install status +type FirmwareInstallVerifier interface { + FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) } -// UpdateBMCFirmwareFromInterfaces pass through to library function -func UpdateBMCFirmwareFromInterfaces(ctx context.Context, fileReader io.Reader, fileSize int64, generic []interface{}) (err error) { - bmcFirmwareUpdater := make([]BMCFirmwareUpdater, 0) - for _, elem := range generic { - switch p := elem.(type) { - case BMCFirmwareUpdater: - bmcFirmwareUpdater = append(bmcFirmwareUpdater, p) - default: - e := fmt.Sprintf("not a BMCFirmwareUpdater implementation: %T", p) - err = multierror.Append(err, errors.New(e)) - } - } - if len(bmcFirmwareUpdater) == 0 { - return multierror.Append(err, errors.New("no BMCFirmwareUpdater implementations found")) - } - - return UpdateBMCFirmware(ctx, fileReader, fileSize, bmcFirmwareUpdater) +// firmwareInstallVerifierProvider is an internal struct to correlate an implementation/provider and its name +type firmwareInstallVerifierProvider struct { + name string + FirmwareInstallVerifier } -// GetBIOSVersion returns the BMC firmware version, trying all interface implementations passed in -func GetBIOSVersion(ctx context.Context, p []BIOSVersionGetter) (version string, err error) { +// FirmwareInstallStatus returns the status of the firmware install process +// +// parameters: +// component (optional) - the component slug for the component update being installed +// taskID (optional) - the task identifier +// +// return values: +// status - returns one of the FirmwareInstall statuses (see devices/constants.go) +func FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { + var metadataLocal Metadata Loop: - for _, elem := range p { - if elem == nil { + for _, elem := range generic { + if elem.FirmwareInstallVerifier == nil { continue } select { @@ -127,76 +120,45 @@ Loop: err = multierror.Append(err, ctx.Err()) break Loop default: - version, vErr := elem.GetBIOSVersion(ctx) + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + status, vErr := elem.FirmwareInstallStatus(ctx, component, installVersion, taskID) if vErr != nil { + err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue - } - return version, nil - } - } - - return version, multierror.Append(err, errors.New("failed to get BIOS version")) -} -// GetBIOSVersionFromInterfaces pass through to library function -func GetBIOSVersionFromInterfaces(ctx context.Context, generic []interface{}) (version string, err error) { - biosVersionGetter := make([]BIOSVersionGetter, 0) - for _, elem := range generic { - switch p := elem.(type) { - case BIOSVersionGetter: - biosVersionGetter = append(biosVersionGetter, p) - default: - e := fmt.Sprintf("not a BIOSVersionGetter implementation: %T", p) - err = multierror.Append(err, errors.New(e)) - } - } - if len(biosVersionGetter) == 0 { - return version, multierror.Append(err, errors.New("no BIOSVersionGetter implementations found")) - } - - return GetBIOSVersion(ctx, biosVersionGetter) -} - -// UpdateBIOSFirmware upgrades the BIOS firmware, trying all interface implementations passed ini -func UpdateBIOSFirmware(ctx context.Context, fileReader io.Reader, fileSize int64, p []BIOSFirmwareUpdater) (err error) { -Loop: - for _, elem := range p { - if elem == nil { - continue - } - select { - case <-ctx.Done(): - err = multierror.Append(err, ctx.Err()) - break Loop - default: - uErr := elem.FirmwareUpdateBIOS(ctx, fileReader, fileSize) - if uErr != nil { - err = multierror.Append(err, uErr) - continue } - return nil + metadataLocal.SuccessfulProvider = elem.name + return status, metadataLocal, nil } } - return multierror.Append(err, errors.New("failed to update BIOS firmware")) + return status, metadataLocal, multierror.Append(err, errors.New("failure in FirmwareInstallStatus")) } -// GetBMCVersionFromInterfaces pass through to library function -func UpdateBIOSFirmwareFromInterfaces(ctx context.Context, fileReader io.Reader, fileSize int64, generic []interface{}) (err error) { - biosFirmwareUpdater := make([]BIOSFirmwareUpdater, 0) +// FirmwareInstallStatusFromInterfaces pass through to library function +func FirmwareInstallStatusFromInterfaces(ctx context.Context, component, installVersion, taskID string, generic []interface{}) (status string, metadata Metadata, err error) { + implementations := make([]firmwareInstallVerifierProvider, 0) for _, elem := range generic { + temp := firmwareInstallVerifierProvider{name: getProviderName(elem)} switch p := elem.(type) { - case BIOSFirmwareUpdater: - biosFirmwareUpdater = append(biosFirmwareUpdater, p) + case FirmwareInstallVerifier: + temp.FirmwareInstallVerifier = p + implementations = append(implementations, temp) default: - e := fmt.Sprintf("not a BIOSFirmwareUpdater implementation: %T", p) + e := fmt.Sprintf("not a FirmwareInstallVerifier implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } - if len(biosFirmwareUpdater) == 0 { - return multierror.Append(err, errors.New("no BIOSFirmwareUpdater implementations found")) + if len(implementations) == 0 { + return taskID, metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + ("no FirmwareInstallVerifier implementations found"), + ), + ) } - return UpdateBIOSFirmware(ctx, fileReader, fileSize, biosFirmwareUpdater) + return FirmwareInstallStatus(ctx, component, installVersion, taskID, implementations) } diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go index 0c4b974b..9325271b 100644 --- a/bmc/firmware_test.go +++ b/bmc/firmware_test.go @@ -1,345 +1,204 @@ package bmc import ( - "bytes" "context" - "errors" "io" "testing" "time" - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/go-multierror" + "github.com/bmc-toolbox/bmclib/devices" + "github.com/bmc-toolbox/bmclib/errors" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/stretchr/testify/assert" ) -type firmwareTester struct { - MakeNotOK bool - MakeErrorOut bool +type firmwareInstallTester struct { + returnTaskID string + returnError error } -func (f *firmwareTester) GetBMCVersion(ctx context.Context) (version string, err error) { - if f.MakeErrorOut { - return "", errors.New("failed to get BMC version") - } - if f.MakeNotOK { - return "", nil - } - return "1.33.7", nil -} - -func (f *firmwareTester) FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) { - if f.MakeErrorOut { - return errors.New("failed update") - } - - return nil +func (f *firmwareInstallTester) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (taskID string, err error) { + return f.returnTaskID, f.returnError } -func (f *firmwareTester) FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) { - if f.MakeErrorOut { - return errors.New("failed update") - } - return nil -} - -func (f *firmwareTester) GetBIOSVersion(ctx context.Context) (version string, err error) { - if f.MakeErrorOut { - return "", errors.New("failed to get BIOS version") - } - if f.MakeNotOK { - return "", nil - } - return "1.44.7", nil +func (r *firmwareInstallTester) Name() string { + return "foo" } -func TestGetBMCVersion(t *testing.T) { +func TestFirmwareInstall(t *testing.T) { testCases := []struct { - name string - version string - makeFail bool - err error - ctxTimeout time.Duration + testName string + component string + applyAt string + forceInstall bool + reader io.Reader + returnTaskID string + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int }{ - {name: "success", version: "1.33.7", err: nil}, - {name: "failure", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed to get BMC version"), errors.New("failed to get BMC version")}}}, - {name: "fail context timeout", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to get BMC version")}}, ctxTimeout: time.Nanosecond * 1}, + {"success with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", nil, 5 * time.Second, "foo", 1}, + {"failure with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", errors.ErrNon200Response, 5 * time.Second, "foo", 1}, + {"failure with context timeout", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} - expectedResult := tc.version + t.Run(tc.testName, func(t *testing.T) { + testImplementation := firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - result, err := GetBMCVersion(ctx, []BMCVersionGetter{&testImplementation}) + taskID, metadata, err := FirmwareInstall(ctx, tc.component, tc.applyAt, tc.forceInstall, tc.reader, []firmwareInstallerProvider{{tc.providerName, &testImplementation}}) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } - } else { - diff := cmp.Diff(expectedResult, result) - if diff != "" { - t.Fatal(diff) - } + t.Fatal(err) } + assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } - -func TestGetBMCVersionFromInterfaces(t *testing.T) { +func TestFirmwareInstallFromInterfaces(t *testing.T) { testCases := []struct { - name string - version string - err error + testName string + component string + applyAt string + forceInstall bool + reader io.Reader + returnTaskID string + returnError error + providerName string badImplementation bool - want string }{ - {name: "success", version: "1.33.7", err: nil}, - {name: "no implementations found", version: "", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BMCVersionGetter implementation: *struct {}"), errors.New("no BMCVersionGetter implementations found")}}}, + {"success with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", nil, "foo", false}, + {"failure with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { + t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { - testImplementation := firmwareTester{} - generic = []interface{}{&testImplementation} + testImplementation := &firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} + generic = []interface{}{testImplementation} } - expectedResult := tc.version - result, err := GetBMCVersionFromInterfaces(context.Background(), generic) - if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } - } else { - diff := cmp.Diff(expectedResult, result) - if diff != "" { - t.Fatal(diff) - } + taskID, metadata, err := FirmwareInstallFromInterfaces(context.Background(), tc.component, tc.applyAt, tc.forceInstall, tc.reader, generic) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return } - }) - } -} - -func TestUpdateBMCFirmware(t *testing.T) { - testCases := []struct { - name string - makeFail bool - err error - ctxTimeout time.Duration - }{ - {name: "success", err: nil}, - {name: "failure", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed update"), errors.New("failed to update BMC firmware")}}}, - {name: "fail context timeout", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to update BMC firmware")}}, ctxTimeout: time.Nanosecond * 1}, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} - if tc.ctxTimeout == 0 { - tc.ctxTimeout = time.Second * 3 - } - ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) - defer cancel() - err := UpdateBMCFirmware(ctx, bytes.NewReader([]byte(`foo`)), 0, []BMCFirmwareUpdater{&testImplementation}) if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } + t.Fatal(err) } - }) - } -} - -func TestUpdateBMCFirmwareFromInterfaces(t *testing.T) { - testCases := []struct { - name string - err error - badImplementation bool - want string - }{ - {name: "success", err: nil}, - {name: "no implementations found", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BMCFirmwareUpdater implementation: *struct {}"), errors.New("no BMCFirmwareUpdater implementations found")}}}, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - var generic []interface{} - if tc.badImplementation { - badImplementation := struct{}{} - generic = []interface{}{&badImplementation} - } else { - testImplementation := firmwareTester{} - generic = []interface{}{&testImplementation} - } - err := UpdateBMCFirmwareFromInterfaces(context.Background(), bytes.NewReader([]byte(`foo`)), 0, generic) - if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } - } + assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } -func TestGetBIOSVersion(t *testing.T) { - testCases := []struct { - name string - version string - makeFail bool - err error - ctxTimeout time.Duration - }{ - {name: "success", version: "1.44.7", err: nil}, - {name: "failure", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed to get BIOS version"), errors.New("failed to get BIOS version")}}}, - {name: "fail context timeout", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to get BIOS version")}}, ctxTimeout: time.Nanosecond * 1}, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} - expectedResult := tc.version - if tc.ctxTimeout == 0 { - tc.ctxTimeout = time.Second * 3 - } - ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) - defer cancel() - result, err := GetBIOSVersion(ctx, []BIOSVersionGetter{&testImplementation}) - if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } - } else { - diff := cmp.Diff(expectedResult, result) - if diff != "" { - t.Fatal(diff) - } - } - }) - } +type firmwareInstallStatusTester struct { + returnStatus string + returnError error } -func TestGetBIOSVersionFromInterfaces(t *testing.T) { - testCases := []struct { - name string - version string - err error - badImplementation bool - want string - }{ - {name: "success", version: "1.44.7", err: nil}, - {name: "no implementations found", version: "", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BIOSVersionGetter implementation: *struct {}"), errors.New("no BIOSVersionGetter implementations found")}}}, - } +func (f *firmwareInstallStatusTester) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { + return f.returnStatus, f.returnError +} - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - var generic []interface{} - if tc.badImplementation { - badImplementation := struct{}{} - generic = []interface{}{&badImplementation} - } else { - testImplementation := firmwareTester{} - generic = []interface{}{&testImplementation} - } - expectedResult := tc.version - result, err := GetBIOSVersionFromInterfaces(context.Background(), generic) - if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } - } else { - diff := cmp.Diff(expectedResult, result) - if diff != "" { - t.Fatal(diff) - } - } - }) - } +func (r *firmwareInstallStatusTester) Name() string { + return "foo" } -func TestUpdateBIOSFirmware(t *testing.T) { +func TestFirmwareInstallStatus(t *testing.T) { testCases := []struct { - name string - makeFail bool - err error - ctxTimeout time.Duration + testName string + component string + installVersion string + taskID string + returnStatus string + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int }{ - {name: "success", err: nil}, - {name: "failure", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed update"), errors.New("failed to update BIOS firmware")}}}, - {name: "fail context timeout", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to update BIOS firmware")}}, ctxTimeout: time.Nanosecond * 1}, + {"success with metadata", devices.SlugBIOS, "1.1", "1234", devices.FirmwareInstallComplete, nil, 5 * time.Second, "foo", 1}, + {"failure with metadata", devices.SlugBIOS, "1.1", "1234", devices.FirmwareInstallFailed, errors.ErrNon200Response, 5 * time.Second, "foo", 1}, + {"failure with context timeout", devices.SlugBIOS, "1.1", "1234", "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} + t.Run(tc.testName, func(t *testing.T) { + testImplementation := firmwareInstallStatusTester{returnStatus: tc.returnStatus, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 4 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - if tc.ctxTimeout != time.Second*4 { - // sleep the timeout length to force timeout - time.Sleep(tc.ctxTimeout) + taskID, metadata, err := FirmwareInstallStatus(ctx, tc.component, tc.installVersion, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return } - err := UpdateBIOSFirmware(ctx, bytes.NewReader([]byte(`foo`)), 0, []BIOSFirmwareUpdater{&testImplementation}) + if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } + t.Fatal(err) } + assert.Equal(t, tc.returnStatus, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } - -func TestUpdateBIOSFirmwareFromInterfaces(t *testing.T) { +func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { testCases := []struct { - name string - err error + testName string + component string + applyAt string + forceInstall bool + reader io.Reader + returnTaskID string + returnError error + providerName string badImplementation bool - want string }{ - {name: "success", err: nil}, - {name: "no implementations found", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BIOSFirmwareUpdater implementation: *struct {}"), errors.New("no BIOSFirmwareUpdater implementations found")}}}, + {"success with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", nil, "foo", false}, + {"failure with bad implementation", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { + t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { - testImplementation := firmwareTester{} - generic = []interface{}{&testImplementation} + testImplementation := &firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + taskID, metadata, err := FirmwareInstallFromInterfaces(context.Background(), tc.component, tc.applyAt, tc.forceInstall, tc.reader, generic) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return } - err := UpdateBIOSFirmwareFromInterfaces(context.Background(), bytes.NewReader([]byte(`foo`)), 0, generic) + if err != nil { - diff := cmp.Diff(tc.err.Error(), err.Error()) - if diff != "" { - t.Fatal(diff) - } + t.Fatal(err) } + + assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } From 7ab7659fc633641344b131802f0b186d5da57649 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:30:21 +0200 Subject: [PATCH 06/25] providers/redfish: implement FirmwareInstallStatus, FirmwareInstallVerifier interface methods --- examples/v1/firmware/firmware.go | 62 ++ providers/redfish/firmware.go | 280 +++++++++ providers/redfish/firmware_test.go | 79 +++ .../fixtures/v1/dell/job_delete_ok.json | 24 + providers/redfish/fixtures/v1/dell/jobs.json | 577 ++++++++++++++++++ .../fixtures/v1/dell/system.embedded.1.json | 512 ++++++++++++++++ .../redfish/fixtures/v1/serviceroot.json | 80 +++ providers/redfish/fixtures/v1/systems.json | 13 + .../redfish/fixtures/v1/updateservice.json | 51 ++ providers/redfish/redfish_test.go | 85 +++ providers/redfish/tasks.go | 173 ++++++ providers/redfish/tasks_test.go | 40 ++ 12 files changed, 1976 insertions(+) create mode 100644 examples/v1/firmware/firmware.go create mode 100644 providers/redfish/firmware.go create mode 100644 providers/redfish/firmware_test.go create mode 100644 providers/redfish/fixtures/v1/dell/job_delete_ok.json create mode 100644 providers/redfish/fixtures/v1/dell/jobs.json create mode 100644 providers/redfish/fixtures/v1/dell/system.embedded.1.json create mode 100644 providers/redfish/fixtures/v1/serviceroot.json create mode 100644 providers/redfish/fixtures/v1/systems.json create mode 100644 providers/redfish/fixtures/v1/updateservice.json create mode 100644 providers/redfish/redfish_test.go create mode 100644 providers/redfish/tasks.go create mode 100644 providers/redfish/tasks_test.go diff --git a/examples/v1/firmware/firmware.go b/examples/v1/firmware/firmware.go new file mode 100644 index 00000000..70551673 --- /dev/null +++ b/examples/v1/firmware/firmware.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/bmc-toolbox/bmclib" + "github.com/bmc-toolbox/bmclib/devices" + "github.com/bombsimon/logrusr/v2" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + // set BMC parameters here + host := "" + port := "" + user := "root" + pass := "" + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.New(l) + + if host == "" || user == "" || pass == "" { + log.Fatal("required host/user/pass parameters not defined") + } + + cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + + err := cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + defer cl.Close(ctx) + + for _, update := range []string{"/tmp/iDRAC-with-Lifecycle-Controller_Firmware_F87RP_WN64_5.00.00.00_A00.EXE"} { + fh, err := os.Open(update) + if err != nil { + log.Fatal(err) + } + + taskID, err := cl.FirmwareInstall(ctx, devices.SlugBMC, devices.FirmwareApplyOnReset, true, fh) + if err != nil { + l.Error(err) + } + + state, err := cl.FirmwareInstallStatus(ctx, "", taskID, "5.00.00.00") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("state: %s\n", state) + } + +} diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go new file mode 100644 index 00000000..dced2b6a --- /dev/null +++ b/providers/redfish/firmware.go @@ -0,0 +1,280 @@ +package redfish + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/pkg/errors" + rf "github.com/stmcginnis/gofish/redfish" + + "github.com/bmc-toolbox/bmclib/devices" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" +) + +var ( + FirmwareApplyAt = []string{ + devices.FirmwareApplyImmediate, + devices.FirmwareApplyOnReset, + } + FirmwareDownloadProtocols = []string{"HTTP", "NFS", "CIFS", "TFTP", "HTTPS"} +) + +// FirmwareInstall uploads and initiates the firmware install process +func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { + // validate firmware update mechanism is supported + err = c.firmwareUpdateCompatible(ctx) + if err != nil { + return "", err + } + + // validate applyAt parameter + if !stringInSlice(applyAt, FirmwareApplyAt) { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "invalid applyAt parameter: %s"+applyAt) + } + + // list redfish firmware install task if theres one present + task, err := c.GetFirmwareInstallTaskQueued(ctx, component) + if err != nil { + return "", err + } + + if task != nil { + msg := fmt.Sprintf("task for %s firmware install present: %s", component, task.ID) + c.Log.V(2).Info("warn", msg) + + if forceInstall { + err = c.purgeQueuedFirmwareInstallTask(ctx, component) + if err != nil { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) + } + } else { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, msg) + } + } + + updateParameters := []byte( + fmt.Sprintf(`{"Targets": [], "@Redfish.OperationApplyTime": "%s", "Oem": {}}`, applyAt), + ) + + payload := map[string]io.Reader{ + "UpdateParameters": bytes.NewReader(updateParameters), + "UpdateFile": reader, + } + + resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) + if err != nil { + return jobID, errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) + } + + if resp.StatusCode != http.StatusAccepted { + return jobID, errors.Wrap( + bmclibErrs.ErrFirmwareUpload, + "non 202 status code returned: "+strconv.Itoa(resp.StatusCode), + ) + } + + // The response contains a location header pointing to the task URI + // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 + if strings.Contains(resp.Header.Get("Location"), "JID_") { + jobID = strings.Split(resp.Header.Get("Location"), "JID_")[1] + } + + return jobID, nil +} + +// FirmwareInstallStatus returns the status of the firmware install task queued +func (c *Conn) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (state string, err error) { + vendor, _, err := c.DeviceVendorModel(ctx) + if err != nil { + return state, errors.Wrap(err, "unable to determine device vendor, model attributes") + } + + var task *rf.Task + switch { + case strings.Contains(vendor, devices.Dell): + task, err = c.dellJobAsRedfishTask(taskID) + default: + err = errors.Wrap( + bmclibErrs.ErrNotImplemented, + "FirmwareInstallStatus() for vendor: "+vendor, + ) + } + + if err != nil { + return state, err + } + + if task == nil { + return state, errors.New("failed to lookup task status for task ID: " + taskID) + } + + state = strings.ToLower(string(task.TaskState)) + + // so much for standards... + switch state { + case "starting", "downloading": + return devices.FirmwareInstallInitializing, nil + case "running", "stopping", "cancelling", "scheduling": + return devices.FirmwareInstallRunning, nil + case "pending", "new": + return devices.FirmwareInstallQueued, nil + case "scheduled": + return devices.FirmwareInstallPowerCyleHost, nil + case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": + return devices.FirmwareInstallFailed, nil + case "completed": + return devices.FirmwareInstallComplete, nil + default: + return devices.FirmwareInstallUnknown + ": " + state, nil + } + +} + +// firmwareUpdateCompatible retuns an error if the firmware update process for the BMC is not supported +func (c *Conn) firmwareUpdateCompatible(ctx context.Context) (err error) { + updateService, err := c.conn.Service.UpdateService() + if err != nil { + return err + } + + // TODO: check for redfish version + + // update service disabled + if !updateService.ServiceEnabled { + return errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") + } + + // for now we expect multipart HTTP push update support + if updateService.MultipartHTTPPushURI == "" { + return errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "Multipart HTTP push updates not supported") + } + + return nil +} + +// runRequestWithMultipartPayload is a copy of https://github.com/stmcginnis/gofish/blob/main/client.go#L349 +// with a change to add the UpdateParameters multipart form field with a json content type header +// the resulting form ends up in this format +// +// Content-Length: 416 +// Content-Type: multipart/form-data; boundary=-------------------- +// ----1771f60800cb2801 + +// --------------------------1771f60800cb2801 +// Content-Disposition: form-data; name="UpdateParameters" +// Content-Type: application/json + +// {"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": +// {}} +// --------------------------1771f60800cb2801 +// Content-Disposition: form-data; name="UpdateFile"; filename="dum +// myfile" +// Content-Type: application/octet-stream + +// hey. +// --------------------------1771f60800cb2801-- +func (c *Conn) runRequestWithMultipartPayload(method, url string, payload map[string]io.Reader) (*http.Response, error) { + if url == "" { + return nil, fmt.Errorf("unable to execute request, no target provided") + } + + var payloadBuffer bytes.Buffer + var err error + payloadWriter := multipart.NewWriter(&payloadBuffer) + for key, reader := range payload { + var partWriter io.Writer + if file, ok := reader.(*os.File); ok { + // Add a file stream + if partWriter, err = payloadWriter.CreateFormFile(key, filepath.Base(file.Name())); err != nil { + return nil, err + } + } else { + // Add other fields + if partWriter, err = updateParametersFormField(key, payloadWriter); err != nil { + return nil, err + } + } + if _, err = io.Copy(partWriter, reader); err != nil { + return nil, err + } + } + payloadWriter.Close() + + return c.conn.RunRawRequestWithHeaders(method, url, bytes.NewReader(payloadBuffer.Bytes()), payloadWriter.FormDataContentType(), nil) +} + +// sets up the UpdateParameters MIMEHeader for the multipart form +// the Go multipart writer CreateFormField does not currently let us set Content-Type on a MIME Header +// https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/mime/multipart/writer.go;l=151 +func updateParametersFormField(fieldName string, writer *multipart.Writer) (io.Writer, error) { + if fieldName != "UpdateParameters" { + return nil, errors.New("") + } + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="UpdateParameters"`) + h.Set("Content-Type", "application/json") + + return writer.CreatePart(h) +} + +// GetFirmwareInstallTaskQueued returns the redfish task object for a queued update task +func (c *Conn) GetFirmwareInstallTaskQueued(ctx context.Context, component string) (*rf.Task, error) { + vendor, _, err := c.DeviceVendorModel(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to determine device vendor, model attributes") + } + + var task *rf.Task + + // check an update task for the component is currently scheduled + switch { + case strings.Contains(vendor, devices.Dell): + task, err = c.getDellFirmwareInstallTaskScheduled(component) + default: + err = errors.Wrap( + bmclibErrs.ErrNotImplemented, + "GetFirmwareInstallTask() for vendor: "+vendor, + ) + } + + if err != nil { + return nil, err + } + + return task, nil +} + +// purgeQueuedFirmwareInstallTask removes any existing queued firmware install task for the given component slug +func (c *Conn) purgeQueuedFirmwareInstallTask(ctx context.Context, component string) error { + vendor, _, err := c.DeviceVendorModel(ctx) + if err != nil { + return errors.Wrap(err, "unable to determine device vendor, model attributes") + } + + // check an update task for the component is currently scheduled + switch { + case strings.Contains(vendor, devices.Dell): + err = c.dellPurgeScheduledFirmwareInstallJob(component) + default: + err = errors.Wrap( + bmclibErrs.ErrNotImplemented, + "purgeFirmwareInstallTask() for vendor: "+vendor, + ) + } + + if err != nil { + return err + } + + return nil +} diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go new file mode 100644 index 00000000..2074913a --- /dev/null +++ b/providers/redfish/firmware_test.go @@ -0,0 +1,79 @@ +package redfish + +import ( + "context" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bmc-toolbox/bmclib/devices" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" +) + +// handler registered in mock_test.go +func multipartUpload(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusNotFound) + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatal(err) + } + + expected := []string{ + `Content-Disposition: form-data; name="UpdateParameters"`, + `Content-Type: application/json`, + `{"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}}`, + `Content-Disposition: form-data; name="UpdateFile"; filename="test.bin"`, + `Content-Type: application/octet-stream`, + `HELLOWORLD`, + } + + for _, want := range expected { + if !strings.Contains(string(body), want) { + log.Fatal("expected value not in multipartUpload payload: " + string(want)) + } + } + + w.Header().Add("Location", "/redfish/v1/TaskService/Tasks/JID_467696020275") + w.WriteHeader(http.StatusAccepted) +} + +func Test_FirmwareUpload(t *testing.T) { + // curl -Lv -s -k -u root:calvin \ + // -F 'UpdateParameters={"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}};type=application/json' \ + // -F'foo.bin=@/tmp/dummyfile;application/octet-stream' + // https://192.168.1.1/redfish/v1/UpdateService/MultipartUpload --trace-ascii /dev/stdout + err := ioutil.WriteFile("/tmp/test.bin", []byte(`HELLOWORLD`), 0600) + if err != nil { + t.Fatal(err) + } + + fh, err := os.Open("/tmp/test.bin") + if err != nil { + t.Fatalf("%s -> %s", err.Error(), "/tmp/test.bin") + } + + _, err = mockClient.FirmwareInstall(context.TODO(), "", "invalid", false, fh) + assert.ErrorIs(t, err, bmclibErrs.ErrFirmwareInstall) + + jobID, err := mockClient.FirmwareInstall(context.TODO(), "", devices.FirmwareApplyOnReset, false, fh) + if err != nil { + t.Fatal("err in FirmwareUpload" + err.Error()) + } + + assert.Equal(t, "467696020275", jobID) +} + +func Test_firmwareUpdateCompatible(t *testing.T) { + err := mockClient.firmwareUpdateCompatible(context.TODO()) + if err != nil { + t.Fatal(err) + } +} diff --git a/providers/redfish/fixtures/v1/dell/job_delete_ok.json b/providers/redfish/fixtures/v1/dell/job_delete_ok.json new file mode 100644 index 00000000..4d10b194 --- /dev/null +++ b/providers/redfish/fixtures/v1/dell/job_delete_ok.json @@ -0,0 +1,24 @@ +{ + "@Message.ExtendedInfo": [ + { + "Message": "Successfully Completed Request", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "Base.1.7.Success", + "RelatedProperties": [], + "RelatedProperties@odata.count": 0, + "Resolution": "None", + "Severity": "OK" + }, + { + "Message": "The operation successfully completed.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "IDRAC.2.4.SYS413", + "RelatedProperties": [], + "RelatedProperties@odata.count": 0, + "Resolution": "No response action is required.", + "Severity": "Informational" + } + ] +} \ No newline at end of file diff --git a/providers/redfish/fixtures/v1/dell/jobs.json b/providers/redfish/fixtures/v1/dell/jobs.json new file mode 100644 index 00000000..7478af4e --- /dev/null +++ b/providers/redfish/fixtures/v1/dell/jobs.json @@ -0,0 +1,577 @@ +{ + "@odata.context": "/redfish/v1/$metadata#DellJobCollection.DellJobCollection", + "@odata.type": "#DellJobCollection.DellJobCollection", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs", + "Description": "Collection of Job Instances", + "Members": [ + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134386578592", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-15T19:24:36", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_134386578592", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-15T19:24:17", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134561935885", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-16T00:16:54", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_134561935885", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-16T00:16:33", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134628507565", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-16T02:07:51", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_134628507565", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-16T02:07:30", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134734988942", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-16T05:05:19", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_134734988942", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-16T05:04:58", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134769926208", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-16T06:03:33", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_134769926208", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-16T06:03:12", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135707407125", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T08:05:59", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135707407125", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T08:05:40", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135729671798", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T08:43:08", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135729671798", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T08:42:47", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135762432944", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T09:37:42", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135762432944", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T09:37:23", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135792879555", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T10:28:28", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135792879555", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T10:28:07", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135855732001", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T12:13:13", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135855732001", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T12:12:53", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135889086333", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-02-17T13:08:46", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_135889086333", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-02-17T13:08:28", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_155889974199", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-12T16:43:38", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_155889974199", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-12T16:43:17", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157492856325", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-14T14:15:04", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_157492856325", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-14T14:14:45", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157527722202", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-14T15:13:12", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_157527722202", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-14T15:12:52", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157563681404", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-14T16:13:08", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_157563681404", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-14T16:12:48", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157882741266", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-15T01:04:54", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_157882741266", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-15T01:04:34", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157915627166", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-15T01:59:43", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_157915627166", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-15T01:59:22", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158221456256", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-15T10:29:25", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_158221456256", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-15T10:29:05", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158337818297", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-15T13:43:22", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_158337818297", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-15T13:43:01", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158625018022", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-03-15T21:42:02", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_158625018022", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-03-15T21:41:41", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_291745931343", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-08-16T23:30:13", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_291745931343", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-08-16T23:29:53", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_348594655738", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-10-21T18:47:46", + "Description": "Job Instance", + "EndTime": "TIME_NA", + "Id": "JID_348594655738", + "JobState": "Completed", + "JobType": "BIOSConfiguration", + "Message": "Job completed successfully.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "PR19", + "Name": "Configure: BIOS.Setup.1-1", + "PercentComplete": 100, + "StartTime": "2021-10-21T18:37:45", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_348601794602", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2021-10-21T18:49:59", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_348601794602", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2021-10-21T18:49:39", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_421738664260", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": "2022-01-14T09:24:26", + "ActualRunningStopTime": "2022-01-14T09:24:57", + "CompletionTime": "2022-01-14T09:24:57", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_421738664260", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2022-01-14T09:24:26", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_452370412267", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": "2022-02-18T20:17:21", + "ActualRunningStopTime": "2022-02-18T20:17:53", + "CompletionTime": "2022-02-18T20:17:53", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_452370412267", + "JobState": "Completed", + "JobType": "ExportConfiguration", + "Message": "Successfully exported Server Configuration Profile", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS043", + "Name": "Export: Server Configuration Profile", + "PercentComplete": 100, + "StartTime": "2022-02-18T20:17:21", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_467762674724", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": null, + "Description": "Job Instance", + "EndTime": "TIME_NA", + "Id": "JID_467762674724", + "JobState": "Scheduled", + "JobType": "FirmwareUpdate", + "Message": "Task successfully scheduled.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "JCP001", + "Name": "Firmware Update: BIOS", + "PercentComplete": 0, + "StartTime": "2022-03-08T15:51:07", + "TargetSettingsURI": null + }, + { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_467767920358", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2022-03-08T16:02:33", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_467767920358", + "JobState": "Completed", + "JobType": "FirmwareUpdate", + "Message": "Job completed successfully.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "RED001", + "Name": "Firmware Update: iDRAC with Lifecycle Controller", + "PercentComplete": 100, + "StartTime": "2022-03-08T15:59:52", + "TargetSettingsURI": null + } + ], + "Members@odata.count": 27, + "Name": "JobQueue" +} \ No newline at end of file diff --git a/providers/redfish/fixtures/v1/dell/system.embedded.1.json b/providers/redfish/fixtures/v1/dell/system.embedded.1.json new file mode 100644 index 00000000..948fe9d2 --- /dev/null +++ b/providers/redfish/fixtures/v1/dell/system.embedded.1.json @@ -0,0 +1,512 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1", + "@odata.type": "#ComputerSystem.v1_10_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "GracefulRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + "PowerCycle" + ] + } + }, + "AssetTag": "", + "Bios": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios" + }, + "BiosVersion": "2.2.4", + "Boot": { + "BootOptions": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions" + }, + "Certificates": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Boot/Certificates" + }, + "BootOrder": [ + "NIC.Slot.3-1-1", + "HardDisk.List.1-1" + ], + "BootOrder@odata.count": 2, + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideMode": "Legacy", + "BootSourceOverrideTarget": "None", + "UefiTargetBootSourceOverride": null, + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Floppy", + "Cd", + "Hdd", + "BiosSetup", + "Utilities", + "UefiTarget", + "SDCard", + "UefiHttp" + ] + }, + "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" + }, + "HostName": "", + "HostWatchdogTimer": { + "FunctionEnabled": false, + "Status": { + "State": "Disabled" + }, + "TimeoutAction": "None" + }, + "HostingRoles": [], + "HostingRoles@odata.count": 0, + "Id": "System.Embedded.1", + "IndicatorLED": "Lit", + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" + } + ], + "Chassis@odata.count": 1, + "CooledBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/1" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/2" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/3" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/4" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/5" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/6" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/7" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/8" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/9" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/10" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/11" + } + ], + "CooledBy@odata.count": 12, + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" + } + ], + "ManagedBy@odata.count": 1, + "Oem": { + "Dell": { + "@odata.type": "#DellOem.v1_1_0.DellOemLinks", + "BootOrder": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" + }, + "DellBootSources": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" + }, + "DellSoftwareInstallationService": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSoftwareInstallationService" + }, + "DellVideoCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideo" + }, + "DellChassisCollection": { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Oem/Dell/DellChassis" + }, + "DellPresenceAndStatusSensorCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPresenceAndStatusSensors" + }, + "DellSensorCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSensors" + }, + "DellRollupStatusCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRollupStatus" + }, + "DellPSNumericSensorCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPSNumericSensors" + }, + "DellVideoNetworkCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideoNetwork" + }, + "DellOSDeploymentService": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellOSDeploymentService" + }, + "DellMetricService": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellMetricService" + }, + "DellGPUSensorCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellGPUSensors" + }, + "DellRaidService": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRaidService" + }, + "DellNumericSensorCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellNumericSensors" + }, + "DellBIOSService": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBIOSService" + }, + "DellSlotCollection": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSlots" + } + } + }, + "PoweredBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/1" + } + ], + "PoweredBy@odata.count": 2 + }, + "Manufacturer": "Dell Inc.", + "Memory": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Memory" + }, + "MemorySummary": { + "MemoryMirroring": "System", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "TotalSystemMemoryGiB": 64 + }, + "Model": "PowerEdge R6515", + "Name": "System", + "NetworkInterfaces": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkInterfaces" + }, + "Oem": { + "Dell": { + "@odata.type": "#DellOem.v1_1_0.DellOemResources", + "DellSystem": { + "BIOSReleaseDate": "04/12/2021", + "BaseBoardChassisSlot": "NA", + "BatteryRollupStatus": "OK", + "BladeGeometry": "NotApplicable", + "CMCIP": null, + "CPURollupStatus": "OK", + "ChassisModel": "", + "ChassisName": "Main System Chassis", + "ChassisServiceTag": "FOOXD53", + "ChassisSystemHeightUnit": 1, + "CurrentRollupStatus": "OK", + "EstimatedExhaustTemperatureCelsius": 255, + "EstimatedSystemAirflowCFM": 255, + "ExpressServiceCode": "33944916423", + "FanRollupStatus": "OK", + "Id": "System.Embedded.1", + "IDSDMRollupStatus": null, + "IntrusionRollupStatus": "OK", + "IsOEMBranded": "False", + "LastSystemInventoryTime": "2022-04-01T17:01:30+00:00", + "LastUpdateTime": "2022-01-14T15:14:30+00:00", + "LicensingRollupStatus": "OK", + "MaxCPUSockets": 1, + "MaxDIMMSlots": 16, + "MaxPCIeSlots": 5, + "MemoryOperationMode": "OptimizerMode", + "Name": "DellSystem", + "NodeID": "FOOXD53", + "PSRollupStatus": "OK", + "PlatformGUID": "3335444f-c0c6-5880-4410-004c4c4c4544", + "PopulatedDIMMSlots": 8, + "PopulatedPCIeSlots": 2, + "PowerCapEnabledState": "Disabled", + "SDCardRollupStatus": null, + "SELRollupStatus": "OK", + "ServerAllocationWatts": null, + "StorageRollupStatus": "OK", + "SysMemErrorMethodology": "Multi-bitECC", + "SysMemFailOverState": "NotInUse", + "SysMemLocation": "SystemBoardOrMotherboard", + "SysMemPrimaryStatus": "OK", + "SystemGeneration": "15G Monolithic", + "SystemID": 2300, + "SystemRevision": "I", + "TempRollupStatus": "OK", + "TempStatisticsRollupStatus": "OK", + "UUID": "4c4c4544-004c-4410-8058-c6c04f443533", + "VoltRollupStatus": "OK", + "smbiosGUID": "44454c4c-4c00-1044-8058-c6c04f443533", + "@odata.context": "/redfish/v1/$metadata#DellSystem.DellSystem", + "@odata.type": "#DellSystem.v1_2_0.DellSystem", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSystem/System.Embedded.1" + } + } + }, + "PCIeDevices": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8" + } + ], + "PCIeDevices@odata.count": 32, + "PCIeFunctions": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0/PCIeFunctions/195-0-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0/PCIeFunctions/72-0-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2/PCIeFunctions/128-2-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3/PCIeFunctions/128-3-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7/PCIeFunctions/128-7-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4/PCIeFunctions/128-4-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8/PCIeFunctions/128-8-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0/PCIeFunctions/194-0-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0/PCIeFunctions/4-0-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1/PCIeFunctions/192-1-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2/PCIeFunctions/192-2-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4/PCIeFunctions/192-4-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0/PCIeFunctions/192-0-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3/PCIeFunctions/192-3-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8/PCIeFunctions/192-8-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7/PCIeFunctions/192-7-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2/PCIeFunctions/64-2-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3/PCIeFunctions/64-3-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7/PCIeFunctions/64-7-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1/PCIeFunctions/64-1-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4/PCIeFunctions/64-4-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8/PCIeFunctions/64-8-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0/PCIeFunctions/1-0-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2/PCIeFunctions/0-2-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3/PCIeFunctions/0-3-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-3" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7/PCIeFunctions/0-7-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1/PCIeFunctions/0-1-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4/PCIeFunctions/0-4-0" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8/PCIeFunctions/0-8-0" + } + ], + "PCIeFunctions@odata.count": 34, + "PartNumber": "FOOCNNA00", + "PowerState": "On", + "ProcessorSummary": { + "Count": 1, + "LogicalProcessorCount": 48, + "Model": "AMD EPYC 7402P 24-Core Processor", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + } + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors" + }, + "SKU": "FOOXD53", + "SecureBoot": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SecureBoot" + }, + "SerialNumber": "CNCMU0005400IJ", + "SimpleStorage": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SimpleStorage" + }, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage" + }, + "SystemType": "Physical", + "TrustedModules": [ + { + "FirmwareVersion": "1.3.1.0", + "InterfaceType": "TPM2_0", + "Status": { + "State": "Enabled" + } + } + ], + "TrustedModules@odata.count": 1, + "UUID": "4c4c4544-004c-4410-8058-c6c04f443533" +} \ No newline at end of file diff --git a/providers/redfish/fixtures/v1/serviceroot.json b/providers/redfish/fixtures/v1/serviceroot.json new file mode 100644 index 00000000..4bd38c6f --- /dev/null +++ b/providers/redfish/fixtures/v1/serviceroot.json @@ -0,0 +1,80 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Description": "Root Service", + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Fabrics": { + "@odata.id": "/redfish/v1/Fabrics" + }, + "Id": "RootService", + "JobService": { + "@odata.id": "/redfish/v1/JobService" + }, + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "Oem": { + "Dell": { + "@odata.context": "/redfish/v1/$metadata#DellServiceRoot.DellServiceRoot", + "@odata.type": "#DellServiceRoot.v1_0_0.DellServiceRoot", + "IsBranded": 0, + "ManagerMACAddress": "d0:8e:79:bb:3e:ea", + "ServiceTag": "FOOBAR" + } + }, + "Product": "Integrated Dell Remote Access Controller", + "ProtocolFeaturesSupported": { + "ExcerptQuery": false, + "ExpandQuery": { + "ExpandAll": true, + "Levels": true, + "Links": true, + "MaxLevels": 1, + "NoLinks": true + }, + "FilterQuery": true, + "OnlyMemberQuery": true, + "SelectQuery": true + }, + "RedfishVersion": "1.9.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + }, + "Vendor": "Dell" +} \ No newline at end of file diff --git a/providers/redfish/fixtures/v1/systems.json b/providers/redfish/fixtures/v1/systems.json new file mode 100644 index 00000000..1611fec8 --- /dev/null +++ b/providers/redfish/fixtures/v1/systems.json @@ -0,0 +1,13 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection", + "@odata.id": "/redfish/v1/Systems", + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "Description": "Collection of Computer Systems", + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1" + } + ], + "Members@odata.count": 1, + "Name": "Computer System Collection" +} \ No newline at end of file diff --git a/providers/redfish/fixtures/v1/updateservice.json b/providers/redfish/fixtures/v1/updateservice.json new file mode 100644 index 00000000..5816ae58 --- /dev/null +++ b/providers/redfish/fixtures/v1/updateservice.json @@ -0,0 +1,51 @@ +{ + "@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService", + "@odata.id": "/redfish/v1/UpdateService", + "@odata.type": "#UpdateService.v1_8_0.UpdateService", + "Actions": { + "#UpdateService.SimpleUpdate": { + "@Redfish.OperationApplyTimeSupport": { + "@odata.type": "#Settings.v1_3_0.OperationApplyTimeSupport", + "SupportedValues": [ + "Immediate", + "OnReset" + ] + }, + "TransferProtocol@Redfish.AllowableValues": [ + "HTTP", + "NFS", + "CIFS", + "TFTP", + "HTTPS" + ], + "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate" + }, + "Oem": { + "DellUpdateService.v1_1_0#DellUpdateService.Install": { + "InstallUpon@Redfish.AllowableValues": [ + "Now", + "NowAndReboot", + "NextReboot" + ], + "target": "/redfish/v1/UpdateService/Actions/Oem/DellUpdateService.Install" + } + } + }, + "Description": "Represents the properties for the Update Service", + "FirmwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" + }, + "HttpPushUri": "/redfish/v1/UpdateService/FirmwareInventory", + "Id": "UpdateService", + "MaxImageSizeBytes": null, + "MultipartHttpPushUri": "/redfish/v1/UpdateService/MultipartUpload", + "Name": "Update Service", + "ServiceEnabled": true, + "SoftwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/SoftwareInventory" + }, + "Status": { + "Health": "OK", + "State": "Enabled" + } +} \ No newline at end of file diff --git a/providers/redfish/redfish_test.go b/providers/redfish/redfish_test.go new file mode 100644 index 00000000..bcddd429 --- /dev/null +++ b/providers/redfish/redfish_test.go @@ -0,0 +1,85 @@ +package redfish + +import ( + "context" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/sirupsen/logrus" +) + +const ( + fixturesDir = "./fixtures" +) + +var ( + mockServer *httptest.Server + mockBMCHost *url.URL + mockClient *Conn +) + +// jsonResponse returns the fixture json response for a request URI +func jsonResponse(endpoint string) []byte { + + jsonResponsesMap := map[string]string{ + "/redfish/v1/": fixturesDir + "/v1/serviceroot.json", + "/redfish/v1/UpdateService": fixturesDir + "/v1/updateservice.json", + "/redfish/v1/Systems": fixturesDir + "/v1/systems.json", + + "/redfish/v1/Systems/System.Embedded.1": fixturesDir + "/v1/dell/system.embedded.1.json", + "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)": fixturesDir + "/v1/dell/jobs.json", + "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_467762674724": fixturesDir + "/v1/dell/job_delete_ok.json", + } + + fh, err := os.Open(jsonResponsesMap[endpoint]) + if err != nil { + log.Fatalf("%s, failed to open fixture: %s for endpoint: %s", err.Error(), jsonResponsesMap[endpoint], endpoint) + } + + defer fh.Close() + + b, err := ioutil.ReadAll(fh) + if err != nil { + log.Fatalf("%s, failed to read fixture: %s for endpoint: %s", err.Error(), jsonResponsesMap[endpoint], endpoint) + } + + return b +} + +func TestMain(m *testing.M) { + // setup mock server + mockServer = func() *httptest.Server { + handler := http.NewServeMux() + handler.HandleFunc("/redfish/v1/", serviceRoot) + handler.HandleFunc("/redfish/v1/UpdateService/MultipartUpload", multipartUpload) + handler.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)", dellJobs) + + return httptest.NewTLSServer(handler) + }() + + mockBMCHost, _ = url.Parse(mockServer.URL) + + mockClient = &Conn{Host: mockBMCHost.String()} + err := mockClient.Open(context.TODO()) + if err != nil { + log.Fatal(err) + } + + l := logrus.New() + l.Level = logrus.DebugLevel + + os.Exit(m.Run()) +} + +func serviceRoot(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodDelete { + w.WriteHeader(http.StatusNotFound) + } + + _, _ = w.Write(jsonResponse(r.RequestURI)) +} diff --git a/providers/redfish/tasks.go b/providers/redfish/tasks.go new file mode 100644 index 00000000..4525c782 --- /dev/null +++ b/providers/redfish/tasks.go @@ -0,0 +1,173 @@ +package redfish + +import ( + "encoding/json" + "io/ioutil" + "strconv" + "strings" + + "github.com/bmc-toolbox/bmclib/devices" + bmcliberrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/pkg/errors" + + rfcommon "github.com/stmcginnis/gofish/common" + rf "github.com/stmcginnis/gofish/redfish" +) + +// Dell specific redfish methods + +var ( + componentSlugDellJobName = map[string]string{ + devices.SlugBIOS: "Firmware Update: BIOS", + devices.SlugBMC: "Firmware Update: iDRAC with Lifecycle Controller", + } +) + +type dellJob struct { + PercentComplete int + OdataID string `json:"@odata.id"` + StartTime string + CompletionTime string + ID string + Message string + Name string + JobState string + JobType string +} + +type dellJobResponseData struct { + Members []*dellJob +} + +// dellJobID formats and returns taskID as a Dell Job ID +func dellJobID(id string) string { + if !strings.HasPrefix(id, "JID") { + return "JID_" + id + } + + return id +} + +func (c *Conn) getDellFirmwareInstallTaskScheduled(slug string) (*rf.Task, error) { + // get tasks by state + tasks, err := c.dellJobs("scheduled") + if err != nil { + return nil, err + } + + // filter to match the task Name based on the component slug + for _, task := range tasks { + // Firmware Update: BIOS + if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { + return task, nil + } + } + + return nil, nil +} + +func (c *Conn) dellPurgeScheduledFirmwareInstallJob(slug string) error { + // get tasks by state + tasks, err := c.dellJobs("scheduled") + if err != nil { + return err + } + + // filter to match the task Name based on the component slug + for _, task := range tasks { + // Firmware Update: BIOS + if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { + err = c.dellPurgeJob(task.ID) + if err != nil { + return err + } + } + } + + return nil +} + +func (c *Conn) dellPurgeJob(id string) error { + id = dellJobID(id) + + endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/" + id + + resp, err := c.conn.Delete(endpoint) + if err != nil { + return errors.Wrap(bmcliberrs.ErrTaskPurge, err.Error()) + } + + if resp.StatusCode != 200 { + return errors.Wrap(bmcliberrs.ErrTaskPurge, "response code: "+resp.Status) + } + + return nil +} + +// dellFirmwareUpdateTaskStatus looks up the Dell Job and returns it as a redfish task object +func (c *Conn) dellJobAsRedfishTask(jobID string) (*rf.Task, error) { + jobID = dellJobID(jobID) + + tasks, err := c.dellJobs("") + if err != nil { + return nil, err + } + + for _, task := range tasks { + if task.ID == jobID { + return task, nil + } + } + + return nil, errors.Wrap(bmcliberrs.ErrTaskNotFound, "task with ID not found: "+jobID) +} + +// dellJobs returns all dell jobs as redfish task objects +// state: optional +func (c *Conn) dellJobs(state string) ([]*rf.Task, error) { + endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)" + + resp, err := c.conn.Get(endpoint) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, errors.New("dell jobs endpoint returned unexpected status code: " + strconv.Itoa(resp.StatusCode)) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + data := dellJobResponseData{} + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + tasks := []*rf.Task{} + for _, job := range data.Members { + if state != "" && !strings.EqualFold(job.JobState, state) { + continue + } + + tasks = append(tasks, &rf.Task{ + Entity: rfcommon.Entity{ + ID: job.ID, + ODataID: job.OdataID, + Name: job.Name, + Client: c.conn, + }, + Description: job.Name, + PercentComplete: job.PercentComplete, + StartTime: job.StartTime, + EndTime: job.CompletionTime, + TaskState: rf.TaskState(job.JobState), + TaskStatus: rfcommon.Health(job.Message), // abuse the TaskStatus to include any status message + }) + } + + return tasks, nil +} diff --git a/providers/redfish/tasks_test.go b/providers/redfish/tasks_test.go new file mode 100644 index 00000000..bf1a376b --- /dev/null +++ b/providers/redfish/tasks_test.go @@ -0,0 +1,40 @@ +package redfish + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// handler registered in redfish_test.go +func dellJobs(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + w.WriteHeader(http.StatusNotFound) + } + + _, _ = w.Write(jsonResponse(r.RequestURI)) +} + +func Test_dellFirmwareUpdateTask(t *testing.T) { + // see fixtures/v1/dell/jobs.json for the job IDs + // completed job + status, err := mockClient.dellJobAsRedfishTask("467767920358") + if err != nil { + t.Fatal(err) + } + + assert.NotNil(t, status) + assert.Equal(t, "2022-03-08T16:02:33", status.EndTime) + assert.Equal(t, "2022-03-08T15:59:52", status.StartTime) + assert.Equal(t, 100, status.PercentComplete) + assert.Equal(t, "Completed", string(status.TaskState)) + assert.Equal(t, "Job completed successfully.", string(status.TaskStatus)) +} + +func Test_dellPurgeScheduledFirmwareInstallJob(t *testing.T) { + err := mockClient.dellPurgeScheduledFirmwareInstallJob("bios") + if err != nil { + t.Fatal(err) + } +} From b061360e1de75c8638dd3fe9c5aa16b61a859147 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:35:30 +0200 Subject: [PATCH 07/25] providers/asrockrack: rework, implement FirmwareInstaller, FirmwareInstallVerifier interface methods - use context for all requests - implement various helper methods for inventory, firmware updates - split up firmware install, status methods - add tests --- providers/asrockrack/asrockrack.go | 197 ++-------------- providers/asrockrack/asrockrack_test.go | 35 +-- providers/asrockrack/firmware.go | 231 +++++++++++++++++++ providers/asrockrack/helpers.go | 284 ++++++++++++++++++++---- providers/asrockrack/mock_test.go | 54 +++++ providers/asrockrack/user.go | 10 +- providers/asrockrack/user_test.go | 14 +- 7 files changed, 561 insertions(+), 264 deletions(-) create mode 100644 providers/asrockrack/firmware.go diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go index caac9258..c3ca04cf 100644 --- a/providers/asrockrack/asrockrack.go +++ b/providers/asrockrack/asrockrack.go @@ -7,8 +7,8 @@ import ( "fmt" "io" "net/http" - "time" + "github.com/bmc-toolbox/bmclib/devices" "github.com/bmc-toolbox/bmclib/internal/httpclient" "github.com/bmc-toolbox/bmclib/providers" "github.com/go-logr/logr" @@ -25,10 +25,10 @@ const ( var ( // Features implemented by asrockrack https Features = registrar.Features{ - providers.FeatureBiosVersionRead, - providers.FeatureBmcVersionRead, - providers.FeatureBiosFirmwareUpdate, - providers.FeatureBmcFirmwareUpdate, + providers.FeatureInventoryRead, + providers.FeatureFirmwareInstall, + providers.FeatureFirmwareInstallStatus, + providers.FeaturePostCodeRead, } ) @@ -96,7 +96,7 @@ func NewWithOptions(ip string, username string, password string, log logr.Logger // Compatible implements the registrar.Verifier interface // returns true if the BMC is identified to be an asrockrack func (a *ASRockRack) Compatible() bool { - resp, statusCode, err := a.queryHTTPS("/", "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(context.TODO(), "/", "GET", nil, nil, 0) if err != nil { return false } @@ -110,7 +110,7 @@ func (a *ASRockRack) Compatible() bool { // Open a connection to a BMC, implements the Opener interface func (a *ASRockRack) Open(ctx context.Context) (err error) { - return a.httpsLogin() + return a.httpsLogin(ctx) } // Close a connection to a BMC, implements the Closer interface @@ -119,186 +119,25 @@ func (a *ASRockRack) Close(ctx context.Context) (err error) { return nil } - return a.httpsLogout() + return a.httpsLogout(ctx) } // CheckCredentials verify whether the credentials are valid or not -func (a *ASRockRack) CheckCredentials() (err error) { - return a.httpsLogin() +func (a *ASRockRack) CheckCredentials(ctx context.Context) (err error) { + return a.httpsLogin(ctx) } -// BiosVersion returns the BIOS version from the BMC -func (a *ASRockRack) GetBIOSVersion(ctx context.Context) (string, error) { - fwInfo, err := a.firmwareInfo() +func (a *ASRockRack) GetPostCode(ctx context.Context) (status string, code int, err error) { + postInfo, err := a.postCodeInfo(ctx) if err != nil { - return "", err + return status, code, err } - return fwInfo.BIOSVersion, nil -} - -// BMCVersion returns the BMC version -func (a *ASRockRack) GetBMCVersion(ctx context.Context) (string, error) { - fwInfo, err := a.firmwareInfo() - if err != nil { - return "", err - } - - return fwInfo.BMCVersion, nil -} - -// nolint: gocyclo -// BMC firmware update is a multi step process -// this method initiates the upgrade process and waits in a loop until the device has been upgraded -// fileSize is required to setup the multipart form upload Content-Length -func (a *ASRockRack) FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader, fileSize int64) error { - defer func() { - // The device needs to be reset to be removed from flash mode, - // this is required once setFlashMode() is invoked. - // The BMC resets itself once a firmware flash is successful or failed. - if a.resetRequired { - a.log.V(1).Info("info", "resetting BMC, this takes a few minutes") - err := a.reset() - if err != nil { - a.log.Error(err, "failed to reset BMC") - } - } - }() - - var err error - - // 1. set the device to flash mode - prepares the flash - a.log.V(2).Info("info", "action", "set device to flash mode, takes a minute...", "step", "1/5") - err = a.setFlashMode() - if err != nil { - return fmt.Errorf("failed in step 1/5 - set device to flash mode: " + err.Error()) - } - - // 2. upload firmware image file - a.log.V(2).Info("info", "action", "upload BMC firmware image", "step", "2/5") - err = a.uploadFirmware("api/maintenance/firmware", fileReader, fileSize) - if err != nil { - return fmt.Errorf("failed in step 2/5 - upload BMC firmware image: " + err.Error()) - } - - // 3. BMC to verify the uploaded file - err = a.verifyUploadedFirmware() - a.log.V(2).Info("info", "action", "BMC verify uploaded firmware", "step", "3/5") - if err != nil { - return fmt.Errorf("failed in step 3/5 - BMC verify uploaded firmware: " + err.Error()) - } - - startTS := time.Now() - // 4. Run the upgrade - preserving current config - a.log.V(2).Info("info", "action", "proceed with upgrade, preserve current configuration", "step", "4/5") - err = a.upgradeBMC() - if err != nil { - return fmt.Errorf("failed in step 4/5 - proceed with upgrade: " + err.Error()) - } - - // progress check interval - progressT := time.NewTicker(500 * time.Millisecond).C - // timeout interval - timeoutT := time.NewTicker(20 * time.Minute).C - maxErrors := 20 - var errorsCount int - - // 5.loop until firmware was updated - with a timeout - for { - select { - case <-progressT: - // check progress - endpoint := "api/maintenance/firmware/flash-progress" - p, err := a.flashProgress(endpoint) - if err != nil { - errorsCount++ - a.log.V(2).Error(err, "step", "5/5 - error checking flash progress", "error count", errorsCount, "max errors", maxErrors, "elapsed time", time.Since(startTS).String()) - continue - } - - a.log.V(2).Info("info", "action", p.Action, "step", "5/5", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) - - // all done! - if p.State == 2 { - a.log.V(2).Info("info", "action", "flash process complete", "step", "5/5", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) - // The BMC resets by itself after a successful flash - a.resetRequired = false - // HTTP sessions are terminated once the BMC resets after an upgrade - a.skipLogout = true - return nil - } - case <-timeoutT: - return fmt.Errorf("timeout in step 5/5 - flash progress, error count: %d, elapsed time: %s", errorsCount, time.Since(startTS).String()) - } - } -} - -func (a *ASRockRack) FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader, fileSize int64) error { - defer func() { - if a.resetRequired { - a.log.V(2).Info("info", "resetting BMC, this takes a few minutes") - err := a.reset() - if err != nil { - a.log.Error(err, "failed to reset BMC") - } - } - }() - - var err error - - // 1. upload firmware image file - a.log.V(2).Info("info", "action", "upload BIOS firmware image", "step", "1/4") - err = a.uploadFirmware("api/asrr/maintenance/BIOS/firmware", fileReader, fileSize) - if err != nil { - return fmt.Errorf("failed in step 1/4 - upload firmware image: " + err.Error()) + code = postInfo.PostData + status, exists := knownPOSTCodes[code] + if !exists { + status = devices.POSTCodeUnknown } - // 2. set update parameters to preserve configurations - a.log.V(2).Info("info", "action", "set flash configuration", "step", "2/4") - err = a.biosUpgradeConfiguration() - if err != nil { - return fmt.Errorf("failed in step 2/4 - set flash configuration: " + err.Error()) - } - - startTS := time.Now() - // 3. run upgrade - a.log.V(2).Info("info", "action", "proceed with upgrade", "step", "3/4") - err = a.biosUpgrade() - if err != nil { - return fmt.Errorf("failed in step 3/4 - proceed with upgrade: " + err.Error()) - } - - // progress check interval - progressT := time.NewTicker(2 * time.Second).C - // timeout interval - timeoutT := time.NewTicker(30 * time.Minute).C - maxErrors := 20 - var errorsCount int - - // 5.loop until firmware was updated - with a timeout - for { - select { - case <-progressT: - // check progress - endpoint := "api/asrr/maintenance/BIOS/flash-progress" - p, err := a.flashProgress(endpoint) - if err != nil { - errorsCount++ - a.log.V(2).Error(err, "action", "check flash progress", "step", "4/4", "error count", errorsCount, "max errors", maxErrors, "elapsed time", time.Since(startTS).String()) - continue - } - - a.log.V(2).Info("info", "action", "check flash progress", "step", "4/4", "progress", p.Progress, "action", p.Action, "elapsed time", time.Since(startTS).String()) - - // all done! - if p.State == 2 { - a.log.V(2).Info("info", "action", "flash process complete", "step", "4/4", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) - // Reset BMC after flash - a.resetRequired = true - return nil - } - case <-timeoutT: - return fmt.Errorf("timeout in step 4/4 - flash progress, error count: %d, elapsed time: %s", errorsCount, time.Since(startTS).String()) - } - } + return status, code, nil } diff --git a/providers/asrockrack/asrockrack_test.go b/providers/asrockrack/asrockrack_test.go index f127a5eb..9006cbdf 100644 --- a/providers/asrockrack/asrockrack_test.go +++ b/providers/asrockrack/asrockrack_test.go @@ -16,7 +16,7 @@ func Test_Compatible(t *testing.T) { } func Test_httpLogin(t *testing.T) { - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } @@ -25,44 +25,19 @@ func Test_httpLogin(t *testing.T) { } func Test_Close(t *testing.T) { - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } - err = aClient.httpsLogout() + err = aClient.httpsLogout(context.TODO()) if err != nil { t.Errorf(err.Error()) } } -func Test_FirmwareInfo(t *testing.T) { - expected := firmwareInfo{ - BMCVersion: "0.01.00", - BIOSVersion: "L2.07B", - MEVersion: "5.1.3.78", - MicrocodeVersion: "000000ca", - CPLDVersion: "N/A", - CMVersion: "0.13.01", - BPBVersion: "0.0.002.0", - NodeID: "2", - } - - err := aClient.httpsLogin() - if err != nil { - t.Errorf(err.Error()) - } - - fwInfo, err := aClient.firmwareInfo() - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, expected, fwInfo) -} - func Test_FirwmwareUpdateBMC(t *testing.T) { - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } @@ -79,7 +54,7 @@ func Test_FirwmwareUpdateBMC(t *testing.T) { } defer fh.Close() - err = aClient.FirmwareUpdateBMC(context.TODO(), fh, 0) + err = aClient.firmwareInstallBMC(context.TODO(), fh, 0) if err != nil { t.Errorf(err.Error()) } diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go new file mode 100644 index 00000000..c5fd9f02 --- /dev/null +++ b/providers/asrockrack/firmware.go @@ -0,0 +1,231 @@ +package asrockrack + +import ( + "context" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/bmc-toolbox/bmclib/devices" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" +) + + + +// FirmwareInstall uploads and initiates firmware update for the component +func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { + var size int64 + if file, ok := reader.(*os.File); ok { + finfo, err := file.Stat() + if err != nil { + a.log.V(2).Info("warn", "unable to determine file size: "+err.Error()) + } + + size = finfo.Size() + } + + switch component { + case devices.SlugBIOS: + err = a.firmwareInstallBIOS(ctx, reader, size) + case devices.SlugBMC: + err = a.firmwareInstallBMC(ctx, reader, size) + default: + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + + } + + return jobID, err +} + +// FirmwareInstallStatus returns the status of the firmware install process, a bool value indicating if the component requires a reset +func (a *ASRockRack) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { + switch component { + case devices.SlugBIOS: + return a.firmwareUpdateBIOSStatus(ctx, installVersion) + case devices.SlugBMC: + return a.firmwareUpdateBMCStatus(ctx, installVersion) + default: + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + } +} + +// firmwareInstallBMC uploads and installs firmware for the BMC component +func (a *ASRockRack) firmwareInstallBMC(ctx context.Context, reader io.Reader, fileSize int64) error { + var err error + + // 1. set the device to flash mode - prepares the flash + a.log.V(2).Info("info", "action", "set device to flash mode, takes a minute...", "step", "1/4") + err = a.setFlashMode(ctx) + if err != nil { + return fmt.Errorf("failed in step 1/4 - set device to flash mode: " + err.Error()) + } + + // 2. upload firmware image file + a.log.V(2).Info("info", "action", "upload BMC firmware image", "step", "2/4") + err = a.uploadFirmware(ctx, "api/maintenance/firmware", reader, fileSize) + if err != nil { + return fmt.Errorf("failed in step 2/4 - upload BMC firmware image: " + err.Error()) + } + + // 3. BMC to verify the uploaded file + err = a.verifyUploadedFirmware(ctx) + a.log.V(2).Info("info", "action", "BMC verify uploaded firmware", "step", "3/4") + if err != nil { + return fmt.Errorf("failed in step 3/4 - BMC verify uploaded firmware: " + err.Error()) + } + + // 4. Run the upgrade - preserving current config + a.log.V(2).Info("info", "action", "proceed with upgrade, preserve current configuration", "step", "4/4") + err = a.upgradeBMC(ctx) + if err != nil { + return fmt.Errorf("failed in step 4/4 - proceed with upgrade: " + err.Error()) + } + + return nil +} + +// firmwareInstallBIOS uploads and installs firmware for the BIOS component +func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, fileSize int64) error { + var err error + + // 1. upload firmware image file + a.log.V(2).Info("info", "action", "upload BIOS firmware image", "step", "1/3") + err = a.uploadFirmware(ctx, "api/asrr/maintenance/BIOS/firmware", reader, fileSize) + if err != nil { + return fmt.Errorf("failed in step 1/3 - upload firmware image: " + err.Error()) + } + + // 2. set update parameters to preserve configurations + a.log.V(2).Info("info", "action", "set flash configuration", "step", "2/3") + err = a.biosUpgradeConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed in step 2/3 - set flash configuration: " + err.Error()) + } + + // 3. run upgrade + a.log.V(2).Info("info", "action", "proceed with upgrade", "step", "3/3") + err = a.upgradeBIOS(ctx) + if err != nil { + return fmt.Errorf("failed in step 3/3 - proceed with upgrade: " + err.Error()) + } + + return nil +} + +// firmwareUpdateBMCStatus returns the BMC firmware install status +func (a *ASRockRack) firmwareUpdateBMCStatus(ctx context.Context, installVersion string) (status string, err error) { + os.Setenv("BMCLIB_LOG_LEVEL", "trace") + defer os.Unsetenv("BMCLIB_LOG_LEVEL") + endpoint := "api/maintenance/firmware/flash-progress" + p, progressErr := a.flashProgress(ctx, endpoint) + if progressErr != nil { + installed, versionErr := a.versionInstalled(ctx, devices.SlugBMC, installVersion) + if err != nil { + return "", versionErr + } + + if installed { + return devices.FirmwareInstallComplete, nil + } + + return "", progressErr + } + + switch p.State { + case 0: + return devices.FirmwareInstallRunning, nil + case 2: + return devices.FirmwareInstallComplete, nil + default: + a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(p.State)) + } + + // at this point the flash-progress endpoint isn't returning useful information + // query the fimware info endpoint to identify the installed version + installed, err := a.versionInstalled(ctx, devices.SlugBMC, installVersion) + if err != nil { + return "", err + } + + if installed { + return devices.FirmwareInstallComplete, nil + } + + return devices.FirmwareInstallUnknown, nil +} + +// firmwareUpdateBIOSStatus returns the BIOS firmware install status +func (a *ASRockRack) firmwareUpdateBIOSStatus(ctx context.Context, installVersion string) (status string, err error) { + os.Setenv("BMCLIB_LOG_LEVEL", "trace") + defer os.Unsetenv("BMCLIB_LOG_LEVEL") + + endpoint := "api/asrr/maintenance/BIOS/flash-progress" + p, progressErr := a.flashProgress(ctx, endpoint) + if progressErr != nil { + installed, versionErr := a.versionInstalled(ctx, devices.SlugBIOS, installVersion) + if versionErr != nil { + return "", versionErr + } + + if installed { + return devices.FirmwareInstallComplete, nil + } + + return "", progressErr + } + + switch p.State { + // Note: we're ignoring case 1 here, should it just be part of case 0 + case 0: + return devices.FirmwareInstallRunning, nil + case 2: + return devices.FirmwareInstallComplete, nil + default: + a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(p.State)) + } + + // at this point the flash-progress endpoint isn't returning useful information + // query the fimware info endpoint to identify the installed version + installed, err := a.versionInstalled(ctx, devices.SlugBIOS, installVersion) + if err != nil { + return "", err + } + + if installed { + return devices.FirmwareInstallComplete, nil + } + + return devices.FirmwareInstallUnknown, nil +} + +// versionInstalled returns a +func (a *ASRockRack) versionInstalled(ctx context.Context, component, version string) (bool, error) { + fwInfo, err := a.firmwareInfo(ctx) + if err != nil { + err = errors.Wrap(err, "error querying for firmware info: ") + a.log.V(3).Info("warn", err.Error()) + return false, err + } + + var installed string + + switch component { + case devices.SlugBIOS: + installed = fwInfo.BIOSVersion + case devices.SlugBMC: + installed = fwInfo.BMCVersion + default: + return false, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + } + + if strings.EqualFold(installed, version) { + fmt.Println("YEP!") + return true, nil + } + + return false, nil +} diff --git a/providers/asrockrack/helpers.go b/providers/asrockrack/helpers.go index 9114ae54..8ff75659 100644 --- a/providers/asrockrack/helpers.go +++ b/providers/asrockrack/helpers.go @@ -2,6 +2,7 @@ package asrockrack import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -11,6 +12,7 @@ import ( "net/http/httputil" "os" + "github.com/bmc-toolbox/bmclib/devices" "github.com/bmc-toolbox/bmclib/errors" ) @@ -34,6 +36,66 @@ type firmwareInfo struct { NodeID string `json:"Node_id"` } +type biosPOSTCode struct { + PostStatus int `json:"poststatus"` + PostData int `json:"postdata"` +} + +// component is part of a payload returned by the inventory info endpoint +type component struct { + DeviceID int `json:"device_id"` + DeviceName string `json:"device_name"` + DeviceType string `json:"device_type"` + ProductManufacturerName string `json:"product_manufacturer_name"` + ProductName string `json:"product_name"` + ProductPartNumber string `json:"product_part_number"` + ProductVersion string `json:"product_version"` + ProductSerialNumber string `json:"product_serial_number"` + ProductAssetTag string `json:"product_asset_tag"` + ProductExtra string `json:"product_extra"` +} + +// fru is part of a payload returned by the fru info endpoint +type fru struct { + Component string + Version int `json:"version"` + Length int `json:"length"` + Language int `json:"language"` + Manufacturer string `json:"manufacturer"` + ProductName string `json:"product_name"` + PartNumber string `json:"part_number"` + ProductVersion string `json:"product_version"` + SerialNumber string `json:"serial_number"` + AssetTag string `json:"asset_tag"` + FruFileID string `json:"fru_file_id"` + Type string `json:"type"` + CustomFields string `json:"custom_fields"` +} + +// sensor is part of the payload returned by the sensors endpoint +type sensor struct { + ID int `json:"id"` + SensorNumber int `json:"sensor_number"` + Name string `json:"name"` + OwnerID int `json:"owner_id"` + OwnerLun int `json:"owner_lun"` + RawReading float64 `json:"raw_reading"` + Type string `json:"type"` + TypeNumber int `json:"type_number"` + Reading float64 `json:"reading"` + SensorState int `json:"sensor_state"` + DiscreteState int `json:"discrete_state"` + SettableReadableThreshMask int `json:"settable_readable_threshMask"` + LowerNonRecoverableThreshold float64 `json:"lower_non_recoverable_threshold"` + LowerCriticalThreshold float64 `json:"lower_critical_threshold"` + LowerNonCriticalThreshold float64 `json:"lower_non_critical_threshold"` + HigherNonCriticalThreshold float64 `json:"higher_non_critical_threshold"` + HigherCriticalThreshold float64 `json:"higher_critical_threshold"` + HigherNonRecoverableThreshold float64 `json:"higher_non_recoverable_threshold"` + Accessible int `json:"accessible"` + Unit string `json:"unit"` +} + // Payload to preseve config when updating the BMC firmware type preserveConfig struct { FlashStatus int `json:"flash_status"` // 1 = full firmware flash, 2 = section based flash, 3 - version compare flash @@ -52,6 +114,12 @@ type upgradeProgress struct { State int `json:"state,omitempty"` } +// Chassis status struct +type chassisStatus struct { + PowerStatus int `json:"power_status"` + LEDStatus int `json:"led_status"` +} + // BIOS upgrade commands // 2 == configure // 3 == apply upgrade @@ -59,10 +127,20 @@ type biosUpdateAction struct { Action int `json:"action"` } -func (a *ASRockRack) listUsers() ([]*UserAccount, error) { +var ( + knownPOSTCodes = map[int]string{ + 160: devices.POSTStateOS, + 2: devices.POSTStateBootINIT, // no differentiation between BIOS init and PXE boot + 144: devices.POSTStateUEFI, + 154: devices.POSTStateUEFI, + 178: devices.POSTStateUEFI, + } +) + +func (a *ASRockRack) listUsers(ctx context.Context) ([]*UserAccount, error) { endpoint := "api/settings/users" - resp, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) if err != nil { return nil, err } @@ -81,7 +159,7 @@ func (a *ASRockRack) listUsers() ([]*UserAccount, error) { return accounts, nil } -func (a *ASRockRack) createUpdateUser(account *UserAccount) error { +func (a *ASRockRack) createUpdateUser(ctx context.Context, account *UserAccount) error { endpoint := "api/settings/users/" + fmt.Sprintf("%d", account.ID) payload, err := json.Marshal(account) @@ -90,7 +168,7 @@ func (a *ASRockRack) createUpdateUser(account *UserAccount) error { } headers := map[string]string{"Content-Type": "application/json"} - _, statusCode, err := a.queryHTTPS(endpoint, "PUT", bytes.NewReader(payload), headers, 0) + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", bytes.NewReader(payload), headers, 0) if err != nil { return err } @@ -105,10 +183,10 @@ func (a *ASRockRack) createUpdateUser(account *UserAccount) error { // 1 Set BMC to flash mode and prepare flash area // at this point all logged in sessions are terminated // and no logins are permitted -func (a *ASRockRack) setFlashMode() error { +func (a *ASRockRack) setFlashMode(ctx context.Context) error { endpoint := "api/maintenance/flash" - _, statusCode, err := a.queryHTTPS(endpoint, "PUT", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", nil, nil, 0) if err != nil { return err } @@ -131,7 +209,7 @@ func multipartSize(fieldname, filename string) int64 { } // 2 Upload the firmware file -func (a *ASRockRack) uploadFirmware(endpoint string, fwReader io.Reader, fileSize int64) error { +func (a *ASRockRack) uploadFirmware(ctx context.Context, endpoint string, fwReader io.Reader, fileSize int64) error { fieldName, fileName := "fwimage", "image" contentLength := multipartSize(fieldName, fileName) + fileSize @@ -170,7 +248,7 @@ func (a *ASRockRack) uploadFirmware(endpoint string, fwReader io.Reader, fileSiz } // POST payload - _, statusCode, err := a.queryHTTPS(endpoint, "POST", pipeReader, headers, contentLength) + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", pipeReader, headers, contentLength) if err != nil { return err } @@ -183,10 +261,10 @@ func (a *ASRockRack) uploadFirmware(endpoint string, fwReader io.Reader, fileSiz } // 3. Verify uploaded firmware file - to be invoked after uploadFirmware() -func (a *ASRockRack) verifyUploadedFirmware() error { +func (a *ASRockRack) verifyUploadedFirmware(ctx context.Context) error { endpoint := "api/maintenance/firmware/verification" - _, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) if err != nil { return err } @@ -199,7 +277,7 @@ func (a *ASRockRack) verifyUploadedFirmware() error { } // 4. Start firmware flashing process - to be invoked after verifyUploadedFirmware -func (a *ASRockRack) upgradeBMC() error { +func (a *ASRockRack) upgradeBMC(ctx context.Context) error { endpoint := "api/maintenance/firmware/upgrade" // preserve all configuration during upgrade, full flash @@ -210,23 +288,7 @@ func (a *ASRockRack) upgradeBMC() error { } headers := map[string]string{"Content-Type": "application/json"} - _, statusCode, err := a.queryHTTPS(endpoint, "PUT", bytes.NewReader(payload), headers, 0) - if err != nil { - return err - } - - if statusCode != http.StatusOK { - return fmt.Errorf("non 200 response: %d", statusCode) - } - - return nil -} - -// 4. reset BMC -func (a *ASRockRack) reset() error { - endpoint := "api/maintenance/reset" - - _, statusCode, err := a.queryHTTPS(endpoint, "POST", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", bytes.NewReader(payload), headers, 0) if err != nil { return err } @@ -239,8 +301,8 @@ func (a *ASRockRack) reset() error { } // 5. firmware flash progress -func (a *ASRockRack) flashProgress(endpoint string) (*upgradeProgress, error) { - resp, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil, 0) +func (a *ASRockRack) flashProgress(ctx context.Context, endpoint string) (*upgradeProgress, error) { + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) if err != nil { return nil, err } @@ -259,10 +321,10 @@ func (a *ASRockRack) flashProgress(endpoint string) (*upgradeProgress, error) { } // Query firmware information from the BMC -func (a *ASRockRack) firmwareInfo() (*firmwareInfo, error) { +func (a *ASRockRack) firmwareInfo(ctx context.Context) (*firmwareInfo, error) { endpoint := "api/asrr/fw-info" - resp, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) if err != nil { return nil, err } @@ -280,9 +342,123 @@ func (a *ASRockRack) firmwareInfo() (*firmwareInfo, error) { return f, nil } +// Query BIOS/UEFI POST code information from the BMC +func (a *ASRockRack) postCodeInfo(ctx context.Context) (*biosPOSTCode, error) { + endpoint := "/api/asrr/getbioscode" + + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + b := &biosPOSTCode{} + err = json.Unmarshal(resp, b) + if err != nil { + return nil, err + } + + return b, nil +} + +// Query the inventory info endpoint +func (a *ASRockRack) inventoryInfo(ctx context.Context) ([]*component, error) { + endpoint := "api/asrr/inventory_info" + + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + components := []*component{} + err = json.Unmarshal(resp, &components) + if err != nil { + return nil, err + } + + return components, nil +} + +// Query the fru info endpoint +func (a *ASRockRack) fruInfo(ctx context.Context) ([]*fru, error) { + endpoint := "api/fru" + + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + data := []map[string]*fru{} + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + if len(data) == 0 { + return nil, fmt.Errorf("no FRU data returned") + } + + frus := []*fru{} + for key, f := range data[0] { + switch key { + case "chassis", "board", "product": + frus = append(frus, &fru{ + Component: key, + Version: f.Version, + Length: f.Length, + Language: f.Language, + Manufacturer: f.Manufacturer, + ProductName: f.ProductName, + PartNumber: f.PartNumber, + ProductVersion: f.ProductVersion, + SerialNumber: f.SerialNumber, + AssetTag: f.SerialNumber, + FruFileID: f.FruFileID, + CustomFields: f.CustomFields, + Type: f.Type, + }) + } + } + + return frus, nil +} + +// Query the sensors endpoint +func (a *ASRockRack) sensors(ctx context.Context) ([]*sensor, error) { + endpoint := "api/sensors" + + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + sensors := []*sensor{} + err = json.Unmarshal(resp, &sensors) + if err != nil { + return nil, err + } + + return sensors, nil +} + // Set the BIOS upgrade configuration // - preserve current configuration -func (a *ASRockRack) biosUpgradeConfiguration() error { +func (a *ASRockRack) biosUpgradeConfiguration(ctx context.Context) error { endpoint := "api/asrr/maintenance/BIOS/configuration" // Preserve existing configuration? @@ -293,7 +469,7 @@ func (a *ASRockRack) biosUpgradeConfiguration() error { } headers := map[string]string{"Content-Type": "application/json"} - resp, statusCode, err := a.queryHTTPS(endpoint, "POST", bytes.NewReader(payload), headers, 0) + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return err } @@ -312,7 +488,7 @@ func (a *ASRockRack) biosUpgradeConfiguration() error { } // Run BIOS upgrade -func (a *ASRockRack) biosUpgrade() error { +func (a *ASRockRack) upgradeBIOS(ctx context.Context) error { endpoint := "api/asrr/maintenance/BIOS/upgrade" // Run upgrade @@ -323,7 +499,7 @@ func (a *ASRockRack) biosUpgrade() error { } headers := map[string]string{"Content-Type": "application/json"} - resp, statusCode, err := a.queryHTTPS(endpoint, "POST", bytes.NewReader(payload), headers, 0) + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return err } @@ -341,8 +517,30 @@ func (a *ASRockRack) biosUpgrade() error { return nil } +// Returns the chassis status object which includes the power state +func (a *ASRockRack) chassisStatusInfo(ctx context.Context) (*chassisStatus, error) { + endpoint := "/api/chassis-status" + + resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + chassisStatus := chassisStatus{} + err = json.Unmarshal(resp, &chassisStatus) + if err != nil { + return nil, err + } + + return &chassisStatus, nil +} + // Aquires a session id cookie and a csrf token -func (a *ASRockRack) httpsLogin() error { +func (a *ASRockRack) httpsLogin(ctx context.Context) error { urlEndpoint := "api/session" // login payload @@ -355,7 +553,7 @@ func (a *ASRockRack) httpsLogin() error { headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} - resp, statusCode, err := a.queryHTTPS(urlEndpoint, "POST", bytes.NewReader(payload), headers, 0) + resp, statusCode, err := a.queryHTTPS(ctx, urlEndpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return fmt.Errorf("Error logging in: " + err.Error()) } @@ -374,10 +572,10 @@ func (a *ASRockRack) httpsLogin() error { } // Close ends the BMC session -func (a *ASRockRack) httpsLogout() error { +func (a *ASRockRack) httpsLogout(ctx context.Context) error { urlEndpoint := "api/session" - _, statusCode, err := a.queryHTTPS(urlEndpoint, "DELETE", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, urlEndpoint, "DELETE", nil, nil, 0) if err != nil { return fmt.Errorf("Error logging out: " + err.Error()) } @@ -395,13 +593,13 @@ func (a *ASRockRack) httpsLogout() error { // queryHTTPS run the HTTPS query passing in the required headers // the / suffix should be excluded from the URLendpoint // returns - response body, http status code, error if any -func (a *ASRockRack) queryHTTPS(URLendpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { +func (a *ASRockRack) queryHTTPS(ctx context.Context, endpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { var body []byte var err error var req *http.Request - URL := fmt.Sprintf("https://%s/%s", a.ip, URLendpoint) - req, err = http.NewRequest(method, URL, payload) + URL := fmt.Sprintf("https://%s/%s", a.ip, endpoint) + req, err = http.NewRequestWithContext(ctx, method, URL, payload) if err != nil { return nil, 0, err } diff --git a/providers/asrockrack/mock_test.go b/providers/asrockrack/mock_test.go index 5eaf717b..cd71a3f6 100644 --- a/providers/asrockrack/mock_test.go +++ b/providers/asrockrack/mock_test.go @@ -25,6 +25,11 @@ var ( fwVerificationResponse = []byte(`[ { "id": 1, "current_image_name": "ast2500e", "current_image_version1": "0.01.00", "current_image_version2": "", "new_image_version": "0.03.00", "section_status": 0, "verification_status": 5 } ]`) fwUpgradeProgress = []byte(`{ "id": 1, "action": "Flashing...", "progress": "__PERCENT__% done ", "state": __STATE__ }`) usersPayload = []byte(`[ { "id": 1, "name": "anonymous", "access": 0, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 4802 }, { "id": 2, "name": "admin", "access": 1, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 188 }, { "id": 3, "name": "foo", "access": 1, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 4802 }, { "id": 4, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 5, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 6, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 7, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 8, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 9, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 10, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 } ]`) + inventoryinfoResponse = []byte(`[ { "device_id": 1, "device_name": "CPU1", "device_type": "CPU", "product_manufacturer_name": "Intel(R) Corporation", "product_name": "Intel(R) Xeon(R) E-2278G CPU @ 3.40GHz", "product_part_number": "N\/A", "product_version": "N\/A", "product_serial_number": "N\/A", "product_asset_tag": "N\/A", "product_extra": "N\/A" }, { "device_id": 5, "device_name": "DDR4_A1", "device_type": "Memory", "product_manufacturer_name": "Micron", "product_name": "SODIMM", "product_part_number": "18ASF2G72HZ-2G6E1 ", "product_version": "N\/A", "product_serial_number": "2724B52D", "product_asset_tag": "N\/A", "product_extra": "2666 MT\/s 16GB" }, { "device_id": 7, "device_name": "DDR4_B1", "device_type": "Memory", "product_manufacturer_name": "Micron", "product_name": "SODIMM", "product_part_number": "18ASF2G72HZ-2G6E1 ", "product_version": "N\/A", "product_serial_number": "2724B58A", "product_asset_tag": "N\/A", "product_extra": "2666 MT\/s 16GB" }, { "device_id": 37, "device_name": "PCIe card 1", "device_type": "PCIe & OCP Card", "product_manufacturer_name": "8086(Intel Corporation)", "product_name": "020000(Ethernet controller)", "product_part_number": "1572", "product_version": "N\/A", "product_serial_number": "N\/A", "product_asset_tag": "PCIE7", "product_extra": "N\/A" }, { "device_id": 105, "device_name": "Storage ", "device_type": "Storage device", "product_manufacturer_name": "N\/A", "product_name": "N\/A", "product_part_number": "INTEL SSDSC2KB480G8", "product_version": "N\/A", "product_serial_number": "PHYF001303ED480BGN", "product_asset_tag": "SATA_4", "product_extra": "N\/A" }, { "device_id": 106, "device_name": "Storage ", "device_type": "Storage device", "product_manufacturer_name": "N\/A", "product_name": "N\/A", "product_part_number": "INTEL SSDSC2KB480G8", "product_version": "N\/A", "product_serial_number": "BTYF01940L38480BGN", "product_asset_tag": "SATA_5", "product_extra": "N\/A" } ]`) + fruinfoResponse = []byte(`[ { "device": { "id": 0, "name": "BMC_FRU" }, "common_header": { "version": 1, "internal_use_area_start_offset": 0, "chassis_info_area_start_offset": 1, "board_info_area_start_offset": 4, "product_info_area_start_offset": 11, "multi_record_area_start_offset": 0 }, "chassis": { "version": 1, "length": 3, "type": "Main Server Chassis", "part_number": "", "serial_number": "K61206147700263", "custom_fields": "" }, "board": { "version": 1, "length": 7, "language": 0, "date": "Mon Jul 20 06:04:00 2020\\n", "manufacturer": "ASRockRack", "product_name": "E3C246D4I-NL", "serial_number": "197965920000514", "part_number": "", "fru_file_id": "", "custom_fields": "" }, "product": { "version": 1, "length": 7, "language": 0, "manufacturer": "Packet", "product_name": "c3.small.x86", "part_number": "Open19", "product_version": "R1.00", "serial_number": "D6S0R8000736", "asset_tag": "", "fru_file_id": "", "custom_fields": "" } } ]`) + biosPOSTCodeResponse = []byte(`{ "poststatus": 1, "postdata": 160 }`) + chassisStatusResponse = []byte(`{ "power_status": 1, "led_status": 0 }`) + // TODO: implement under rw mutex httpRequestTestVar *http.Request ) @@ -72,6 +77,11 @@ func mockASRockBMC() *httptest.Server { handler.HandleFunc("/", index) handler.HandleFunc("/api/session", session) handler.HandleFunc("/api/asrr/fw-info", fwinfo) + handler.HandleFunc("/api/fru", fruinfo) + handler.HandleFunc("/api/asrr/inventory_info", inventoryinfo) + handler.HandleFunc("/api/sensors", sensorsinfo) + handler.HandleFunc("/api/asrr/getbioscode", biosPOSTCodeinfo) + handler.HandleFunc("/api/chassis-status", chassisStatusInfo) // fw update endpoints - in order of invocation handler.HandleFunc("/api/maintenance/flash", bmcFirmwareUpgrade) @@ -238,6 +248,50 @@ func fwinfo(w http.ResponseWriter, r *http.Request) { } } +func fruinfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write(fruinfoResponse) + } +} + +func inventoryinfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write(inventoryinfoResponse) + } +} + +func sensorsinfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + fh, err := os.Open("./fixtures/E3C246D4I-NL/sensors.json") + if err != nil { + log.Fatal(err) + } + + b, err := ioutil.ReadAll(fh) + if err != nil { + log.Fatal(err) + } + _, _ = w.Write(b) + } +} + +func biosPOSTCodeinfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write(biosPOSTCodeResponse) + } +} + +func chassisStatusInfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write(chassisStatusResponse) + } +} + func session(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": diff --git a/providers/asrockrack/user.go b/providers/asrockrack/user.go index 7091bfec..76f92a0b 100644 --- a/providers/asrockrack/user.go +++ b/providers/asrockrack/user.go @@ -49,7 +49,7 @@ func (a *ASRockRack) UserRead(ctx context.Context) (users []map[string]string, e return nil, err } - accounts, err := a.listUsers() + accounts, err := a.listUsers(ctx) if err != nil { return nil, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } @@ -80,7 +80,7 @@ func (a *ASRockRack) UserCreate(ctx context.Context, user, pass, role string) (o } // fetch current list of accounts - accounts, err := a.listUsers() + accounts, err := a.listUsers(ctx) if err != nil { return false, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } @@ -99,7 +99,7 @@ func (a *ASRockRack) UserCreate(ctx context.Context, user, pass, role string) (o if account.Access == 0 && account.Name == "" { newAccount := newUserAccount(account.ID, user, pass, strings.ToLower(role)) - err := a.createUpdateUser(newAccount) + err := a.createUpdateUser(ctx, newAccount) if err != nil { return false, err } @@ -122,7 +122,7 @@ func (a *ASRockRack) UserUpdate(ctx context.Context, user, pass, role string) (o return false, bmclibErrs.ErrUserParamsRequired } - accounts, err := a.listUsers() + accounts, err := a.listUsers(ctx) if err != nil { return false, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } @@ -141,7 +141,7 @@ func (a *ASRockRack) UserUpdate(ctx context.Context, user, pass, role string) (o user.CreationTime = 6000 // doesn't mean anything. } - err := a.createUpdateUser(user) + err := a.createUpdateUser(ctx, user) if err != nil { return false, errors.Wrap(bmclibErrs.ErrUserAccountUpdate, err.Error()) } diff --git a/providers/asrockrack/user_test.go b/providers/asrockrack/user_test.go index 2003d836..1b16062d 100644 --- a/providers/asrockrack/user_test.go +++ b/providers/asrockrack/user_test.go @@ -60,7 +60,7 @@ func Test_UserRead(t *testing.T) { }, } - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } @@ -109,7 +109,7 @@ func Test_UserCreate(t *testing.T) { }..., ) - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Error(err) } @@ -144,7 +144,7 @@ func Test_UserUpdate(t *testing.T) { }..., ) - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Error(err) } @@ -157,7 +157,7 @@ func Test_UserUpdate(t *testing.T) { } func Test_createUser(t *testing.T) { - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } @@ -181,7 +181,7 @@ func Test_createUser(t *testing.T) { PasswordSize: "", } - err = aClient.createUpdateUser(account) + err = aClient.createUpdateUser(context.TODO(), account) if err != nil { t.Error(err) } @@ -200,7 +200,7 @@ func Test_createUser(t *testing.T) { } func Test_userAccounts(t *testing.T) { - err := aClient.httpsLogin() + err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf(err.Error()) } @@ -225,7 +225,7 @@ func Test_userAccounts(t *testing.T) { EmailFormat: "ami_format", } - accounts, err := aClient.listUsers() + accounts, err := aClient.listUsers(context.TODO()) if err != nil { t.Error(err) } From 3bddf1dfdfb6acf367a87f76a09a2b5d2f49fcea Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:38:11 +0200 Subject: [PATCH 08/25] providers/asrockrack: implement PowerStateGetter, PowerStateSetter interfaces --- providers/asrockrack/power.go | 104 ++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 providers/asrockrack/power.go diff --git a/providers/asrockrack/power.go b/providers/asrockrack/power.go new file mode 100644 index 00000000..7f64de5d --- /dev/null +++ b/providers/asrockrack/power.go @@ -0,0 +1,104 @@ +package asrockrack + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/pkg/errors" +) + +type power struct { + Command int `json:"power_command"` +} + +// PowerStateGet gets the power state of a machine +func (a *ASRockRack) PowerStateGet(ctx context.Context) (state string, err error) { + info, err := a.chassisStatusInfo(ctx) + if err != nil { + return "", errors.Wrap(bmclibErrs.ErrPowerStatusRead, err.Error()) + } + + switch info.PowerStatus { + case 0: + return "Off", nil + case 1: + return "On", nil + default: + return "", errors.Wrap( + bmclibErrs.ErrPowerStatusRead, + fmt.Errorf("unknown status: %d", info.PowerStatus).Error(), + ) + } +} + +// PowerSet sets the hardware power state of a machine +func (a *ASRockRack) PowerSet(ctx context.Context, state string) (ok bool, err error) { + switch strings.ToLower(state) { + case "on": + return a.powerAction(ctx, 1) + case "off": + return a.powerAction(ctx, 0) + case "soft": + return a.powerAction(ctx, 5) + case "reset": + return a.powerAction(ctx, 3) + case "cycle": + return a.powerAction(ctx, 2) + default: + return false, errors.New("requested power state unknown: " + state) + } +} + +func (a *ASRockRack) powerAction(ctx context.Context, action int) (ok bool, err error) { + endpoint := "/api/actions/power" + + p := power{Command: action} + payload, err := json.Marshal(p) + if err != nil { + return false, err + } + + headers := map[string]string{"Content-Type": "application/json"} + _, statusCode, err := a.queryHTTPS( + ctx, + endpoint, + "POST", + bytes.NewReader(payload), + headers, + 0, + ) + if err != nil { + return false, errors.Wrap(bmclibErrs.ErrPowerStatusSet, err.Error()) + } + + if statusCode != http.StatusOK { + return false, errors.Wrap( + bmclibErrs.ErrNon200Response, + fmt.Errorf("%d", statusCode).Error(), + ) + } + + return true, nil +} + +// 4. reset BMC +// nolint: unused // left here for reference +func (a *ASRockRack) resetBMC(ctx context.Context) error { + endpoint := "api/maintenance/reset" + + _, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", nil, nil, 0) + if err != nil { + return err + } + + if statusCode != http.StatusOK { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + return nil +} From df48b12d3e0da629b7c0a02c4d4241b411c83269 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:39:08 +0200 Subject: [PATCH 09/25] bmc/postcode: define interfaces to collect BIOS/UEFI POST codes --- bmc/postcode.go | 80 ++++++++++++++++++++++++++++++++ bmc/postcode_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 bmc/postcode.go create mode 100644 bmc/postcode_test.go diff --git a/bmc/postcode.go b/bmc/postcode.go new file mode 100644 index 00000000..43bfe37b --- /dev/null +++ b/bmc/postcode.go @@ -0,0 +1,80 @@ +package bmc + +import ( + "context" + "fmt" + + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" +) + +// PostCodeGetter defines methods to retrieve device BIOS/UEFI POST code +type PostCodeGetter interface { + // GetPostCode retrieves the BIOS/UEFI POST code from a device + // + // returns 'status' which is a (bmclib specific) string identifier for the POST code + // and 'code' with the actual POST code returned to bmclib by the device + GetPostCode(ctx context.Context) (status string, code int, err error) +} + +type postCodeGetterProvider struct { + name string + PostCodeGetter +} + +// GetPostCode returns the device BIOS/UEFI POST code +func GetPostCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { + var metadataLocal Metadata +Loop: + for _, elem := range generic { + if elem.PostCodeGetter == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + status, code, vErr := elem.GetPostCode(ctx) + if vErr != nil { + err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) + err = multierror.Append(err, vErr) + continue + + } + metadataLocal.SuccessfulProvider = elem.name + return status, code, metadataLocal, nil + } + } + + return status, code, metadataLocal, multierror.Append(err, errors.New("failure to get device POST code")) +} + +// GetPostCodeFromInterfaces is a pass through to library function +func GetPostCodeInterfaces(ctx context.Context, generic []interface{}) (status string, code int, metadata Metadata, err error) { + implementations := make([]postCodeGetterProvider, 0) + for _, elem := range generic { + temp := postCodeGetterProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case PostCodeGetter: + temp.PostCodeGetter = p + implementations = append(implementations, temp) + default: + e := fmt.Sprintf("not a PostCodeGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(implementations) == 0 { + return status, code, metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + ("no PostCodeGetter implementations found"), + ), + ) + } + + return GetPostCode(ctx, implementations) +} diff --git a/bmc/postcode_test.go b/bmc/postcode_test.go new file mode 100644 index 00000000..c286486a --- /dev/null +++ b/bmc/postcode_test.go @@ -0,0 +1,107 @@ +package bmc + +import ( + "context" + "testing" + "time" + + "github.com/bmc-toolbox/bmclib/devices" + bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/stretchr/testify/assert" +) + +type postCodeGetterTester struct { + returnStatus string + returnCode int + returnError error +} + +func (p *postCodeGetterTester) GetPostCode(ctx context.Context) (status string, code int, err error) { + return p.returnStatus, p.returnCode, p.returnError +} + +func (p *postCodeGetterTester) Name() string { + return "foo" +} + +func TestGetPostCode(t *testing.T) { + testCases := []struct { + testName string + returnStatus string + returnCode int + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + }{ + {"success with metadata", devices.POSTStateOS, 164, nil, 5 * time.Second, "foo", 1}, + {"failure with metadata", devices.POSTCodeUnknown, 0, bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, + {"failure with context timeout", "", 0, context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + testImplementation := postCodeGetterTester{returnStatus: tc.returnStatus, returnCode: tc.returnCode, returnError: tc.returnError} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + status, code, metadata, err := GetPostCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tc.returnStatus, status) + assert.Equal(t, tc.returnCode, code) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) + }) + } +} + +func TestGetPostCodeFromInterfaces(t *testing.T) { + testCases := []struct { + testName string + returnStatus string + returnCode int + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + badImplementation bool + }{ + {"success with metadata", devices.POSTStateOS, 164, nil, 5 * time.Second, "foo", 1, false}, + {"failure with bad implementation", "", 0, bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := &postCodeGetterTester{returnStatus: tc.returnStatus, returnCode: tc.returnCode, returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + status, code, metadata, err := GetPostCodeInterfaces(context.Background(), generic) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, tc.returnStatus, status) + assert.Equal(t, tc.returnCode, code) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + }) + } +} From d2657e47093e07754f7c709b30aa1c273aa3649e Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Thu, 7 Apr 2022 16:39:55 +0200 Subject: [PATCH 10/25] bmc/client: define methods for Inventory, Firmware, PostCode actions --- client.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index b07b2b28..af7d7c42 100644 --- a/client.go +++ b/client.go @@ -10,6 +10,7 @@ import ( "sync" "github.com/bmc-toolbox/bmclib/bmc" + "github.com/bmc-toolbox/bmclib/devices" "github.com/bmc-toolbox/bmclib/internal/httpclient" "github.com/bmc-toolbox/bmclib/providers/asrockrack" "github.com/bmc-toolbox/bmclib/providers/dell/idrac9" @@ -228,22 +229,31 @@ func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err e return ok, err } -// GetBMCVersion pass through library function -func (c *Client) GetBMCVersion(ctx context.Context) (version string, err error) { - return bmc.GetBMCVersionFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +// GetInventory pass through library function to collect hardware and firmware inventory +func (c *Client) GetInventory(ctx context.Context) (device *devices.Device, err error) { + device, metadata, err := bmc.GetInventoryFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) + c.setMetadata(metadata) + return device, err } -// UpdateBMCFirmware pass through library function -func (c *Client) UpdateBMCFirmware(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) { - return bmc.UpdateBMCFirmwareFromInterfaces(ctx, fileReader, fileSize, c.Registry.GetDriverInterfaces()) +// FirmwareInstall pass through library function to upload firmware and install firmware +func (c *Client) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (taskID string, err error) { + taskID, metadata, err := bmc.FirmwareInstallFromInterfaces(ctx, component, applyAt, forceInstall, reader, c.Registry.GetDriverInterfaces()) + c.setMetadata(metadata) + return taskID, err } -// GetBIOSVersion pass through library function -func (c *Client) GetBIOSVersion(ctx context.Context) (version string, err error) { - return bmc.GetBIOSVersionFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +// FirmwareInstallStatus pass through library function to check firmware install status +func (c *Client) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { + status, metadata, err := bmc.FirmwareInstallStatusFromInterfaces(ctx, component, installVersion, taskID, c.Registry.GetDriverInterfaces()) + c.setMetadata(metadata) + return status, err + } -// UpdateBIOSFirmware pass through library function -func (c *Client) UpdateBIOSFirmware(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) { - return bmc.UpdateBIOSFirmwareFromInterfaces(ctx, fileReader, fileSize, c.Registry.GetDriverInterfaces()) +// PostCodeGetter pass through library function to return the BIOS/UEFI POST code +func (c *Client) GetPostCode(ctx context.Context) (status string, code int, err error) { + status, code, metadata, err := bmc.GetPostCodeInterfaces(ctx, c.Registry.GetDriverInterfaces()) + c.setMetadata(metadata) + return status, code, err } From a73cdccc0be2fad4e74c06627fff83eeb5545b73 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 13 Apr 2022 07:07:04 +0200 Subject: [PATCH 11/25] rename getter methods --- bmc/inventory.go | 10 +++++----- bmc/inventory_test.go | 8 ++++---- bmc/postcode.go | 12 ++++++------ bmc/postcode_test.go | 8 ++++---- client.go | 6 +++--- examples/v1/inventory/inventory.go | 2 +- providers/asrockrack/asrockrack.go | 2 +- providers/asrockrack/inventory.go | 4 ++-- providers/asrockrack/inventory_test.go | 2 +- providers/redfish/inventory.go | 2 +- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/bmc/inventory.go b/bmc/inventory.go index d6f0d55f..c8391dd2 100644 --- a/bmc/inventory.go +++ b/bmc/inventory.go @@ -13,7 +13,7 @@ import ( // InventoryGetter defines methods to retrieve device hardware and firmware inventory type InventoryGetter interface { - GetInventory(ctx context.Context) (device *devices.Device, err error) + Inventory(ctx context.Context) (device *devices.Device, err error) } type inventoryGetterProvider struct { @@ -21,8 +21,8 @@ type inventoryGetterProvider struct { InventoryGetter } -// GetInventory returns hardware and firmware inventory -func GetInventory(ctx context.Context, generic []inventoryGetterProvider) (device *devices.Device, metadata Metadata, err error) { +// Inventory returns hardware and firmware inventory +func Inventory(ctx context.Context, generic []inventoryGetterProvider) (device *devices.Device, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -35,7 +35,7 @@ Loop: break Loop default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) - device, vErr := elem.GetInventory(ctx) + device, vErr := elem.Inventory(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) @@ -74,5 +74,5 @@ func GetInventoryFromInterfaces(ctx context.Context, generic []interface{}) (dev ) } - return GetInventory(ctx, implementations) + return Inventory(ctx, implementations) } diff --git a/bmc/inventory_test.go b/bmc/inventory_test.go index c8a43403..e0e18368 100644 --- a/bmc/inventory_test.go +++ b/bmc/inventory_test.go @@ -16,7 +16,7 @@ type inventoryGetterTester struct { returnError error } -func (f *inventoryGetterTester) GetInventory(ctx context.Context) (device *devices.Device, err error) { +func (f *inventoryGetterTester) Inventory(ctx context.Context) (device *devices.Device, err error) { return f.returnDevice, f.returnError } @@ -24,7 +24,7 @@ func (f *inventoryGetterTester) Name() string { return "foo" } -func TestGetInventory(t *testing.T) { +func TestInventory(t *testing.T) { testCases := []struct { testName string returnDevice *devices.Device @@ -46,7 +46,7 @@ func TestGetInventory(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - device, metadata, err := GetInventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) + device, metadata, err := Inventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return @@ -62,7 +62,7 @@ func TestGetInventory(t *testing.T) { } } -func TestGetInventoryFromInterfaces(t *testing.T) { +func TestInventoryFromInterfaces(t *testing.T) { testCases := []struct { testName string returnDevice *devices.Device diff --git a/bmc/postcode.go b/bmc/postcode.go index 43bfe37b..fd266bed 100644 --- a/bmc/postcode.go +++ b/bmc/postcode.go @@ -11,11 +11,11 @@ import ( // PostCodeGetter defines methods to retrieve device BIOS/UEFI POST code type PostCodeGetter interface { - // GetPostCode retrieves the BIOS/UEFI POST code from a device + // PostCode retrieves the BIOS/UEFI POST code from a device // // returns 'status' which is a (bmclib specific) string identifier for the POST code // and 'code' with the actual POST code returned to bmclib by the device - GetPostCode(ctx context.Context) (status string, code int, err error) + PostCode(ctx context.Context) (status string, code int, err error) } type postCodeGetterProvider struct { @@ -23,8 +23,8 @@ type postCodeGetterProvider struct { PostCodeGetter } -// GetPostCode returns the device BIOS/UEFI POST code -func GetPostCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { +// PostCode returns the device BIOS/UEFI POST code +func PostCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -37,7 +37,7 @@ Loop: break Loop default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) - status, code, vErr := elem.GetPostCode(ctx) + status, code, vErr := elem.PostCode(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) @@ -76,5 +76,5 @@ func GetPostCodeInterfaces(ctx context.Context, generic []interface{}) (status s ) } - return GetPostCode(ctx, implementations) + return PostCode(ctx, implementations) } diff --git a/bmc/postcode_test.go b/bmc/postcode_test.go index c286486a..257f695d 100644 --- a/bmc/postcode_test.go +++ b/bmc/postcode_test.go @@ -16,7 +16,7 @@ type postCodeGetterTester struct { returnError error } -func (p *postCodeGetterTester) GetPostCode(ctx context.Context) (status string, code int, err error) { +func (p *postCodeGetterTester) PostCode(ctx context.Context) (status string, code int, err error) { return p.returnStatus, p.returnCode, p.returnError } @@ -24,7 +24,7 @@ func (p *postCodeGetterTester) Name() string { return "foo" } -func TestGetPostCode(t *testing.T) { +func TestPostCode(t *testing.T) { testCases := []struct { testName string returnStatus string @@ -47,7 +47,7 @@ func TestGetPostCode(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - status, code, metadata, err := GetPostCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) + status, code, metadata, err := PostCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return @@ -64,7 +64,7 @@ func TestGetPostCode(t *testing.T) { } } -func TestGetPostCodeFromInterfaces(t *testing.T) { +func TestPostCodeFromInterfaces(t *testing.T) { testCases := []struct { testName string returnStatus string diff --git a/client.go b/client.go index af7d7c42..ba3103d4 100644 --- a/client.go +++ b/client.go @@ -229,8 +229,8 @@ func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err e return ok, err } -// GetInventory pass through library function to collect hardware and firmware inventory -func (c *Client) GetInventory(ctx context.Context) (device *devices.Device, err error) { +// Inventory pass through library function to collect hardware and firmware inventory +func (c *Client) Inventory(ctx context.Context) (device *devices.Device, err error) { device, metadata, err := bmc.GetInventoryFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) c.setMetadata(metadata) return device, err @@ -252,7 +252,7 @@ func (c *Client) FirmwareInstallStatus(ctx context.Context, component, installVe } // PostCodeGetter pass through library function to return the BIOS/UEFI POST code -func (c *Client) GetPostCode(ctx context.Context) (status string, code int, err error) { +func (c *Client) PostCode(ctx context.Context) (status string, code int, err error) { status, code, metadata, err := bmc.GetPostCodeInterfaces(ctx, c.Registry.GetDriverInterfaces()) c.setMetadata(metadata) return status, code, err diff --git a/examples/v1/inventory/inventory.go b/examples/v1/inventory/inventory.go index 137b414d..12298e1f 100644 --- a/examples/v1/inventory/inventory.go +++ b/examples/v1/inventory/inventory.go @@ -39,7 +39,7 @@ func main() { defer cl.Close(ctx) - inv, err := cl.GetInventory(ctx) + inv, err := cl.Inventory(ctx) if err != nil { l.Error(err) } diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go index c3ca04cf..431a5769 100644 --- a/providers/asrockrack/asrockrack.go +++ b/providers/asrockrack/asrockrack.go @@ -127,7 +127,7 @@ func (a *ASRockRack) CheckCredentials(ctx context.Context) (err error) { return a.httpsLogin(ctx) } -func (a *ASRockRack) GetPostCode(ctx context.Context) (status string, code int, err error) { +func (a *ASRockRack) PostCode(ctx context.Context) (status string, code int, err error) { postInfo, err := a.postCodeInfo(ctx) if err != nil { return status, code, err diff --git a/providers/asrockrack/inventory.go b/providers/asrockrack/inventory.go index fd21fda7..5036014f 100644 --- a/providers/asrockrack/inventory.go +++ b/providers/asrockrack/inventory.go @@ -6,7 +6,7 @@ import ( "github.com/bmc-toolbox/bmclib/devices" ) -func (a *ASRockRack) GetInventory(ctx context.Context) (device *devices.Device, err error) { +func (a *ASRockRack) Inventory(ctx context.Context) (device *devices.Device, err error) { // initialize device to be populated with inventory device = devices.NewDevice() device.Metadata = map[string]string{} @@ -63,7 +63,7 @@ func (a *ASRockRack) systemHealth(ctx context.Context, device *devices.Device) e } // we don't want to fail inventory collection hence ignore POST code collection error - device.Status.PostCodeStatus, device.Status.PostCode, _ = a.GetPostCode(ctx) + device.Status.PostCodeStatus, device.Status.PostCode, _ = a.PostCode(ctx) return nil } diff --git a/providers/asrockrack/inventory_test.go b/providers/asrockrack/inventory_test.go index 50de7913..5e7302cb 100644 --- a/providers/asrockrack/inventory_test.go +++ b/providers/asrockrack/inventory_test.go @@ -8,7 +8,7 @@ import ( ) func Test_GetInventory(t *testing.T) { - device, err := aClient.GetInventory(context.TODO()) + device, err := aClient.Inventory(context.TODO()) if err != nil { t.Fatal(err) } diff --git a/providers/redfish/inventory.go b/providers/redfish/inventory.go index a1f1fa4a..d8e91bfa 100644 --- a/providers/redfish/inventory.go +++ b/providers/redfish/inventory.go @@ -62,7 +62,7 @@ func (c *Conn) DeviceVendorModel(ctx context.Context) (vendor, model string, err return vendor, model, bmclibErrs.ErrRedfishSystemOdataID } -func (c *Conn) GetInventory(ctx context.Context) (device *devices.Device, err error) { +func (c *Conn) Inventory(ctx context.Context) (device *devices.Device, err error) { // initialize inventory object inv := &inventory{conn: c.conn} // TODO: this can soft fail From 34c2169615bcd357bdf2a835df9a1bd5aebabb7a Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 07:18:50 +0200 Subject: [PATCH 12/25] Implement changes based on feedback from GH PR#261 --- bmc/firmware.go | 51 +++++++++---------- bmc/firmware_test.go | 4 +- bmc/inventory.go | 6 +-- bmc/inventory_test.go | 2 +- bmc/postcode.go | 6 +-- bmc/postcode_test.go | 2 +- devices/constants.go | 8 +++ providers/asrockrack/firmware.go | 14 +----- providers/asrockrack/helpers.go | 40 ++++----------- providers/redfish/firmware.go | 39 +++++++++------ providers/redfish/firmware_test.go | 79 +++++++++++++++++++++++++----- providers/redfish/inventory.go | 6 +-- providers/redfish/redfish_test.go | 5 +- 13 files changed, 152 insertions(+), 110 deletions(-) diff --git a/bmc/firmware.go b/bmc/firmware.go index d36ed6cb..82fecd85 100644 --- a/bmc/firmware.go +++ b/bmc/firmware.go @@ -7,15 +7,23 @@ import ( bmclibErrs "github.com/bmc-toolbox/bmclib/errors" - "github.com/pkg/errors" - "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" ) // FirmwareInstaller defines an interface to install firmware updates type FirmwareInstaller interface { // FirmwareInstall uploads firmware update payload to the BMC returning the task ID - FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) + // + // parameters: + // component - the component slug for the component update being installed. + // applyAt - one of "Immediate", "OnReset". + // forceInstall - purge the install task queued/scheduled firmware install BMC task (if any). + // reader - the io.reader to the firmware update file. + // + // return values: + // taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process. + FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (taskID string, err error) } // firmwareInstallerProvider is an internal struct to correlate an implementation/provider and its name @@ -24,17 +32,8 @@ type firmwareInstallerProvider struct { FirmwareInstaller } -// FirmwareInstall uploads and initiates firmware update for the component -// -// parameters: -// component - the component slug for the component update being installed -// applyAt - one of "Immediate", "OnReset" -// forceInstall - purge the install task queued/scheduled firmware install BMC task (if any) -// reader - the io.reader to the firmware update file -// -// return values: -// taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process -func FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader, generic []firmwareInstallerProvider) (taskID string, metadata Metadata, err error) { +// firmwareInstall uploads and initiates firmware update for the component +func firmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader, generic []firmwareInstallerProvider) (taskID string, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -86,11 +85,20 @@ func FirmwareInstallFromInterfaces(ctx context.Context, component, applyAt strin ) } - return FirmwareInstall(ctx, component, applyAt, forceInstall, reader, implementations) + return firmwareInstall(ctx, component, applyAt, forceInstall, reader, implementations) } // FirmwareInstallVerifier defines an interface to check firmware install status type FirmwareInstallVerifier interface { + // FirmwareInstallStatus returns the status of the firmware install process. + // + // parameters: + // component (optional) - the component slug for the component update being installed. + // installVersion (required) - the version this method should check is installed. + // taskID (optional) - the task identifier. + // + // return values: + // status - returns one of the FirmwareInstall statuses (see devices/constants.go). FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) } @@ -100,15 +108,8 @@ type firmwareInstallVerifierProvider struct { FirmwareInstallVerifier } -// FirmwareInstallStatus returns the status of the firmware install process -// -// parameters: -// component (optional) - the component slug for the component update being installed -// taskID (optional) - the task identifier -// -// return values: -// status - returns one of the FirmwareInstall statuses (see devices/constants.go) -func FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { +// firmwareInstallStatus returns the status of the firmware install process +func firmwareInstallStatus(ctx context.Context, component, installVersion, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -160,5 +161,5 @@ func FirmwareInstallStatusFromInterfaces(ctx context.Context, component, install ) } - return FirmwareInstallStatus(ctx, component, installVersion, taskID, implementations) + return firmwareInstallStatus(ctx, component, installVersion, taskID, implementations) } diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go index 9325271b..894886c2 100644 --- a/bmc/firmware_test.go +++ b/bmc/firmware_test.go @@ -51,7 +51,7 @@ func TestFirmwareInstall(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - taskID, metadata, err := FirmwareInstall(ctx, tc.component, tc.applyAt, tc.forceInstall, tc.reader, []firmwareInstallerProvider{{tc.providerName, &testImplementation}}) + taskID, metadata, err := firmwareInstall(ctx, tc.component, tc.applyAt, tc.forceInstall, tc.reader, []firmwareInstallerProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return @@ -146,7 +146,7 @@ func TestFirmwareInstallStatus(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - taskID, metadata, err := FirmwareInstallStatus(ctx, tc.component, tc.installVersion, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) + taskID, metadata, err := firmwareInstallStatus(ctx, tc.component, tc.installVersion, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return diff --git a/bmc/inventory.go b/bmc/inventory.go index c8391dd2..b93dd66f 100644 --- a/bmc/inventory.go +++ b/bmc/inventory.go @@ -21,8 +21,8 @@ type inventoryGetterProvider struct { InventoryGetter } -// Inventory returns hardware and firmware inventory -func Inventory(ctx context.Context, generic []inventoryGetterProvider) (device *devices.Device, metadata Metadata, err error) { +// inventory returns hardware and firmware inventory +func inventory(ctx context.Context, generic []inventoryGetterProvider) (device *devices.Device, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -74,5 +74,5 @@ func GetInventoryFromInterfaces(ctx context.Context, generic []interface{}) (dev ) } - return Inventory(ctx, implementations) + return inventory(ctx, implementations) } diff --git a/bmc/inventory_test.go b/bmc/inventory_test.go index e0e18368..7e5f705f 100644 --- a/bmc/inventory_test.go +++ b/bmc/inventory_test.go @@ -46,7 +46,7 @@ func TestInventory(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - device, metadata, err := Inventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) + device, metadata, err := inventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return diff --git a/bmc/postcode.go b/bmc/postcode.go index fd266bed..cd7c50b4 100644 --- a/bmc/postcode.go +++ b/bmc/postcode.go @@ -23,8 +23,8 @@ type postCodeGetterProvider struct { PostCodeGetter } -// PostCode returns the device BIOS/UEFI POST code -func PostCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { +// postCode returns the device BIOS/UEFI POST code +func postCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -76,5 +76,5 @@ func GetPostCodeInterfaces(ctx context.Context, generic []interface{}) (status s ) } - return PostCode(ctx, implementations) + return postCode(ctx, implementations) } diff --git a/bmc/postcode_test.go b/bmc/postcode_test.go index 257f695d..63b1f48d 100644 --- a/bmc/postcode_test.go +++ b/bmc/postcode_test.go @@ -47,7 +47,7 @@ func TestPostCode(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - status, code, metadata, err := PostCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) + status, code, metadata, err := postCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return diff --git a/devices/constants.go b/devices/constants.go index 4bb7bbbe..c5820a75 100644 --- a/devices/constants.go +++ b/devices/constants.go @@ -126,6 +126,14 @@ func VendorFromProductName(productName string) string { switch { case strings.Contains(n, "intel"): return Intel + case strings.Contains(n, "dell"): + return Dell + case strings.Contains(n, "supermicro"): + return Supermicro + case strings.Contains(n, "cloudline"): + return Cloudline + case strings.Contains(n, "quanta"): + return Quanta default: return productName } diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go index c5fd9f02..0ac74860 100644 --- a/providers/asrockrack/firmware.go +++ b/providers/asrockrack/firmware.go @@ -14,8 +14,6 @@ import ( bmclibErrs "github.com/bmc-toolbox/bmclib/errors" ) - - // FirmwareInstall uploads and initiates firmware update for the component func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { var size int64 @@ -118,10 +116,7 @@ func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, // firmwareUpdateBMCStatus returns the BMC firmware install status func (a *ASRockRack) firmwareUpdateBMCStatus(ctx context.Context, installVersion string) (status string, err error) { - os.Setenv("BMCLIB_LOG_LEVEL", "trace") - defer os.Unsetenv("BMCLIB_LOG_LEVEL") - endpoint := "api/maintenance/firmware/flash-progress" - p, progressErr := a.flashProgress(ctx, endpoint) + p, progressErr := a.flashProgress(ctx, "api/maintenance/firmware/flash-progress") if progressErr != nil { installed, versionErr := a.versionInstalled(ctx, devices.SlugBMC, installVersion) if err != nil { @@ -160,11 +155,7 @@ func (a *ASRockRack) firmwareUpdateBMCStatus(ctx context.Context, installVersion // firmwareUpdateBIOSStatus returns the BIOS firmware install status func (a *ASRockRack) firmwareUpdateBIOSStatus(ctx context.Context, installVersion string) (status string, err error) { - os.Setenv("BMCLIB_LOG_LEVEL", "trace") - defer os.Unsetenv("BMCLIB_LOG_LEVEL") - - endpoint := "api/asrr/maintenance/BIOS/flash-progress" - p, progressErr := a.flashProgress(ctx, endpoint) + p, progressErr := a.flashProgress(ctx, "api/asrr/maintenance/BIOS/flash-progress") if progressErr != nil { installed, versionErr := a.versionInstalled(ctx, devices.SlugBIOS, installVersion) if versionErr != nil { @@ -223,7 +214,6 @@ func (a *ASRockRack) versionInstalled(ctx context.Context, component, version st } if strings.EqualFold(installed, version) { - fmt.Println("YEP!") return true, nil } diff --git a/providers/asrockrack/helpers.go b/providers/asrockrack/helpers.go index 8ff75659..a450ef1b 100644 --- a/providers/asrockrack/helpers.go +++ b/providers/asrockrack/helpers.go @@ -138,9 +138,7 @@ var ( ) func (a *ASRockRack) listUsers(ctx context.Context) ([]*UserAccount, error) { - endpoint := "api/settings/users" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "api/settings/users", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -184,9 +182,7 @@ func (a *ASRockRack) createUpdateUser(ctx context.Context, account *UserAccount) // at this point all logged in sessions are terminated // and no logins are permitted func (a *ASRockRack) setFlashMode(ctx context.Context) error { - endpoint := "api/maintenance/flash" - - _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, "api/maintenance/flash", "PUT", nil, nil, 0) if err != nil { return err } @@ -262,9 +258,7 @@ func (a *ASRockRack) uploadFirmware(ctx context.Context, endpoint string, fwRead // 3. Verify uploaded firmware file - to be invoked after uploadFirmware() func (a *ASRockRack) verifyUploadedFirmware(ctx context.Context) error { - endpoint := "api/maintenance/firmware/verification" - - _, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, "api/maintenance/firmware/verification", "GET", nil, nil, 0) if err != nil { return err } @@ -322,9 +316,7 @@ func (a *ASRockRack) flashProgress(ctx context.Context, endpoint string) (*upgra // Query firmware information from the BMC func (a *ASRockRack) firmwareInfo(ctx context.Context) (*firmwareInfo, error) { - endpoint := "api/asrr/fw-info" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "api/asrr/fw-info", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -344,9 +336,7 @@ func (a *ASRockRack) firmwareInfo(ctx context.Context) (*firmwareInfo, error) { // Query BIOS/UEFI POST code information from the BMC func (a *ASRockRack) postCodeInfo(ctx context.Context) (*biosPOSTCode, error) { - endpoint := "/api/asrr/getbioscode" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "/api/asrr/getbioscode", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -366,9 +356,7 @@ func (a *ASRockRack) postCodeInfo(ctx context.Context) (*biosPOSTCode, error) { // Query the inventory info endpoint func (a *ASRockRack) inventoryInfo(ctx context.Context) ([]*component, error) { - endpoint := "api/asrr/inventory_info" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "api/asrr/inventory_info", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -388,9 +376,7 @@ func (a *ASRockRack) inventoryInfo(ctx context.Context) ([]*component, error) { // Query the fru info endpoint func (a *ASRockRack) fruInfo(ctx context.Context) ([]*fru, error) { - endpoint := "api/fru" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "api/fru", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -436,9 +422,7 @@ func (a *ASRockRack) fruInfo(ctx context.Context) ([]*fru, error) { // Query the sensors endpoint func (a *ASRockRack) sensors(ctx context.Context) ([]*sensor, error) { - endpoint := "api/sensors" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "api/sensors", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -519,9 +503,7 @@ func (a *ASRockRack) upgradeBIOS(ctx context.Context) error { // Returns the chassis status object which includes the power state func (a *ASRockRack) chassisStatusInfo(ctx context.Context) (*chassisStatus, error) { - endpoint := "/api/chassis-status" - - resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) + resp, statusCode, err := a.queryHTTPS(ctx, "/api/chassis-status", "GET", nil, nil, 0) if err != nil { return nil, err } @@ -573,9 +555,7 @@ func (a *ASRockRack) httpsLogin(ctx context.Context) error { // Close ends the BMC session func (a *ASRockRack) httpsLogout(ctx context.Context) error { - urlEndpoint := "api/session" - - _, statusCode, err := a.queryHTTPS(ctx, urlEndpoint, "DELETE", nil, nil, 0) + _, statusCode, err := a.queryHTTPS(ctx, "api/session", "DELETE", nil, nil, 0) if err != nil { return fmt.Errorf("Error logging out: " + err.Error()) } diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index dced2b6a..e1098040 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -3,6 +3,7 @@ package redfish import ( "bytes" "context" + "encoding/json" "fmt" "io" "mime/multipart" @@ -20,16 +21,16 @@ import ( bmclibErrs "github.com/bmc-toolbox/bmclib/errors" ) -var ( - FirmwareApplyAt = []string{ +// SupportedFirmwareApplyAtValues returns the supported redfish firmware applyAt values +func SupportedFirmwareApplyAtValues() []string { + return []string{ devices.FirmwareApplyImmediate, devices.FirmwareApplyOnReset, } - FirmwareDownloadProtocols = []string{"HTTP", "NFS", "CIFS", "TFTP", "HTTPS"} -) +} // FirmwareInstall uploads and initiates the firmware install process -func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { +func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (taskID string, err error) { // validate firmware update mechanism is supported err = c.firmwareUpdateCompatible(ctx) if err != nil { @@ -37,8 +38,8 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f } // validate applyAt parameter - if !stringInSlice(applyAt, FirmwareApplyAt) { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "invalid applyAt parameter: %s"+applyAt) + if !stringInSlice(applyAt, SupportedFirmwareApplyAtValues()) { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "invalid applyAt parameter: "+applyAt) } // list redfish firmware install task if theres one present @@ -61,9 +62,19 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f } } - updateParameters := []byte( - fmt.Sprintf(`{"Targets": [], "@Redfish.OperationApplyTime": "%s", "Oem": {}}`, applyAt), - ) + updateParameters, err := json.Marshal(struct { + Targets []string `json:"Targets"` + RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` + Oem struct{} `json:"Oem"` + }{ + []string{}, + applyAt, + struct{}{}, + }) + + if err != nil { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) + } payload := map[string]io.Reader{ "UpdateParameters": bytes.NewReader(updateParameters), @@ -72,11 +83,11 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) if err != nil { - return jobID, errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) + return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) } if resp.StatusCode != http.StatusAccepted { - return jobID, errors.Wrap( + return "", errors.Wrap( bmclibErrs.ErrFirmwareUpload, "non 202 status code returned: "+strconv.Itoa(resp.StatusCode), ) @@ -85,10 +96,10 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f // The response contains a location header pointing to the task URI // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 if strings.Contains(resp.Header.Get("Location"), "JID_") { - jobID = strings.Split(resp.Header.Get("Location"), "JID_")[1] + taskID = strings.Split(resp.Header.Get("Location"), "JID_")[1] } - return jobID, nil + return taskID, nil } // FirmwareInstallStatus returns the status of the firmware install task queued diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 2074913a..6b35244c 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -2,10 +2,13 @@ package redfish import ( "context" + "fmt" + "io" "io/ioutil" "log" "net/http" "os" + "path/filepath" "strings" "testing" @@ -29,7 +32,7 @@ func multipartUpload(w http.ResponseWriter, r *http.Request) { expected := []string{ `Content-Disposition: form-data; name="UpdateParameters"`, `Content-Type: application/json`, - `{"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}}`, + `{"Targets":[],"@Redfish.OperationApplyTime":"OnReset","Oem":{}}`, `Content-Disposition: form-data; name="UpdateFile"; filename="test.bin"`, `Content-Type: application/octet-stream`, `HELLOWORLD`, @@ -37,6 +40,7 @@ func multipartUpload(w http.ResponseWriter, r *http.Request) { for _, want := range expected { if !strings.Contains(string(body), want) { + fmt.Println(string(body)) log.Fatal("expected value not in multipartUpload payload: " + string(want)) } } @@ -45,30 +49,83 @@ func multipartUpload(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) } -func Test_FirmwareUpload(t *testing.T) { +func Test_FirmwareInstall(t *testing.T) { // curl -Lv -s -k -u root:calvin \ // -F 'UpdateParameters={"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}};type=application/json' \ // -F'foo.bin=@/tmp/dummyfile;application/octet-stream' // https://192.168.1.1/redfish/v1/UpdateService/MultipartUpload --trace-ascii /dev/stdout - err := ioutil.WriteFile("/tmp/test.bin", []byte(`HELLOWORLD`), 0600) + + tmpdir := t.TempDir() + binPath := filepath.Join(tmpdir, "test.bin") + err := os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) if err != nil { t.Fatal(err) } - fh, err := os.Open("/tmp/test.bin") + fh, err := os.Open(binPath) if err != nil { - t.Fatalf("%s -> %s", err.Error(), "/tmp/test.bin") + t.Fatalf("%s -> %s", err.Error(), binPath) } - _, err = mockClient.FirmwareInstall(context.TODO(), "", "invalid", false, fh) - assert.ErrorIs(t, err, bmclibErrs.ErrFirmwareInstall) + defer os.Remove(binPath) - jobID, err := mockClient.FirmwareInstall(context.TODO(), "", devices.FirmwareApplyOnReset, false, fh) - if err != nil { - t.Fatal("err in FirmwareUpload" + err.Error()) + tests := []struct { + component string + applyAt string + forceInstall bool + reader io.Reader + expectTaskID string + expectErr error + expectErrSubStr string + testName string + }{ + { + devices.SlugBIOS, + "invalidApplyAt", + false, + nil, + "", + bmclibErrs.ErrFirmwareInstall, + "invalid applyAt parameter", + "applyAt parameter invalid", + }, + { + devices.SlugBIOS, + devices.FirmwareApplyOnReset, + false, + fh, + "467696020275", + bmclibErrs.ErrFirmwareInstall, + "task for BIOS firmware install present", + "task ID exists", + }, + { + devices.SlugBIOS, + devices.FirmwareApplyOnReset, + true, + fh, + "467696020275", + nil, + "task for BIOS firmware install present", + "task created (previous task purged with force)", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + taskID, err := mockClient.FirmwareInstall(context.TODO(), tc.component, tc.applyAt, tc.forceInstall, tc.reader) + if tc.expectErr != nil { + assert.ErrorIs(t, err, tc.expectErr) + if tc.expectErrSubStr != "" { + assert.True(t, strings.Contains(err.Error(), tc.expectErrSubStr)) + } + } else { + assert.Nil(t, err) + assert.Equal(t, tc.expectTaskID, taskID) + } + }) } - assert.Equal(t, "467696020275", jobID) } func Test_firmwareUpdateCompatible(t *testing.T) { diff --git a/providers/redfish/inventory.go b/providers/redfish/inventory.go index d8e91bfa..6e3593d8 100644 --- a/providers/redfish/inventory.go +++ b/providers/redfish/inventory.go @@ -62,7 +62,7 @@ func (c *Conn) DeviceVendorModel(ctx context.Context) (vendor, model string, err return vendor, model, bmclibErrs.ErrRedfishSystemOdataID } -func (c *Conn) Inventory(ctx context.Context) (device *devices.Device, err error) { +func (c *Conn) GetInventory(ctx context.Context) (device *devices.Device, err error) { // initialize inventory object inv := &inventory{conn: c.conn} // TODO: this can soft fail @@ -408,7 +408,6 @@ func (i *inventory) collectBIOS(sys *redfish.ComputerSystem, device *devices.Dev return err } - //bios.Attributes.String("SystemCpldVersion") device.BIOS = &devices.BIOS{ Description: bios.Description, Firmware: &devices.Firmware{ @@ -492,9 +491,6 @@ func (i *inventory) collectStorageControllers(sys *redfish.ComputerSystem, devic Model: controller.PartNumber, Serial: controller.SerialNumber, SpeedGbps: int64(controller.SpeedGbps), - // SupportedControllerProtocols: join(controller.SupportedControllerProtocols), - // SupportedDeviceProtocols: join(controller.SupportedDeviceProtocols), - // SupportedRAIDTypes: join(controller.SupportedRAIDTypes), Status: &devices.Status{ Health: string(controller.Status.Health), State: string(controller.Status.State), diff --git a/providers/redfish/redfish_test.go b/providers/redfish/redfish_test.go index bcddd429..e7d9c81f 100644 --- a/providers/redfish/redfish_test.go +++ b/providers/redfish/redfish_test.go @@ -10,7 +10,7 @@ import ( "os" "testing" - "github.com/sirupsen/logrus" + "github.com/go-logr/logr" ) const ( @@ -70,8 +70,7 @@ func TestMain(m *testing.M) { log.Fatal(err) } - l := logrus.New() - l.Level = logrus.DebugLevel + mockClient.Log = logr.Discard() os.Exit(m.Run()) } From 3673df4f8b022355b5d52b0f229a9b9d78d3e119 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 11:14:48 +0200 Subject: [PATCH 13/25] providers/asrockrack: empty version string from BMC returned as state FirmwareInstallUnknown this is so that the caller is aware that the BMC is not aware of the component version and can decide if it wants to power cycle or take other actions. instead of assuming the device needs a powercycle --- providers/asrockrack/firmware.go | 132 +++++++++++++++---------------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go index 0ac74860..374d3c29 100644 --- a/providers/asrockrack/firmware.go +++ b/providers/asrockrack/firmware.go @@ -14,6 +14,13 @@ import ( bmclibErrs "github.com/bmc-toolbox/bmclib/errors" ) +const ( + versionStrError = -1 + versionStrMatch = 0 + versionStrMismatch = 1 + versionStrEmpty = 2 +) + // FirmwareInstall uploads and initiates firmware update for the component func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { var size int64 @@ -42,12 +49,10 @@ func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt str // FirmwareInstallStatus returns the status of the firmware install process, a bool value indicating if the component requires a reset func (a *ASRockRack) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { switch component { - case devices.SlugBIOS: - return a.firmwareUpdateBIOSStatus(ctx, installVersion) - case devices.SlugBMC: - return a.firmwareUpdateBMCStatus(ctx, installVersion) + case devices.SlugBIOS, devices.SlugBMC: + return a.firmwareUpdateStatus(ctx, component, installVersion) default: - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "component unsupported: "+component) } } @@ -114,92 +119,74 @@ func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, return nil } -// firmwareUpdateBMCStatus returns the BMC firmware install status -func (a *ASRockRack) firmwareUpdateBMCStatus(ctx context.Context, installVersion string) (status string, err error) { - p, progressErr := a.flashProgress(ctx, "api/maintenance/firmware/flash-progress") - if progressErr != nil { - installed, versionErr := a.versionInstalled(ctx, devices.SlugBMC, installVersion) - if err != nil { - return "", versionErr - } - - if installed { - return devices.FirmwareInstallComplete, nil - } - - return "", progressErr - } +// firmwareUpdateBIOSStatus returns the BIOS firmware install status +func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, installVersion string) (status string, err error) { + // TODO: purge debug logging + os.Setenv("BMCLIB_LOG_LEVEL", "trace") + defer os.Unsetenv("BMCLIB_LOG_LEVEL") - switch p.State { - case 0: - return devices.FirmwareInstallRunning, nil - case 2: - return devices.FirmwareInstallComplete, nil + var endpoint string + switch component { + case devices.SlugBIOS: + endpoint = "api/asrr/maintenance/BIOS/flash-progress" + case devices.SlugBMC: + endpoint = "api/maintenance/firmware/flash-progress" default: - a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(p.State)) + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "component unsupported: "+component) } - // at this point the flash-progress endpoint isn't returning useful information - // query the fimware info endpoint to identify the installed version - installed, err := a.versionInstalled(ctx, devices.SlugBMC, installVersion) + // 1. query the flash progress endpoint + // + // once an update completes/fails this endpoint will return 500 + progress, err := a.flashProgress(ctx, endpoint) if err != nil { - return "", err + a.log.V(3).Info("warn", "bmc query for install progress returned error: "+err.Error()) } - if installed { - return devices.FirmwareInstallComplete, nil - } - - return devices.FirmwareInstallUnknown, nil -} - -// firmwareUpdateBIOSStatus returns the BIOS firmware install status -func (a *ASRockRack) firmwareUpdateBIOSStatus(ctx context.Context, installVersion string) (status string, err error) { - p, progressErr := a.flashProgress(ctx, "api/asrr/maintenance/BIOS/flash-progress") - if progressErr != nil { - installed, versionErr := a.versionInstalled(ctx, devices.SlugBIOS, installVersion) - if versionErr != nil { - return "", versionErr - } - - if installed { + if progress != nil { + switch progress.State { + case 0: + return devices.FirmwareInstallRunning, nil + case 2: return devices.FirmwareInstallComplete, nil + default: + a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(progress.State)) } - - return "", progressErr - } - - switch p.State { - // Note: we're ignoring case 1 here, should it just be part of case 0 - case 0: - return devices.FirmwareInstallRunning, nil - case 2: - return devices.FirmwareInstallComplete, nil - default: - a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(p.State)) } + // 2. query the firmware info endpoint to determine the update status + // // at this point the flash-progress endpoint isn't returning useful information - // query the fimware info endpoint to identify the installed version - installed, err := a.versionInstalled(ctx, devices.SlugBIOS, installVersion) + var installStatus int + + installStatus, err = a.versionInstalled(ctx, component, installVersion) if err != nil { - return "", err + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } - if installed { + switch installStatus { + case versionStrMatch: return devices.FirmwareInstallComplete, nil + case versionStrEmpty: + return devices.FirmwareInstallUnknown, nil + case versionStrMismatch: + return devices.FirmwareInstallRunning, nil } return devices.FirmwareInstallUnknown, nil } -// versionInstalled returns a -func (a *ASRockRack) versionInstalled(ctx context.Context, component, version string) (bool, error) { +// versionInstalled returns int values on the status of the firmware version install +// +// - 0 indicates the given version parameter matches the version installed +// - 1 indicates the given version parameter does not match the version installed +// - 2 the version parameter returned from the BMC is empty (which means the BMC needs a reset) +func (a *ASRockRack) versionInstalled(ctx context.Context, component, version string) (status int, err error) { fwInfo, err := a.firmwareInfo(ctx) if err != nil { err = errors.Wrap(err, "error querying for firmware info: ") a.log.V(3).Info("warn", err.Error()) - return false, err + return versionStrError, err } var installed string @@ -210,12 +197,19 @@ func (a *ASRockRack) versionInstalled(ctx context.Context, component, version st case devices.SlugBMC: installed = fwInfo.BMCVersion default: - return false, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + return versionStrError, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) } + // version match if strings.EqualFold(installed, version) { - return true, nil + return versionStrMatch, nil + } + + // fwinfo returned an empty string for firmware revision + // this indicates the BMC is out of sync with the firmware versions installed + if strings.TrimSpace(installed) == "" { + return versionStrEmpty, nil } - return false, nil + return 1, nil } From 84306c7cd7e11a7b9aa2cd0a3d9379d01afe283a Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 11:21:48 +0200 Subject: [PATCH 14/25] providers/asrockrack: include various other fields in the BMC update payload this is an attempt to have the BMC preserve the User, Network BMC configuration after a flash --- providers/asrockrack/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/asrockrack/helpers.go b/providers/asrockrack/helpers.go index a450ef1b..03380fe3 100644 --- a/providers/asrockrack/helpers.go +++ b/providers/asrockrack/helpers.go @@ -275,7 +275,7 @@ func (a *ASRockRack) upgradeBMC(ctx context.Context) error { endpoint := "api/maintenance/firmware/upgrade" // preserve all configuration during upgrade, full flash - pConfig := &preserveConfig{PreserveConfig: 1, FlashStatus: 1} + pConfig := &preserveConfig{FlashStatus: 1, PreserveConfig: 1, PreserveNetwork: 1, PreserveUser: 1} payload, err := json.Marshal(pConfig) if err != nil { return err From f09df96a76c1653fabb8d13af200459d12bf9a72 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 11:23:16 +0200 Subject: [PATCH 15/25] consts, errors: firmware install status returned when a BMC needs a reset error returned when the install status lookup fails --- devices/constants.go | 4 +++- errors/errors.go | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/devices/constants.go b/devices/constants.go index c5820a75..f0fc4aae 100644 --- a/devices/constants.go +++ b/devices/constants.go @@ -78,9 +78,11 @@ const ( FirmwareInstallFailed = "failed" // FirmwareInstallPowerCycleHost indicates the firmware install requires a host power cycle - // this covers the dell redfish state - 'scheduled' FirmwareInstallPowerCyleHost = "powercycle-host" + // FirmwareInstallPowerCycleBMC indicates the firmware install requires a BMC power cycle + FirmwareInstallPowerCycleBMC = "powercycle-bmc" + FirmwareInstallUnknown = "unknown" // device BIOS/UEFI POST code bmclib identifiers diff --git a/errors/errors.go b/errors/errors.go index fe4af1f1..f6546eb1 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -93,9 +93,12 @@ var ( // ErrFirmwareUpload is returned when a firmware upload method fails ErrFirmwareUpload = errors.New("error uploading firmware") - // ErrFirmwareInstall is returned for firmware update failures + // ErrFirmwareInstall is returned for firmware install failures ErrFirmwareInstall = errors.New("error updating firmware") + // ErrFirmwareInstallStatus is returned for firmware install status read + ErrFirmwareInstallStatus = errors.New("error querying firmware install status") + // ErrRedfishUpdateService is returned on redfish update service errors ErrRedfishUpdateService = errors.New("redfish update service error") From 201a0372825ab6b6b2e4b07b095478045c02b4f8 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 11:24:58 +0200 Subject: [PATCH 16/25] providers/asrr, redfish: implement BmcReset interface This enables the ASRR and Redfish providers to reset the BMC --- providers/asrockrack/asrockrack.go | 1 + providers/asrockrack/power.go | 13 +++++++++++-- providers/redfish/redfish.go | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go index 431a5769..ff241cf9 100644 --- a/providers/asrockrack/asrockrack.go +++ b/providers/asrockrack/asrockrack.go @@ -29,6 +29,7 @@ var ( providers.FeatureFirmwareInstall, providers.FeatureFirmwareInstallStatus, providers.FeaturePostCodeRead, + providers.FeatureBmcReset, } ) diff --git a/providers/asrockrack/power.go b/providers/asrockrack/power.go index 7f64de5d..f4deae18 100644 --- a/providers/asrockrack/power.go +++ b/providers/asrockrack/power.go @@ -86,8 +86,17 @@ func (a *ASRockRack) powerAction(ctx context.Context, action int) (ok bool, err return true, nil } -// 4. reset BMC -// nolint: unused // left here for reference +// BmcReset will reset the BMC - ASRR BMCs only support a cold reset. +func (a *ASRockRack) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { + err = a.resetBMC(ctx) + if err != nil { + return false, err + } + + return true, nil +} + +// 4. reset BMC - performs a cold reset func (a *ASRockRack) resetBMC(ctx context.Context) error { endpoint := "api/maintenance/reset" diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index abbb8073..9199acb6 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -36,6 +36,7 @@ var ( providers.FeatureInventoryRead, providers.FeatureFirmwareInstall, providers.FeatureFirmwareInstallStatus, + providers.FeatureBmcReset, } ) @@ -145,6 +146,23 @@ func (c *Conn) Compatible(ctx context.Context) bool { return err == nil } +// BmcReset powercycles the BMC +func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { + managers, err := c.conn.Service.Managers() + if err != nil { + return false, err + } + + for _, manager := range managers { + err = manager.Reset(rf.ResetType(resetType)) + if err != nil { + return false, err + } + } + + return true, nil +} + // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { return c.status(ctx) From 95d01974ba2ea92875fe5d4eea77a8b2fcb04790 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 22 Apr 2022 16:05:32 +0200 Subject: [PATCH 17/25] redfish: fix implementation, rename GetInventory() -> Inventory() --- providers/redfish/inventory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/redfish/inventory.go b/providers/redfish/inventory.go index 6e3593d8..d12a44bb 100644 --- a/providers/redfish/inventory.go +++ b/providers/redfish/inventory.go @@ -62,7 +62,7 @@ func (c *Conn) DeviceVendorModel(ctx context.Context) (vendor, model string, err return vendor, model, bmclibErrs.ErrRedfishSystemOdataID } -func (c *Conn) GetInventory(ctx context.Context) (device *devices.Device, err error) { +func (c *Conn) Inventory(ctx context.Context) (device *devices.Device, err error) { // initialize inventory object inv := &inventory{conn: c.conn} // TODO: this can soft fail From 91fc9048e4ee0e6a0ec524493cb0bd93153e87f5 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Mon, 25 Apr 2022 16:05:24 +0200 Subject: [PATCH 18/25] bmc/firmware: update FirmwareInstallStatus() method signature - moves the optional component parameter at the end - providers/asrockrack/firmware: wrap FirmwareInstall errors --- bmc/firmware.go | 12 +- bmc/firmware_test.go | 21 +- client.go | 4 +- providers/asrockrack/firmware.go | 20 +- providers/redfish/firmware.go | 2 +- providers/redfish/inventory_collect.go | 344 +++++++++++++++++++++++++ 6 files changed, 374 insertions(+), 29 deletions(-) create mode 100644 providers/redfish/inventory_collect.go diff --git a/bmc/firmware.go b/bmc/firmware.go index 82fecd85..a20f28c1 100644 --- a/bmc/firmware.go +++ b/bmc/firmware.go @@ -93,13 +93,13 @@ type FirmwareInstallVerifier interface { // FirmwareInstallStatus returns the status of the firmware install process. // // parameters: - // component (optional) - the component slug for the component update being installed. // installVersion (required) - the version this method should check is installed. + // component (optional) - the component slug for the component update being installed. // taskID (optional) - the task identifier. // // return values: // status - returns one of the FirmwareInstall statuses (see devices/constants.go). - FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) + FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) } // firmwareInstallVerifierProvider is an internal struct to correlate an implementation/provider and its name @@ -109,7 +109,7 @@ type firmwareInstallVerifierProvider struct { } // firmwareInstallStatus returns the status of the firmware install process -func firmwareInstallStatus(ctx context.Context, component, installVersion, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { +func firmwareInstallStatus(ctx context.Context, installVersion, component, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { var metadataLocal Metadata Loop: for _, elem := range generic { @@ -122,7 +122,7 @@ Loop: break Loop default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) - status, vErr := elem.FirmwareInstallStatus(ctx, component, installVersion, taskID) + status, vErr := elem.FirmwareInstallStatus(ctx, installVersion, component, taskID) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) @@ -138,7 +138,7 @@ Loop: } // FirmwareInstallStatusFromInterfaces pass through to library function -func FirmwareInstallStatusFromInterfaces(ctx context.Context, component, installVersion, taskID string, generic []interface{}) (status string, metadata Metadata, err error) { +func FirmwareInstallStatusFromInterfaces(ctx context.Context, installVersion, component, taskID string, generic []interface{}) (status string, metadata Metadata, err error) { implementations := make([]firmwareInstallVerifierProvider, 0) for _, elem := range generic { temp := firmwareInstallVerifierProvider{name: getProviderName(elem)} @@ -161,5 +161,5 @@ func FirmwareInstallStatusFromInterfaces(ctx context.Context, component, install ) } - return firmwareInstallStatus(ctx, component, installVersion, taskID, implementations) + return firmwareInstallStatus(ctx, installVersion, component, taskID, implementations) } diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go index 894886c2..f530cc66 100644 --- a/bmc/firmware_test.go +++ b/bmc/firmware_test.go @@ -113,7 +113,7 @@ type firmwareInstallStatusTester struct { returnError error } -func (f *firmwareInstallStatusTester) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { +func (f *firmwareInstallStatusTester) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) { return f.returnStatus, f.returnError } @@ -146,7 +146,7 @@ func TestFirmwareInstallStatus(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - taskID, metadata, err := firmwareInstallStatus(ctx, tc.component, tc.installVersion, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) + taskID, metadata, err := firmwareInstallStatus(ctx, tc.installVersion, tc.component, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return @@ -165,16 +165,15 @@ func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { testCases := []struct { testName string component string - applyAt string - forceInstall bool - reader io.Reader - returnTaskID string + installVersion string + taskID string + returnStatus string returnError error providerName string badImplementation bool }{ - {"success with metadata", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", nil, "foo", false}, - {"failure with bad implementation", devices.SlugBIOS, devices.FirmwareApplyOnReset, false, nil, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, + {"success with metadata", devices.SlugBIOS, "1.1", "1234", "status-done", nil, "foo", false}, + {"failure with bad implementation", devices.SlugBIOS, "1.1", "1234", "status-done", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { @@ -184,10 +183,10 @@ func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { - testImplementation := &firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} + testImplementation := &firmwareInstallStatusTester{returnStatus: tc.returnStatus, returnError: tc.returnError} generic = []interface{}{testImplementation} } - taskID, metadata, err := FirmwareInstallFromInterfaces(context.Background(), tc.component, tc.applyAt, tc.forceInstall, tc.reader, generic) + status, metadata, err := FirmwareInstallStatusFromInterfaces(context.Background(), tc.component, tc.installVersion, tc.taskID, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return @@ -197,7 +196,7 @@ func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { t.Fatal(err) } - assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } diff --git a/client.go b/client.go index ba3103d4..24b74598 100644 --- a/client.go +++ b/client.go @@ -244,8 +244,8 @@ func (c *Client) FirmwareInstall(ctx context.Context, component, applyAt string, } // FirmwareInstallStatus pass through library function to check firmware install status -func (c *Client) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { - status, metadata, err := bmc.FirmwareInstallStatusFromInterfaces(ctx, component, installVersion, taskID, c.Registry.GetDriverInterfaces()) +func (c *Client) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) { + status, metadata, err := bmc.FirmwareInstallStatusFromInterfaces(ctx, installVersion, component, taskID, c.Registry.GetDriverInterfaces()) c.setMetadata(metadata) return status, err diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go index 374d3c29..1020b765 100644 --- a/providers/asrockrack/firmware.go +++ b/providers/asrockrack/firmware.go @@ -2,7 +2,6 @@ package asrockrack import ( "context" - "fmt" "io" "os" "strconv" @@ -40,14 +39,17 @@ func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt str err = a.firmwareInstallBMC(ctx, reader, size) default: return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + } + if err != nil { + err = errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) } return jobID, err } // FirmwareInstallStatus returns the status of the firmware install process, a bool value indicating if the component requires a reset -func (a *ASRockRack) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (status string, err error) { +func (a *ASRockRack) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) { switch component { case devices.SlugBIOS, devices.SlugBMC: return a.firmwareUpdateStatus(ctx, component, installVersion) @@ -64,28 +66,28 @@ func (a *ASRockRack) firmwareInstallBMC(ctx context.Context, reader io.Reader, f a.log.V(2).Info("info", "action", "set device to flash mode, takes a minute...", "step", "1/4") err = a.setFlashMode(ctx) if err != nil { - return fmt.Errorf("failed in step 1/4 - set device to flash mode: " + err.Error()) + return errors.Wrap(err, "failed in step 1/4 - set device to flash mode") } // 2. upload firmware image file a.log.V(2).Info("info", "action", "upload BMC firmware image", "step", "2/4") err = a.uploadFirmware(ctx, "api/maintenance/firmware", reader, fileSize) if err != nil { - return fmt.Errorf("failed in step 2/4 - upload BMC firmware image: " + err.Error()) + return errors.Wrap(err, "failed in step 2/4 - upload BMC firmware image") } // 3. BMC to verify the uploaded file err = a.verifyUploadedFirmware(ctx) a.log.V(2).Info("info", "action", "BMC verify uploaded firmware", "step", "3/4") if err != nil { - return fmt.Errorf("failed in step 3/4 - BMC verify uploaded firmware: " + err.Error()) + return errors.Wrap(err, "failed in step 3/4 - BMC verify uploaded firmware") } // 4. Run the upgrade - preserving current config a.log.V(2).Info("info", "action", "proceed with upgrade, preserve current configuration", "step", "4/4") err = a.upgradeBMC(ctx) if err != nil { - return fmt.Errorf("failed in step 4/4 - proceed with upgrade: " + err.Error()) + return errors.Wrap(err, "failed in step 4/4 - proceed with upgrade") } return nil @@ -99,21 +101,21 @@ func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, a.log.V(2).Info("info", "action", "upload BIOS firmware image", "step", "1/3") err = a.uploadFirmware(ctx, "api/asrr/maintenance/BIOS/firmware", reader, fileSize) if err != nil { - return fmt.Errorf("failed in step 1/3 - upload firmware image: " + err.Error()) + return errors.Wrap(err, "failed in step 1/3 - upload firmware image") } // 2. set update parameters to preserve configurations a.log.V(2).Info("info", "action", "set flash configuration", "step", "2/3") err = a.biosUpgradeConfiguration(ctx) if err != nil { - return fmt.Errorf("failed in step 2/3 - set flash configuration: " + err.Error()) + return errors.Wrap(err, "failed in step 2/3 - set flash configuration") } // 3. run upgrade a.log.V(2).Info("info", "action", "proceed with upgrade", "step", "3/3") err = a.upgradeBIOS(ctx) if err != nil { - return fmt.Errorf("failed in step 3/3 - proceed with upgrade: " + err.Error()) + return errors.Wrap(err, "failed in step 3/3 - proceed with upgrade") } return nil diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index e1098040..f7dfb6d2 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -103,7 +103,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f } // FirmwareInstallStatus returns the status of the firmware install task queued -func (c *Conn) FirmwareInstallStatus(ctx context.Context, component, installVersion, taskID string) (state string, err error) { +func (c *Conn) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (state string, err error) { vendor, _, err := c.DeviceVendorModel(ctx) if err != nil { return state, errors.Wrap(err, "unable to determine device vendor, model attributes") diff --git a/providers/redfish/inventory_collect.go b/providers/redfish/inventory_collect.go new file mode 100644 index 00000000..04349373 --- /dev/null +++ b/providers/redfish/inventory_collect.go @@ -0,0 +1,344 @@ +package redfish + +// defines various inventory collection helper methods + +// collectEnclosure collects Enclosure information +func (i *inventory) collectEnclosure(ch *redfish.Chassis, device *devices.Device) (err error) { + + e := &devices.Enclosure{ + ID: ch.ID, + Description: ch.Description, + Vendor: ch.Manufacturer, + Model: ch.Model, + ChassisType: string(ch.ChassisType), + Status: &devices.Status{ + Health: string(ch.Status.Health), + State: string(ch.Status.State), + }, + Firmware: &devices.Firmware{}, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugEnclosure, e.ID, e.Firmware) + + device.Enclosures = append(device.Enclosures, e) + + return nil +} + +// collectPSUs collects Power Supply Unit component information +func (i *inventory) collectPSUs(ch *redfish.Chassis, device *devices.Device) (err error) { + power, err := ch.Power() + if err != nil { + return err + } + + if power == nil { + return nil + } + + for _, psu := range power.PowerSupplies { + p := &devices.PSU{ + ID: psu.ID, + Description: psu.Name, + Vendor: psu.Manufacturer, + Model: psu.Model, + Serial: psu.SerialNumber, + PowerCapacityWatts: int64(psu.PowerCapacityWatts), + Status: &devices.Status{ + Health: string(psu.Status.Health), + State: string(psu.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: psu.FirmwareVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugPSU, psu.ID, p.Firmware) + + device.PSUs = append(device.PSUs, p) + + } + return nil +} + +// collectTPMs collects Trusted Platform Module component information +func (i *inventory) collectTPMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + for _, module := range sys.TrustedModules { + + tpm := &devices.TPM{ + InterfaceType: string(module.InterfaceType), + Firmware: &devices.Firmware{ + Installed: module.FirmwareVersion, + }, + Status: &devices.Status{ + State: string(module.Status.State), + Health: string(module.Status.Health), + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugTPM, "TPM", tpm.Firmware) + + device.TPMs = append(device.TPMs, tpm) + } + + return nil +} + +// collectNICs collects network interface component information +func (i *inventory) collectNICs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + // collect network interface information + nics, err := sys.NetworkInterfaces() + if err != nil { + return err + } + + // collect network ethernet interface information, these attributes are not available in NetworkAdapter, NetworkInterfaces + ethernetInterfaces, err := sys.EthernetInterfaces() + if err != nil { + return err + } + + for _, nic := range nics { + + // collect network interface adaptor information + adapter, err := nic.NetworkAdapter() + if err != nil { + return err + } + + n := &devices.NIC{ + ID: nic.ID, // "Id": "NIC.Slot.3", + Vendor: adapter.Manufacturer, + Model: adapter.Model, + Serial: adapter.SerialNumber, + Status: &devices.Status{ + State: string(nic.Status.State), + Health: string(nic.Status.Health), + }, + } + + if len(adapter.Controllers) > 0 { + n.Firmware = &devices.Firmware{ + Installed: adapter.Controllers[0].FirmwarePackageVersion, + } + } + + // populate mac addresses from ethernet interfaces + for _, ethInterface := range ethernetInterfaces { + // the ethernet interface includes the port and position number NIC.Slot.3-1-1 + if !strings.HasPrefix(ethInterface.ID, adapter.ID) { + continue + } + + // The ethernet interface description is + n.Description = ethInterface.Description + n.MacAddress = ethInterface.MACAddress + n.SpeedBits = int64(ethInterface.SpeedMbps*10 ^ 6) + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugNIC, n.ID, n.Firmware) + + device.NICs = append(device.NICs, n) + } + + return nil +} + +func (i *inventory) collectBIOS(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + bios, err := sys.Bios() + if err != nil { + return err + } + + device.BIOS = &devices.BIOS{ + Description: bios.Description, + Firmware: &devices.Firmware{ + Installed: sys.BIOSVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(devices.SlugBIOS, "BIOS", device.BIOS.Firmware) + + return nil +} + +// collectDrives collects drive component information +func (i *inventory) collectDrives(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + storage, err := sys.Storage() + if err != nil { + return err + } + + for _, member := range storage { + if member.DrivesCount == 0 { + continue + } + + drives, err := member.Drives() + if err != nil { + return err + } + + for _, drive := range drives { + d := &devices.Drive{ + ID: drive.ID, + ProductName: drive.Model, + Type: string(drive.MediaType), + Description: drive.Description, + Serial: drive.SerialNumber, + StorageController: member.ID, + Vendor: drive.Manufacturer, + Model: drive.Model, + Protocol: string(drive.Protocol), + CapacityBytes: drive.CapacityBytes, + CapableSpeedGbps: int64(drive.CapableSpeedGbs), + NegotiatedSpeedGbps: int64(drive.NegotiatedSpeedGbs), + BlockSizeBytes: int64(drive.BlockSizeBytes), + Firmware: &devices.Firmware{ + Installed: drive.Revision, + }, + Status: &devices.Status{ + Health: string(drive.Status.Health), + State: string(drive.Status.State), + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes("Disk", drive.ID, d.Firmware) + + device.Drives = append(device.Drives, d) + + } + + } + + return nil +} + +// collectStorageControllers populates the device with Storage controller component attributes +func (i *inventory) collectStorageControllers(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + storage, err := sys.Storage() + if err != nil { + return err + } + + for _, member := range storage { + for _, controller := range member.StorageControllers { + + c := &devices.StorageController{ + ID: controller.ID, + Description: controller.Name, + Vendor: controller.Manufacturer, + Model: controller.PartNumber, + Serial: controller.SerialNumber, + SpeedGbps: int64(controller.SpeedGbps), + Status: &devices.Status{ + Health: string(controller.Status.Health), + State: string(controller.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: controller.FirmwareVersion, + }, + } + + // include additional firmware attributes from redfish firmware inventory + i.firmwareAttributes(c.Description, c.ID, c.Firmware) + + device.StorageControllers = append(device.StorageControllers, c) + } + + } + + return nil +} + +// collectCPUs populates the device with CPU component attributes +func (i *inventory) collectCPUs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + procs, err := sys.Processors() + if err != nil { + return err + } + + for _, proc := range procs { + if proc.ProcessorType != "CPU" { + // TODO: handle this case + continue + } + + device.CPUs = append(device.CPUs, &devices.CPU{ + ID: proc.ID, + Description: proc.Description, + Vendor: proc.Manufacturer, + Model: proc.Model, + Architecture: string(proc.ProcessorArchitecture), + Serial: "", + Slot: proc.Socket, + ClockSpeedHz: int64(proc.MaxSpeedMHz), + Cores: proc.TotalCores, + Threads: proc.TotalThreads, + Status: &devices.Status{ + Health: string(proc.Status.Health), + State: string(proc.Status.State), + }, + Firmware: &devices.Firmware{ + Installed: proc.ProcessorID.MicrocodeInfo, + }, + }) + } + + return nil +} + +// collectDIMMs populates the device with memory component attributes +func (i *inventory) collectDIMMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { + dimms, err := sys.Memory() + if err != nil { + return err + } + + for _, dimm := range dimms { + device.Memory = append(device.Memory, &devices.Memory{ + Description: dimm.Description, + Slot: dimm.ID, + Type: string(dimm.MemoryType), + Vendor: dimm.Manufacturer, + Model: "", + Serial: dimm.SerialNumber, + SizeBytes: int64(dimm.VolatileSizeMiB), + FormFactor: "", + PartNumber: dimm.PartNumber, + ClockSpeedHz: int64(dimm.OperatingSpeedMhz), + Status: &devices.Status{ + Health: string(dimm.Status.Health), + State: string(dimm.Status.State), + }, + }) + } + + return nil +} + +// collecCPLDs populates the device with CPLD component attributes +func (i *inventory) collectCPLDs(device *devices.Device) (err error) { + + cpld := &devices.CPLD{ + Vendor: device.Vendor, + Model: device.Model, + Firmware: &devices.Firmware{Metadata: make(map[string]string)}, + } + + i.firmwareAttributes(devices.SlugCPLD, "", cpld.Firmware) + name, exists := cpld.Firmware.Metadata["name"] + if exists { + cpld.Description = name + } + + device.CPLDs = []*devices.CPLD{cpld} + + return nil +} From e2543e3c7c30b7b3eb580fbb8aa7e67001f382cd Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Mon, 25 Apr 2022 16:08:51 +0200 Subject: [PATCH 19/25] redfish/inventory: split out helper methods --- go.mod | 7 +- go.sum | 19 +- providers/redfish/inventory.go | 341 ------------------------- providers/redfish/inventory_collect.go | 7 + 4 files changed, 22 insertions(+), 352 deletions(-) diff --git a/go.mod b/go.mod index d4227f44..b918e83d 100644 --- a/go.mod +++ b/go.mod @@ -39,9 +39,10 @@ require ( github.com/spf13/viper v1.7.1 github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 - golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 - golang.org/x/text v0.3.5 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3e9ab8f5..56bf5d6c 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -262,8 +262,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -288,17 +288,20 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/providers/redfish/inventory.go b/providers/redfish/inventory.go index d12a44bb..c324fa3c 100644 --- a/providers/redfish/inventory.go +++ b/providers/redfish/inventory.go @@ -256,347 +256,6 @@ func (i *inventory) systemAttributes(device *devices.Device) (err error) { return nil } -// collectEnclosure collects Enclosure information -func (i *inventory) collectEnclosure(ch *redfish.Chassis, device *devices.Device) (err error) { - - e := &devices.Enclosure{ - ID: ch.ID, - Description: ch.Description, - Vendor: ch.Manufacturer, - Model: ch.Model, - ChassisType: string(ch.ChassisType), - Status: &devices.Status{ - Health: string(ch.Status.Health), - State: string(ch.Status.State), - }, - Firmware: &devices.Firmware{}, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(devices.SlugEnclosure, e.ID, e.Firmware) - - device.Enclosures = append(device.Enclosures, e) - - return nil -} - -// collectPSUs collects Power Supply Unit component information -func (i *inventory) collectPSUs(ch *redfish.Chassis, device *devices.Device) (err error) { - power, err := ch.Power() - if err != nil { - return err - } - - if power == nil { - return nil - } - - for _, psu := range power.PowerSupplies { - p := &devices.PSU{ - ID: psu.ID, - Description: psu.Name, - Vendor: psu.Manufacturer, - Model: psu.Model, - Serial: psu.SerialNumber, - PowerCapacityWatts: int64(psu.PowerCapacityWatts), - Status: &devices.Status{ - Health: string(psu.Status.Health), - State: string(psu.Status.State), - }, - Firmware: &devices.Firmware{ - Installed: psu.FirmwareVersion, - }, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(devices.SlugPSU, psu.ID, p.Firmware) - - device.PSUs = append(device.PSUs, p) - - } - return nil -} - -// collectTPMs collects Trusted Platform Module component information -func (i *inventory) collectTPMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - for _, module := range sys.TrustedModules { - - tpm := &devices.TPM{ - InterfaceType: string(module.InterfaceType), - Firmware: &devices.Firmware{ - Installed: module.FirmwareVersion, - }, - Status: &devices.Status{ - State: string(module.Status.State), - Health: string(module.Status.Health), - }, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(devices.SlugTPM, "TPM", tpm.Firmware) - - device.TPMs = append(device.TPMs, tpm) - } - - return nil -} - -// collectNICs collects network interface component information -func (i *inventory) collectNICs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - // collect network interface information - nics, err := sys.NetworkInterfaces() - if err != nil { - return err - } - - // collect network ethernet interface information, these attributes are not available in NetworkAdapter, NetworkInterfaces - ethernetInterfaces, err := sys.EthernetInterfaces() - if err != nil { - return err - } - - for _, nic := range nics { - - // collect network interface adaptor information - adapter, err := nic.NetworkAdapter() - if err != nil { - return err - } - - n := &devices.NIC{ - ID: nic.ID, // "Id": "NIC.Slot.3", - Vendor: adapter.Manufacturer, - Model: adapter.Model, - Serial: adapter.SerialNumber, - Status: &devices.Status{ - State: string(nic.Status.State), - Health: string(nic.Status.Health), - }, - } - - if len(adapter.Controllers) > 0 { - n.Firmware = &devices.Firmware{ - Installed: adapter.Controllers[0].FirmwarePackageVersion, - } - } - - // populate mac addresses from ethernet interfaces - for _, ethInterface := range ethernetInterfaces { - // the ethernet interface includes the port and position number NIC.Slot.3-1-1 - if !strings.HasPrefix(ethInterface.ID, adapter.ID) { - continue - } - - // The ethernet interface description is - n.Description = ethInterface.Description - n.MacAddress = ethInterface.MACAddress - n.SpeedBits = int64(ethInterface.SpeedMbps*10 ^ 6) - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(devices.SlugNIC, n.ID, n.Firmware) - - device.NICs = append(device.NICs, n) - } - - return nil -} - -func (i *inventory) collectBIOS(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - bios, err := sys.Bios() - if err != nil { - return err - } - - device.BIOS = &devices.BIOS{ - Description: bios.Description, - Firmware: &devices.Firmware{ - Installed: sys.BIOSVersion, - }, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(devices.SlugBIOS, "BIOS", device.BIOS.Firmware) - - return nil -} - -// collectDrives collects drive component information -func (i *inventory) collectDrives(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - storage, err := sys.Storage() - if err != nil { - return err - } - - for _, member := range storage { - if member.DrivesCount == 0 { - continue - } - - drives, err := member.Drives() - if err != nil { - return err - } - - for _, drive := range drives { - d := &devices.Drive{ - ID: drive.ID, - ProductName: drive.Model, - Type: string(drive.MediaType), - Description: drive.Description, - Serial: drive.SerialNumber, - StorageController: member.ID, - Vendor: drive.Manufacturer, - Model: drive.Model, - Protocol: string(drive.Protocol), - CapacityBytes: drive.CapacityBytes, - CapableSpeedGbps: int64(drive.CapableSpeedGbs), - NegotiatedSpeedGbps: int64(drive.NegotiatedSpeedGbs), - BlockSizeBytes: int64(drive.BlockSizeBytes), - Firmware: &devices.Firmware{ - Installed: drive.Revision, - }, - Status: &devices.Status{ - Health: string(drive.Status.Health), - State: string(drive.Status.State), - }, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes("Disk", drive.ID, d.Firmware) - - device.Drives = append(device.Drives, d) - - } - - } - - return nil -} - -// collectStorageControllers populates the device with Storage controller component attributes -func (i *inventory) collectStorageControllers(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - storage, err := sys.Storage() - if err != nil { - return err - } - - for _, member := range storage { - for _, controller := range member.StorageControllers { - - c := &devices.StorageController{ - ID: controller.ID, - Description: controller.Name, - Vendor: controller.Manufacturer, - Model: controller.PartNumber, - Serial: controller.SerialNumber, - SpeedGbps: int64(controller.SpeedGbps), - Status: &devices.Status{ - Health: string(controller.Status.Health), - State: string(controller.Status.State), - }, - Firmware: &devices.Firmware{ - Installed: controller.FirmwareVersion, - }, - } - - // include additional firmware attributes from redfish firmware inventory - i.firmwareAttributes(c.Description, c.ID, c.Firmware) - - device.StorageControllers = append(device.StorageControllers, c) - } - - } - - return nil -} - -// collectCPUs populates the device with CPU component attributes -func (i *inventory) collectCPUs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - procs, err := sys.Processors() - if err != nil { - return err - } - - for _, proc := range procs { - if proc.ProcessorType != "CPU" { - // TODO: handle this case - continue - } - - device.CPUs = append(device.CPUs, &devices.CPU{ - ID: proc.ID, - Description: proc.Description, - Vendor: proc.Manufacturer, - Model: proc.Model, - Architecture: string(proc.ProcessorArchitecture), - Serial: "", - Slot: proc.Socket, - ClockSpeedHz: int64(proc.MaxSpeedMHz), - Cores: proc.TotalCores, - Threads: proc.TotalThreads, - Status: &devices.Status{ - Health: string(proc.Status.Health), - State: string(proc.Status.State), - }, - Firmware: &devices.Firmware{ - Installed: proc.ProcessorID.MicrocodeInfo, - }, - }) - } - - return nil -} - -// collectDIMMs populates the device with memory component attributes -func (i *inventory) collectDIMMs(sys *redfish.ComputerSystem, device *devices.Device) (err error) { - dimms, err := sys.Memory() - if err != nil { - return err - } - - for _, dimm := range dimms { - device.Memory = append(device.Memory, &devices.Memory{ - Description: dimm.Description, - Slot: dimm.ID, - Type: string(dimm.MemoryType), - Vendor: dimm.Manufacturer, - Model: "", - Serial: dimm.SerialNumber, - SizeBytes: int64(dimm.VolatileSizeMiB), - FormFactor: "", - PartNumber: dimm.PartNumber, - ClockSpeedHz: int64(dimm.OperatingSpeedMhz), - Status: &devices.Status{ - Health: string(dimm.Status.Health), - State: string(dimm.Status.State), - }, - }) - } - - return nil -} - -// collecCPLDs populates the device with CPLD component attributes -func (i *inventory) collectCPLDs(device *devices.Device) (err error) { - - cpld := &devices.CPLD{ - Vendor: device.Vendor, - Model: device.Model, - Firmware: &devices.Firmware{Metadata: make(map[string]string)}, - } - - i.firmwareAttributes(devices.SlugCPLD, "", cpld.Firmware) - name, exists := cpld.Firmware.Metadata["name"] - if exists { - cpld.Description = name - } - - device.CPLDs = []*devices.CPLD{cpld} - - return nil -} - // firmwareInventory looks up the redfish inventory for objects that // match - 1. slug, 2. id // and returns the intalled or previous firmware for objects that matched diff --git a/providers/redfish/inventory_collect.go b/providers/redfish/inventory_collect.go index 04349373..678d05c5 100644 --- a/providers/redfish/inventory_collect.go +++ b/providers/redfish/inventory_collect.go @@ -1,5 +1,12 @@ package redfish +import ( + "strings" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/stmcginnis/gofish/redfish" +) + // defines various inventory collection helper methods // collectEnclosure collects Enclosure information From a68ebce9e2e2dcdaf68b14d465e20b50c1cd1019 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Tue, 26 Apr 2022 11:54:50 +0200 Subject: [PATCH 20/25] providers/asrockrack: use structured logging, purge debug statements --- examples/v1/firmware/firmware.go | 19 ++++++------------ providers/asrockrack/firmware.go | 33 ++++++++++++++------------------ 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/examples/v1/firmware/firmware.go b/examples/v1/firmware/firmware.go index 70551673..dbe19f32 100644 --- a/examples/v1/firmware/firmware.go +++ b/examples/v1/firmware/firmware.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "log" "os" "time" @@ -18,13 +17,13 @@ func main() { defer cancel() // set BMC parameters here - host := "" + host := "10.247.150.161" port := "" - user := "root" - pass := "" + user := "admin" + pass := "RmrJ56BFUarn6g" l := logrus.New() - l.Level = logrus.DebugLevel + l.Level = logrus.TraceLevel logger := logrusr.New(l) if host == "" || user == "" || pass == "" { @@ -40,23 +39,17 @@ func main() { defer cl.Close(ctx) - for _, update := range []string{"/tmp/iDRAC-with-Lifecycle-Controller_Firmware_F87RP_WN64_5.00.00.00_A00.EXE"} { + for _, update := range []string{"/tmp/E6D4INL2.09C.ima"} { fh, err := os.Open(update) if err != nil { log.Fatal(err) } - taskID, err := cl.FirmwareInstall(ctx, devices.SlugBMC, devices.FirmwareApplyOnReset, true, fh) + _, err = cl.FirmwareInstall(ctx, devices.SlugBMC, devices.FirmwareApplyOnReset, true, fh) if err != nil { l.Error(err) } - state, err := cl.FirmwareInstallStatus(ctx, "", taskID, "5.00.00.00") - if err != nil { - log.Fatal(err) - } - - fmt.Printf("state: %s\n", state) } } diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go index 1020b765..572cc6f1 100644 --- a/providers/asrockrack/firmware.go +++ b/providers/asrockrack/firmware.go @@ -4,7 +4,6 @@ import ( "context" "io" "os" - "strconv" "strings" "github.com/pkg/errors" @@ -26,7 +25,7 @@ func (a *ASRockRack) FirmwareInstall(ctx context.Context, component, applyAt str if file, ok := reader.(*os.File); ok { finfo, err := file.Stat() if err != nil { - a.log.V(2).Info("warn", "unable to determine file size: "+err.Error()) + a.log.V(2).Error(err, "unable to determine file size") } size = finfo.Size() @@ -63,14 +62,14 @@ func (a *ASRockRack) firmwareInstallBMC(ctx context.Context, reader io.Reader, f var err error // 1. set the device to flash mode - prepares the flash - a.log.V(2).Info("info", "action", "set device to flash mode, takes a minute...", "step", "1/4") + a.log.V(2).WithValues("step", "1/4").Info("set device to flash mode, takes a minute...") err = a.setFlashMode(ctx) if err != nil { return errors.Wrap(err, "failed in step 1/4 - set device to flash mode") } // 2. upload firmware image file - a.log.V(2).Info("info", "action", "upload BMC firmware image", "step", "2/4") + a.log.V(2).WithValues("step", "2/4").Info("upload BMC firmware image") err = a.uploadFirmware(ctx, "api/maintenance/firmware", reader, fileSize) if err != nil { return errors.Wrap(err, "failed in step 2/4 - upload BMC firmware image") @@ -78,16 +77,16 @@ func (a *ASRockRack) firmwareInstallBMC(ctx context.Context, reader io.Reader, f // 3. BMC to verify the uploaded file err = a.verifyUploadedFirmware(ctx) - a.log.V(2).Info("info", "action", "BMC verify uploaded firmware", "step", "3/4") + a.log.V(2).WithValues("step", "3/4").Info("verify uploaded BMC firmware") if err != nil { - return errors.Wrap(err, "failed in step 3/4 - BMC verify uploaded firmware") + return errors.Wrap(err, "failed in step 3/4 - verify uploaded BMC firmware") } // 4. Run the upgrade - preserving current config - a.log.V(2).Info("info", "action", "proceed with upgrade, preserve current configuration", "step", "4/4") + a.log.V(2).WithValues("step", "4/4").Info("proceed with BMC firmware install, preserve current configuration") err = a.upgradeBMC(ctx) if err != nil { - return errors.Wrap(err, "failed in step 4/4 - proceed with upgrade") + return errors.Wrap(err, "failed in step 4/4 - proceed with BMC firmware install") } return nil @@ -98,24 +97,24 @@ func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, var err error // 1. upload firmware image file - a.log.V(2).Info("info", "action", "upload BIOS firmware image", "step", "1/3") + a.log.V(2).WithValues("step", "1/3").Info("upload BIOS firmware image") err = a.uploadFirmware(ctx, "api/asrr/maintenance/BIOS/firmware", reader, fileSize) if err != nil { - return errors.Wrap(err, "failed in step 1/3 - upload firmware image") + return errors.Wrap(err, "failed in step 1/3 - upload BIOS firmware image") } // 2. set update parameters to preserve configurations - a.log.V(2).Info("info", "action", "set flash configuration", "step", "2/3") + a.log.V(2).WithValues("step", "2/3").Info("set BIOS preserve flash configuration") err = a.biosUpgradeConfiguration(ctx) if err != nil { return errors.Wrap(err, "failed in step 2/3 - set flash configuration") } // 3. run upgrade - a.log.V(2).Info("info", "action", "proceed with upgrade", "step", "3/3") + a.log.V(2).WithValues("step", "3/3").Info("proceed with BIOS firmware install") err = a.upgradeBIOS(ctx) if err != nil { - return errors.Wrap(err, "failed in step 3/3 - proceed with upgrade") + return errors.Wrap(err, "failed in step 3/3 - proceed with BIOS firmware install") } return nil @@ -123,10 +122,6 @@ func (a *ASRockRack) firmwareInstallBIOS(ctx context.Context, reader io.Reader, // firmwareUpdateBIOSStatus returns the BIOS firmware install status func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, installVersion string) (status string, err error) { - // TODO: purge debug logging - os.Setenv("BMCLIB_LOG_LEVEL", "trace") - defer os.Unsetenv("BMCLIB_LOG_LEVEL") - var endpoint string switch component { case devices.SlugBIOS: @@ -142,7 +137,7 @@ func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, // once an update completes/fails this endpoint will return 500 progress, err := a.flashProgress(ctx, endpoint) if err != nil { - a.log.V(3).Info("warn", "bmc query for install progress returned error: "+err.Error()) + a.log.V(3).Error(err, "bmc query for install progress returned error: ") } if progress != nil { @@ -152,7 +147,7 @@ func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, case 2: return devices.FirmwareInstallComplete, nil default: - a.log.V(3).Info("warn", "bmc returned unknown flash progress state: "+strconv.Itoa(progress.State)) + a.log.V(3).WithValues("state", progress.State).Info("warn", "bmc returned unknown flash progress state") } } From 8e5a78458dc843301837ffc9080f2263330f8ced Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Tue, 26 Apr 2022 15:34:05 +0200 Subject: [PATCH 21/25] examples: merge changes from master, fixup --- examples/v1/firmware/firmware.go | 55 ------------------- .../main.go | 25 ++++++--- examples/v1/inventory/inventory.go | 35 +++++++++--- examples/v1/status/main.go | 13 ++--- 4 files changed, 49 insertions(+), 79 deletions(-) delete mode 100644 examples/v1/firmware/firmware.go rename examples/v1/{update-firmware => install-firmware}/main.go (72%) diff --git a/examples/v1/firmware/firmware.go b/examples/v1/firmware/firmware.go deleted file mode 100644 index dbe19f32..00000000 --- a/examples/v1/firmware/firmware.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - "time" - - "github.com/bmc-toolbox/bmclib" - "github.com/bmc-toolbox/bmclib/devices" - "github.com/bombsimon/logrusr/v2" - "github.com/sirupsen/logrus" -) - -func main() { - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - - // set BMC parameters here - host := "10.247.150.161" - port := "" - user := "admin" - pass := "RmrJ56BFUarn6g" - - l := logrus.New() - l.Level = logrus.TraceLevel - logger := logrusr.New(l) - - if host == "" || user == "" || pass == "" { - log.Fatal("required host/user/pass parameters not defined") - } - - cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) - - err := cl.Open(ctx) - if err != nil { - log.Fatal(err, "bmc login failed") - } - - defer cl.Close(ctx) - - for _, update := range []string{"/tmp/E6D4INL2.09C.ima"} { - fh, err := os.Open(update) - if err != nil { - log.Fatal(err) - } - - _, err = cl.FirmwareInstall(ctx, devices.SlugBMC, devices.FirmwareApplyOnReset, true, fh) - if err != nil { - l.Error(err) - } - - } - -} diff --git a/examples/v1/update-firmware/main.go b/examples/v1/install-firmware/main.go similarity index 72% rename from examples/v1/update-firmware/main.go rename to examples/v1/install-firmware/main.go index 100de760..d09ed8cb 100644 --- a/examples/v1/update-firmware/main.go +++ b/examples/v1/install-firmware/main.go @@ -9,11 +9,13 @@ import ( "crypto/x509" "flag" "io/ioutil" + "log" "os" "strconv" "time" "github.com/bmc-toolbox/bmclib" + "github.com/bmc-toolbox/bmclib/devices" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) @@ -26,6 +28,8 @@ func main() { withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolPath := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") firmwarePath := flag.String("firmware", "", "The firmware path to read") + firmwareVersion := flag.String("version", "", "The firmware version being installed") + flag.Parse() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) @@ -62,11 +66,13 @@ func main() { defer cl.Close(ctx) - v, err := cl.GetBMCVersion(ctx) + // collect inventory + inventory, err := cl.Inventory(ctx) if err != nil { - l.Fatal(err, "unable to retrieve BMC version") + l.Fatal(err) } - logger.Info("BMC version", v) + + l.WithField("bmc-version", inventory.BMC.Firmware.Installed).Info() // open file handle fh, err := os.Open(*firmwarePath) @@ -75,14 +81,17 @@ func main() { } defer fh.Close() - fi, err := fh.Stat() + // SlugBMC hardcoded here, this can be any of the existing component slugs from devices/constants.go + // assuming that the BMC provider implements the required component firmware update support + taskID, err := cl.FirmwareInstall(ctx, devices.SlugBMC, devices.FirmwareApplyOnReset, true, fh) if err != nil { - l.Fatal(err) + l.Error(err) } - err = cl.UpdateBMCFirmware(ctx, fh, fi.Size()) + state, err := cl.FirmwareInstallStatus(ctx, taskID, devices.SlugBMC, *firmwareVersion) if err != nil { - l.Fatal(err) + log.Fatal(err) } - logger.WithValues("host", *host).Info("Updated BMC firmware") + + l.WithField("state", state).Info("BMC firmware install state") } diff --git a/examples/v1/inventory/inventory.go b/examples/v1/inventory/inventory.go index 12298e1f..3668dae1 100644 --- a/examples/v1/inventory/inventory.go +++ b/examples/v1/inventory/inventory.go @@ -2,9 +2,13 @@ package main import ( "context" + "crypto/x509" "encoding/json" + "flag" "fmt" + "io/ioutil" "log" + "strconv" "time" "github.com/bmc-toolbox/bmclib" @@ -16,21 +20,36 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() - // set BMC parameters here - host := "" - port := "" - user := "" - pass := "" + user := flag.String("user", "", "Username to login with") + pass := flag.String("password", "", "Username to login with") + host := flag.String("host", "", "BMC hostname to connect to") + port := flag.Int("port", 443, "BMC port to connect to") + withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") + certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") + flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) - if host == "" || user == "" || pass == "" { - log.Fatal("required host/user/pass parameters not defined") + clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} + + if *withSecureTLS { + var pool *x509.CertPool + if *certPoolFile != "" { + pool = x509.NewCertPool() + data, err := ioutil.ReadFile(*certPoolFile) + if err != nil { + l.Fatal(err) + } + pool.AppendCertsFromPEM(data) + } + // a nil pool uses the system certs + clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } - cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + cl := bmclib.NewClient(*host, strconv.Itoa(*port), *user, *pass, clientOpts...) + cl.Registry.Drivers = cl.Registry.Using("redfish") err := cl.Open(ctx) if err != nil { diff --git a/examples/v1/status/main.go b/examples/v1/status/main.go index 32df90ce..0fc0d44f 100644 --- a/examples/v1/status/main.go +++ b/examples/v1/status/main.go @@ -59,11 +59,12 @@ func main() { } defer cl.Close(ctx) - version, err := cl.GetBMCVersion(ctx) + inventory, err := cl.Inventory(ctx) if err != nil { - l.WithError(err).Error() + l.Fatal(err) } - l.WithField("bmc-version", version).Info() + + l.WithField("bmc-version", inventory.BMC.Firmware.Installed).Info() state, err := cl.GetPowerState(ctx) if err != nil { @@ -71,9 +72,5 @@ func main() { } l.WithField("power-state", state).Info() - version, err = cl.GetBIOSVersion(ctx) - if err != nil { - l.WithError(err).Error() - } - l.WithField("bios-version", version).Info() + l.WithField("bios-version", inventory.BIOS.Firmware.Installed).Info() } From 0f7a4c723483e0d2357c9e1417e79016ca86da6b Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Tue, 26 Apr 2022 16:48:02 +0200 Subject: [PATCH 22/25] providers: merge changes from master, a few suggested fixes --- providers/asrockrack/asrockrack.go | 2 -- providers/asrockrack/firmware.go | 7 +++++-- providers/redfish/redfish.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go index ff241cf9..faa44d46 100644 --- a/providers/asrockrack/asrockrack.go +++ b/providers/asrockrack/asrockrack.go @@ -4,8 +4,6 @@ import ( "bytes" "context" "crypto/x509" - "fmt" - "io" "net/http" "github.com/bmc-toolbox/bmclib/devices" diff --git a/providers/asrockrack/firmware.go b/providers/asrockrack/firmware.go index 572cc6f1..37c308de 100644 --- a/providers/asrockrack/firmware.go +++ b/providers/asrockrack/firmware.go @@ -10,6 +10,7 @@ import ( "github.com/bmc-toolbox/bmclib/devices" bmclibErrs "github.com/bmc-toolbox/bmclib/errors" + "github.com/bmc-toolbox/bmclib/internal" ) const ( @@ -179,6 +180,10 @@ func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, // - 1 indicates the given version parameter does not match the version installed // - 2 the version parameter returned from the BMC is empty (which means the BMC needs a reset) func (a *ASRockRack) versionInstalled(ctx context.Context, component, version string) (status int, err error) { + if !internal.StringInSlice(component, []string{devices.SlugBIOS, devices.SlugBMC}) { + return versionStrError, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) + } + fwInfo, err := a.firmwareInfo(ctx) if err != nil { err = errors.Wrap(err, "error querying for firmware info: ") @@ -193,8 +198,6 @@ func (a *ASRockRack) versionInstalled(ctx context.Context, component, version st installed = fwInfo.BIOSVersion case devices.SlugBMC: installed = fwInfo.BMCVersion - default: - return versionStrError, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) } // version match diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 9199acb6..a372bfbe 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -91,7 +91,7 @@ func (c *Conn) Open(ctx context.Context) (err error) { } config := gofish.ClientConfig{ - Endpoint: "https://" + c.Host, + Endpoint: c.Host, Username: c.User, Password: c.Pass, Insecure: true, From 92f6fc0b17ee00bd1c29301d4215e50e22ca5b2b Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Tue, 26 Apr 2022 16:52:18 +0200 Subject: [PATCH 23/25] go.mod: deps updated after rebase --- go.mod | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index b918e83d..02879aa3 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,16 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.7.1 - github.com/stmcginnis/gofish v0.12.0 + github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 - golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v9 v9.31.0 ) +require github.com/subosito/gotenv v1.2.0 // indirect + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect @@ -36,11 +38,6 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.7.1 - github.com/stmcginnis/gofish v0.12.1-0.20220311113027-6072260f4c8d - github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect From b14936a416aa64512fe605355c345f4753ebb1bd Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 27 Apr 2022 17:59:33 +0200 Subject: [PATCH 24/25] providers/asrockrack: pass context to Compatible() also fixes the registrar.Verifier implementation --- providers/asrockrack/asrockrack.go | 4 ++-- providers/asrockrack/asrockrack_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go index faa44d46..8a965707 100644 --- a/providers/asrockrack/asrockrack.go +++ b/providers/asrockrack/asrockrack.go @@ -94,8 +94,8 @@ func NewWithOptions(ip string, username string, password string, log logr.Logger // Compatible implements the registrar.Verifier interface // returns true if the BMC is identified to be an asrockrack -func (a *ASRockRack) Compatible() bool { - resp, statusCode, err := a.queryHTTPS(context.TODO(), "/", "GET", nil, nil, 0) +func (a *ASRockRack) Compatible(ctx context.Context) bool { + resp, statusCode, err := a.queryHTTPS(ctx, "/", "GET", nil, nil, 0) if err != nil { return false } diff --git a/providers/asrockrack/asrockrack_test.go b/providers/asrockrack/asrockrack_test.go index 9006cbdf..cc59d32e 100644 --- a/providers/asrockrack/asrockrack_test.go +++ b/providers/asrockrack/asrockrack_test.go @@ -9,7 +9,7 @@ import ( ) func Test_Compatible(t *testing.T) { - b := aClient.Compatible() + b := aClient.Compatible(context.TODO()) if !b { t.Errorf("expected true, got false") } From eb3610c7ad0a56cd53e18a208eed26418c946ebe Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 27 Apr 2022 18:00:37 +0200 Subject: [PATCH 25/25] examples/v1/inventory: fix JSON indent --- examples/v1/inventory/inventory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/v1/inventory/inventory.go b/examples/v1/inventory/inventory.go index 3668dae1..d0b1c70d 100644 --- a/examples/v1/inventory/inventory.go +++ b/examples/v1/inventory/inventory.go @@ -63,7 +63,7 @@ func main() { l.Error(err) } - b, err := json.MarshalIndent(inv, " ", " ") + b, err := json.MarshalIndent(inv, "", " ") if err != nil { l.Error(err) }