diff --git a/oem/smc/updateservice.go b/oem/smc/updateservice.go index a1094eca..d2859c88 100644 --- a/oem/smc/updateservice.go +++ b/oem/smc/updateservice.go @@ -7,6 +7,7 @@ package smc import ( "encoding/json" "errors" + "io" "github.com/stmcginnis/gofish/common" "github.com/stmcginnis/gofish/redfish" @@ -15,13 +16,16 @@ import ( type SSLCert struct { common.Entity - ValidFrom string + // GoodThrough is the certificate expiration date. GoodThrough string `json:"GoodTHRU"` + // ValidFrom is the certificate start date. It's misspelled as VaildFrom in the schema. + ValidFrom string `json:"VaildFrom"` + // uploadTarget is the URL to upload certificates to. uploadTarget string } -// UnmarshalJSON unmarshals a UpdateService object from the raw JSON. +// UnmarshalJSON unmarshals a SSLCert object from the raw JSON. func (cert *SSLCert) UnmarshalJSON(b []byte) error { type temp SSLCert var t struct { @@ -48,16 +52,22 @@ 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 { +// Upload will update the SSL certificate on the BMC with the provided certificate and key. +func (cert *SSLCert) Upload(certFile, keyFile io.Reader) error { if cert.uploadTarget == "" { return errors.New("upload is not supported by this system") } - return cert.Post(cert.uploadTarget, nil) + payload := make(map[string]io.Reader) + payload["cert_file"] = certFile + payload["key_file"] = keyFile + + resp, err := cert.GetClient().PostMultipart(cert.uploadTarget, payload) + if err != nil { + return err + } + + return resp.Body.Close() } type IPMIConfig struct { @@ -168,7 +178,7 @@ func GetUpdateService(c common.Client, uri string) (*UpdateService, error) { return common.GetObject[UpdateService](c, uri) } -// Install performs an install of an update. +// Install performs the installation of an update. func (us *UpdateService) Install(targets, installOptions []string) error { if us.installTarget == "" { return errors.New("install is not supported by this system") diff --git a/oem/smc/updateservice_test.go b/oem/smc/updateservice_test.go index fbaafe6b..8fa4eebd 100644 --- a/oem/smc/updateservice_test.go +++ b/oem/smc/updateservice_test.go @@ -5,12 +5,92 @@ package smc import ( - "encoding/json" + "net/http" + "net/http/httptest" + "strings" "testing" - "github.com/stmcginnis/gofish/redfish" + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" ) +const serviceRootBody = `{ + "@odata.type": "#ServiceRoot.v1_9_0.ServiceRoot", + "@odata.id": "/redfish/v1", + "Id": "ServiceRoot", + "Name": "Root Service", + "RedfishVersion": "1.11.0", + "UUID": "00000000-0000-0000-0000-3CECEFE32D23", + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "Product": null, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Oem": { + "Supermicro": { + "DumpService": { + "@odata.id": "/redfish/v1/Oem/Supermicro/DumpService" + } + } + }, + "ProtocolFeaturesSupported": { + "FilterQuery": true, + "SelectQuery": true, + "ExcerptQuery": false, + "OnlyMemberQuery": false, + "DeepOperations": { + "DeepPATCH": false, + "DeepPOST": false, + "MaxLevels": 1 + }, + "ExpandQuery": { + "Links": true, + "NoLinks": true, + "ExpandAll": true, + "Levels": true, + "MaxLevels": 2 + } + }, + "@odata.etag": "\"1a10733cff76c5506e6903b25ab88e55\"" +}` + var updateServiceBody = `{ "@odata.type": "#UpdateService.v1_8_4.UpdateService", "@odata.id": "/redfish/v1/UpdateService", @@ -56,16 +136,123 @@ var updateServiceBody = `{ "@odata.etag": "\"e9b94401dae9992fef2e71ef30cbcfdc\"" }` +const smcSSLCertBody = `{ + "@odata.type": "#SSLCert.v1_0_0.SSLCert", + "@odata.id": "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert", + "Id": "SSLCert", + "Name": "SSLCert", + "VaildFrom": "Oct 9 11:15:00 2024 GMT", + "GoodTHRU": "Oct 9 11:15:00 2025 GMT", + "Actions": { + "#SmcSSLCert.Upload": { + "target": "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert/Actions/SmcSSLCert.Upload", + "UploadKeyWords@Redfish.AllowableValues": [ + "cert_file", + "key_file" + ] + } + }, + "@odata.etag": "\"e4be24decdd8b293984fb26e1a78e62a\"" +}` + +const smcSSLCertUploadResponse = `{ + "Success": { + "code": "Base.v1_10_3.Success", + "message": "Successfully Completed Request. See ExtendedInfo for more information.", + "@Message.ExtendedInfo": [ + { + "MessageId": "SMC.1.0.OemSslcertUploaded", + "Severity": "OK", + "Resolution": "No resolution was required.", + "Message": "SSL certificate and private key were successfully uploaded.", + "MessageArgs": [ + "" + ], + "RelatedProperties": [ + "" + ] + } + ] + } +}` + +const sslCertFile = `-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIUIf +-----END CERTIFICATE-----` + +//nolint:gosec +const sslKeyFile = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAz +-----END RSA PRIVATE KEY-----` + // TestSmcUpdateService tests the parsing of the UpdateService oem field func TestSmcUpdateService(t *testing.T) { - us := &redfish.UpdateService{} - if err := json.Unmarshal([]byte(updateServiceBody), us); err != nil { - t.Fatalf("error decoding json: %v", err) + const redfishBaseURL = "/redfish/v1/" + var ( + c common.Client + err error + requestCounter int // this counter is used to verify that the received requests are in the expected order + ) + + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet && + req.URL.String() == redfishBaseURL && + requestCounter < 2 { // ServiceRoot + contentType := req.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("gofish connect sent wrong header. Content-Type:"+ + " is %v and not expected application/json", contentType) + } + + requestCounter++ + + // Send response to be tested + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", "application/json") + + rw.Write([]byte(serviceRootBody)) //nolint:errcheck + } else if req.Method == http.MethodGet && // Get event service + req.URL.String() == "/redfish/v1/UpdateService" && + requestCounter == 2 { + requestCounter++ + rw.Write([]byte(updateServiceBody)) //nolint:errcheck + } else if req.Method == http.MethodGet && + req.URL.String() == "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert" && + requestCounter == 3 { + requestCounter++ + rw.Write([]byte(smcSSLCertBody)) //nolint:errcheck + } else if req.Method == http.MethodPost && // SubmitTestEvent + req.URL.String() == "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert/Actions/SmcSSLCert.Upload" && + requestCounter == 4 { + // TODO: Actually check if the request body is correct + requestCounter++ + rw.Write([]byte(smcSSLCertUploadResponse)) //nolint:errcheck + } else { + t.Errorf("mock got unexpected %v request to path %v while request counter is %v", + req.Method, req.URL.String(), requestCounter) + } + })) + + // Close the server when test finishes + defer server.Close() + + c, err = gofish.Connect(gofish.ClientConfig{Endpoint: server.URL, HTTPClient: server.Client()}) + if err != nil { + t.Errorf("failed to establish client to mock http server due to: %v", err) } - updateService, err := FromUpdateService(us) + serviceRoot, err := gofish.ServiceRoot(c) + if err != nil { + t.Errorf("failed to get redfish service root due to: %v", err) + } + origUpdateService, err := serviceRoot.UpdateService() + if err != nil { + t.Errorf("failed to get update service due to: %v", err) + } + updateService, err := FromUpdateService(origUpdateService) if err != nil { - t.Fatalf("error getting oem object: %v", err) + t.Errorf("error getting OEM object: %v", err) } if updateService.ID != "UpdateService" { @@ -83,4 +270,14 @@ func TestSmcUpdateService(t *testing.T) { if updateService.ipmiConfig != "/redfish/v1/UpdateService/Oem/Supermicro/IPMIConfig" { t.Errorf("unexpected ipmi config link: %s", updateService.installTarget) } + + cert, err := updateService.SSLCert() + if err != nil { + t.Errorf("Failed to get SSL certificate due to: %v", err) + } + + err = cert.Upload(strings.NewReader(sslCertFile), strings.NewReader(sslKeyFile)) + if err != nil { + t.Errorf("Failed to upload SSL certificate due to: %v", err) + } }