Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSLCert Upload to Supermicro UpdateService #378

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-----
lamaral marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}