Skip to content

Commit

Permalink
Add SSLCert Upload to Supermicro UpdateService
Browse files Browse the repository at this point in the history
  • Loading branch information
lamaral committed Oct 21, 2024
1 parent 58a9e90 commit aa3c0ca
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 16 deletions.
28 changes: 19 additions & 9 deletions oem/smc/updateservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package smc
import (
"encoding/json"
"errors"
"io"

"github.com/stmcginnis/gofish/common"
"github.com/stmcginnis/gofish/redfish"
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
211 changes: 204 additions & 7 deletions oem/smc/updateservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
"[email protected]": [
"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" {
Expand All @@ -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)
}
}

0 comments on commit aa3c0ca

Please sign in to comment.