From 4c227db0c1a716956d351ce0aa1cb1ec65640607 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Thu, 17 Oct 2024 15:30:34 -0500 Subject: [PATCH] Add Supermicro UpdateService object (#375) This adds an OEM version of the UpdateService to expose some OEM-specific actions and properties. Signed-off-by: Sean McGinnis --- oem/smc/updateservice.go | 191 ++++++++++++++++++++++++++++++++++ oem/smc/updateservice_test.go | 86 +++++++++++++++ redfish/updateservice.go | 6 +- 3 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 oem/smc/updateservice.go create mode 100644 oem/smc/updateservice_test.go diff --git a/oem/smc/updateservice.go b/oem/smc/updateservice.go new file mode 100644 index 00000000..a8b65df5 --- /dev/null +++ b/oem/smc/updateservice.go @@ -0,0 +1,191 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package smc + +import ( + "encoding/json" + "errors" + + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/redfish" +) + +type SSLCert struct { + common.Entity + + ValidFrom string + GoodThrough string `json:"GoodTHRU"` + + uploadTarget string +} + +// UnmarshalJSON unmarshals a UpdateService object from the raw JSON. +func (cert *SSLCert) UnmarshalJSON(b []byte) error { + type temp SSLCert + var t struct { + temp + Actions struct { + Upload common.ActionTarget `json:"#SmcSSLCert.Upload"` + } + } + + err := json.Unmarshal(b, &t) + if err != nil { + return err + } + + *cert = SSLCert(t.temp) + cert.uploadTarget = t.Actions.Upload.Target + + return nil +} + +// GetSSLCert will get the SSLCert instance from the Redfish +// service. +func GetSSLCert(c common.Client, uri string) (*SSLCert, error) { + return common.GetObject[SSLCert](c, uri) +} + +// Upload installs an SSL cert. +// NOTE: This is probably not correct. The jsonschema reported by SMC does not +// include any parameters for this action. That seems very unlikely, so expect +// this to fail. +func (cert *SSLCert) Upload() error { + if cert.uploadTarget == "" { + return errors.New("upload is not supported by this system") + } + + return cert.Post(cert.uploadTarget, nil) +} + +type IPMIConfig struct { + common.Entity + + uploadTarget string + downloadTarget string +} + +// UnmarshalJSON unmarshals a UpdateService object from the raw JSON. +func (ipmi *IPMIConfig) UnmarshalJSON(b []byte) error { + type temp IPMIConfig + var t struct { + temp + Actions struct { + Upload common.ActionTarget `json:"#SmcIPMIConfig.Upload"` + Download common.ActionTarget `json:"#SmcIPMIConfig.Download"` + } + } + + err := json.Unmarshal(b, &t) + if err != nil { + return err + } + + *ipmi = IPMIConfig(t.temp) + ipmi.uploadTarget = t.Actions.Upload.Target + ipmi.downloadTarget = t.Actions.Download.Target + + return nil +} + +// GetIPMIConfig will get the IPMIConfig instance from the Redfish +// service. +func GetIPMIConfig(c common.Client, uri string) (*IPMIConfig, error) { + return common.GetObject[IPMIConfig](c, uri) +} + +// Upload restores a saved IPMI configuration. +// NOTE: This is probably not correct. The jsonschema reported by SMC does not +// include any parameters for this action. That seems very unlikely, so expect +// this to fail. +func (ipmi *IPMIConfig) Upload() error { + if ipmi.uploadTarget == "" { + return errors.New("upload is not supported by this system") + } + + return ipmi.Post(ipmi.uploadTarget, nil) +} + +// Download saves the current IPMI configuration. +// NOTE: This is probably not correct. The jsonschema reported by SMC does not +// include any parameters for this action. That seems very unlikely, so expect +// this to fail. +func (ipmi *IPMIConfig) Download() error { + if ipmi.downloadTarget == "" { + return errors.New("download is not supported by this system") + } + + return ipmi.Post(ipmi.downloadTarget, nil) +} + +// UpdateService is the license manager instance associated with the system. +type UpdateService struct { + redfish.UpdateService + + sslCert string + ipmiConfig string + + installTarget string +} + +// FromUpdateService gets the OEM instance of the UpdateService. +func FromUpdateService(updateService *redfish.UpdateService) (*UpdateService, error) { + us := UpdateService{ + UpdateService: *updateService, + } + + var t struct { + Actions struct { + Oem struct { + Install common.ActionTarget `json:"#SmcUpdateService.Install"` + } + } + Oem struct { + Supermicro struct { + SSLCert common.Link + IPMIConfig common.Link + } + } + } + + err := json.Unmarshal(updateService.RawData, &t) + if err != nil { + return nil, err + } + + us.sslCert = t.Oem.Supermicro.SSLCert.String() + us.ipmiConfig = t.Oem.Supermicro.IPMIConfig.String() + + us.installTarget = t.Actions.Oem.Install.Target + + return &us, nil +} + +// GetUpdateService will get a UpdateService instance from the service. +func GetUpdateService(c common.Client, uri string) (*UpdateService, error) { + return common.GetObject[UpdateService](c, uri) +} + +// ActivateLicense performs the ActivateLicense action of the UpdateService. +func (us *UpdateService) Install(targets, installOptions []string) error { + if us.installTarget == "" { + return errors.New("Install is not supported by this system") + } + + return us.Post(us.installTarget, map[string]any{ + "Targets": targets, + "InstallOptions": installOptions, + }) +} + +// SSLCert will get the SSLCert information from the service. +func (us *UpdateService) SSLCert() (*SSLCert, error) { + return GetSSLCert(us.GetClient(), us.sslCert) +} + +// IPMIConfig will get the IPMIConfig information from the service. +func (us *UpdateService) IPMIConfig() (*IPMIConfig, error) { + return GetIPMIConfig(us.GetClient(), us.ipmiConfig) +} diff --git a/oem/smc/updateservice_test.go b/oem/smc/updateservice_test.go new file mode 100644 index 00000000..d230282d --- /dev/null +++ b/oem/smc/updateservice_test.go @@ -0,0 +1,86 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package smc + +import ( + "encoding/json" + "testing" + + "github.com/stmcginnis/gofish/redfish" +) + +var updateServiceBody = `{ + "@odata.type": "#UpdateService.v1_8_4.UpdateService", + "@odata.id": "/redfish/v1/UpdateService", + "Id": "UpdateService", + "Name": "Update Service", + "Description": "Service for updating firmware and includes inventory of firmware", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "ServiceEnabled": true, + "MultipartHttpPushUri": "/redfish/v1/UpdateService/upload", + "FirmwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" + }, + "Actions": { + "Oem": { + "#SmcUpdateService.Install": { + "target": "/redfish/v1/UpdateService/Actions/Oem/SmcUpdateService.Install", + "@Redfish.ActionInfo": "/redfish/v1/UpdateService/Oem/Supermicro/InstallActionInfo" + } + }, + "#UpdateService.SimpleUpdate": { + "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate", + "@Redfish.ActionInfo": "/redfish/v1/UpdateService/SimpleUpdateActionInfo" + }, + "#UpdateService.StartUpdate": { + "target": "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate" + } + }, + "Oem": { + "Supermicro": { + "@odata.type": "#SmcUpdateServiceExtensions.v1_0_0.UpdateService", + "SSLCert": { + "@odata.id": "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert" + }, + "IPMIConfig": { + "@odata.id": "/redfish/v1/UpdateService/Oem/Supermicro/IPMIConfig" + } + } + }, + "@odata.etag": "\"e9b94401dae9992fef2e71ef30cbcfdc\"" +}` + +// TestSmcUpdateServiceOem tests the parsing of the UpdateService oem field +func TestSmcUpdateServiceOem(t *testing.T) { + us := &redfish.UpdateService{} + if err := json.Unmarshal([]byte(updateServiceBody), us); err != nil { + t.Fatalf("error decoding json: %v", err) + } + + updateService, err := FromUpdateService(us) + if err != nil { + t.Fatalf("error getting oem object: %v", err) + } + + if updateService.ID != "UpdateService" { + t.Errorf("unexpected ID: %s", updateService.ID) + } + + if updateService.installTarget != "/redfish/v1/UpdateService/Actions/Oem/SmcUpdateService.Install" { + t.Errorf("unexpected install target: %s", updateService.installTarget) + } + + if updateService.sslCert != "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert" { + t.Errorf("unexpected ssl cert link: %s", updateService.installTarget) + } + + if updateService.ipmiConfig != "/redfish/v1/UpdateService/Oem/Supermicro/IPMIConfig" { + t.Errorf("unexpected ipmi config link: %s", updateService.installTarget) + } +} diff --git a/redfish/updateservice.go b/redfish/updateservice.go index c0173326..525220a0 100644 --- a/redfish/updateservice.go +++ b/redfish/updateservice.go @@ -193,8 +193,8 @@ type UpdateService struct { // this object contains shall conform to the Redfish Specification // described requirements. Oem json.RawMessage - // rawData holds the original serialized JSON so we can compare updates. - rawData []byte + // RawData holds the original serialized JSON so we can compare updates. + RawData []byte } // UnmarshalJSON unmarshals a UpdateService object from the raw JSON. @@ -243,7 +243,7 @@ func (updateService *UpdateService) UnmarshalJSON(b []byte) error { updateService.TransferProtocol = t.Actions.SimpleUpdate.AllowableValues updateService.OemActions = t.Actions.Oem - updateService.rawData = b + updateService.RawData = b return nil }