Skip to content

Commit aa3c0ca

Browse files
committed
Add SSLCert Upload to Supermicro UpdateService
1 parent 58a9e90 commit aa3c0ca

File tree

2 files changed

+223
-16
lines changed

2 files changed

+223
-16
lines changed

oem/smc/updateservice.go

+19-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package smc
77
import (
88
"encoding/json"
99
"errors"
10+
"io"
1011

1112
"github.com/stmcginnis/gofish/common"
1213
"github.com/stmcginnis/gofish/redfish"
@@ -15,13 +16,16 @@ import (
1516
type SSLCert struct {
1617
common.Entity
1718

18-
ValidFrom string
19+
// GoodThrough is the certificate expiration date.
1920
GoodThrough string `json:"GoodTHRU"`
21+
// ValidFrom is the certificate start date. It's misspelled as VaildFrom in the schema.
22+
ValidFrom string `json:"VaildFrom"`
2023

24+
// uploadTarget is the URL to upload certificates to.
2125
uploadTarget string
2226
}
2327

24-
// UnmarshalJSON unmarshals a UpdateService object from the raw JSON.
28+
// UnmarshalJSON unmarshals a SSLCert object from the raw JSON.
2529
func (cert *SSLCert) UnmarshalJSON(b []byte) error {
2630
type temp SSLCert
2731
var t struct {
@@ -48,16 +52,22 @@ func GetSSLCert(c common.Client, uri string) (*SSLCert, error) {
4852
return common.GetObject[SSLCert](c, uri)
4953
}
5054

51-
// Upload installs an SSL cert.
52-
// NOTE: This is probably not correct. The jsonschema reported by SMC does not
53-
// include any parameters for this action. That seems very unlikely, so expect
54-
// this to fail.
55-
func (cert *SSLCert) Upload() error {
55+
// Upload will update the SSL certificate on the BMC with the provided certificate and key.
56+
func (cert *SSLCert) Upload(certFile, keyFile io.Reader) error {
5657
if cert.uploadTarget == "" {
5758
return errors.New("upload is not supported by this system")
5859
}
5960

60-
return cert.Post(cert.uploadTarget, nil)
61+
payload := make(map[string]io.Reader)
62+
payload["cert_file"] = certFile
63+
payload["key_file"] = keyFile
64+
65+
resp, err := cert.GetClient().PostMultipart(cert.uploadTarget, payload)
66+
if err != nil {
67+
return err
68+
}
69+
70+
return resp.Body.Close()
6171
}
6272

6373
type IPMIConfig struct {
@@ -168,7 +178,7 @@ func GetUpdateService(c common.Client, uri string) (*UpdateService, error) {
168178
return common.GetObject[UpdateService](c, uri)
169179
}
170180

171-
// Install performs an install of an update.
181+
// Install performs the installation of an update.
172182
func (us *UpdateService) Install(targets, installOptions []string) error {
173183
if us.installTarget == "" {
174184
return errors.New("install is not supported by this system")

oem/smc/updateservice_test.go

+204-7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,92 @@
55
package smc
66

77
import (
8-
"encoding/json"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
911
"testing"
1012

11-
"github.com/stmcginnis/gofish/redfish"
13+
"github.com/stmcginnis/gofish"
14+
"github.com/stmcginnis/gofish/common"
1215
)
1316

17+
const serviceRootBody = `{
18+
"@odata.type": "#ServiceRoot.v1_9_0.ServiceRoot",
19+
"@odata.id": "/redfish/v1",
20+
"Id": "ServiceRoot",
21+
"Name": "Root Service",
22+
"RedfishVersion": "1.11.0",
23+
"UUID": "00000000-0000-0000-0000-3CECEFE32D23",
24+
"Systems": {
25+
"@odata.id": "/redfish/v1/Systems"
26+
},
27+
"Chassis": {
28+
"@odata.id": "/redfish/v1/Chassis"
29+
},
30+
"Managers": {
31+
"@odata.id": "/redfish/v1/Managers"
32+
},
33+
"Tasks": {
34+
"@odata.id": "/redfish/v1/TaskService"
35+
},
36+
"SessionService": {
37+
"@odata.id": "/redfish/v1/SessionService"
38+
},
39+
"AccountService": {
40+
"@odata.id": "/redfish/v1/AccountService"
41+
},
42+
"EventService": {
43+
"@odata.id": "/redfish/v1/EventService"
44+
},
45+
"UpdateService": {
46+
"@odata.id": "/redfish/v1/UpdateService"
47+
},
48+
"CertificateService": {
49+
"@odata.id": "/redfish/v1/CertificateService"
50+
},
51+
"Registries": {
52+
"@odata.id": "/redfish/v1/Registries"
53+
},
54+
"JsonSchemas": {
55+
"@odata.id": "/redfish/v1/JsonSchemas"
56+
},
57+
"TelemetryService": {
58+
"@odata.id": "/redfish/v1/TelemetryService"
59+
},
60+
"Product": null,
61+
"Links": {
62+
"Sessions": {
63+
"@odata.id": "/redfish/v1/SessionService/Sessions"
64+
}
65+
},
66+
"Oem": {
67+
"Supermicro": {
68+
"DumpService": {
69+
"@odata.id": "/redfish/v1/Oem/Supermicro/DumpService"
70+
}
71+
}
72+
},
73+
"ProtocolFeaturesSupported": {
74+
"FilterQuery": true,
75+
"SelectQuery": true,
76+
"ExcerptQuery": false,
77+
"OnlyMemberQuery": false,
78+
"DeepOperations": {
79+
"DeepPATCH": false,
80+
"DeepPOST": false,
81+
"MaxLevels": 1
82+
},
83+
"ExpandQuery": {
84+
"Links": true,
85+
"NoLinks": true,
86+
"ExpandAll": true,
87+
"Levels": true,
88+
"MaxLevels": 2
89+
}
90+
},
91+
"@odata.etag": "\"1a10733cff76c5506e6903b25ab88e55\""
92+
}`
93+
1494
var updateServiceBody = `{
1595
"@odata.type": "#UpdateService.v1_8_4.UpdateService",
1696
"@odata.id": "/redfish/v1/UpdateService",
@@ -56,16 +136,123 @@ var updateServiceBody = `{
56136
"@odata.etag": "\"e9b94401dae9992fef2e71ef30cbcfdc\""
57137
}`
58138

139+
const smcSSLCertBody = `{
140+
"@odata.type": "#SSLCert.v1_0_0.SSLCert",
141+
"@odata.id": "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert",
142+
"Id": "SSLCert",
143+
"Name": "SSLCert",
144+
"VaildFrom": "Oct 9 11:15:00 2024 GMT",
145+
"GoodTHRU": "Oct 9 11:15:00 2025 GMT",
146+
"Actions": {
147+
"#SmcSSLCert.Upload": {
148+
"target": "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert/Actions/SmcSSLCert.Upload",
149+
150+
"cert_file",
151+
"key_file"
152+
]
153+
}
154+
},
155+
"@odata.etag": "\"e4be24decdd8b293984fb26e1a78e62a\""
156+
}`
157+
158+
const smcSSLCertUploadResponse = `{
159+
"Success": {
160+
"code": "Base.v1_10_3.Success",
161+
"message": "Successfully Completed Request. See ExtendedInfo for more information.",
162+
"@Message.ExtendedInfo": [
163+
{
164+
"MessageId": "SMC.1.0.OemSslcertUploaded",
165+
"Severity": "OK",
166+
"Resolution": "No resolution was required.",
167+
"Message": "SSL certificate and private key were successfully uploaded.",
168+
"MessageArgs": [
169+
""
170+
],
171+
"RelatedProperties": [
172+
""
173+
]
174+
}
175+
]
176+
}
177+
}`
178+
179+
const sslCertFile = `-----BEGIN CERTIFICATE-----
180+
MIIDpDCCAoygAwIBAgIUIf
181+
-----END CERTIFICATE-----`
182+
183+
//nolint:gosec
184+
const sslKeyFile = `-----BEGIN RSA PRIVATE KEY-----
185+
MIIEpAIBAAKCAQEAz
186+
-----END RSA PRIVATE KEY-----`
187+
59188
// TestSmcUpdateService tests the parsing of the UpdateService oem field
60189
func TestSmcUpdateService(t *testing.T) {
61-
us := &redfish.UpdateService{}
62-
if err := json.Unmarshal([]byte(updateServiceBody), us); err != nil {
63-
t.Fatalf("error decoding json: %v", err)
190+
const redfishBaseURL = "/redfish/v1/"
191+
var (
192+
c common.Client
193+
err error
194+
requestCounter int // this counter is used to verify that the received requests are in the expected order
195+
)
196+
197+
// Start a local HTTP server
198+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
199+
if req.Method == http.MethodGet &&
200+
req.URL.String() == redfishBaseURL &&
201+
requestCounter < 2 { // ServiceRoot
202+
contentType := req.Header.Get("Content-Type")
203+
if contentType != "application/json" {
204+
t.Errorf("gofish connect sent wrong header. Content-Type:"+
205+
" is %v and not expected application/json", contentType)
206+
}
207+
208+
requestCounter++
209+
210+
// Send response to be tested
211+
rw.WriteHeader(http.StatusOK)
212+
rw.Header().Set("Content-Type", "application/json")
213+
214+
rw.Write([]byte(serviceRootBody)) //nolint:errcheck
215+
} else if req.Method == http.MethodGet && // Get event service
216+
req.URL.String() == "/redfish/v1/UpdateService" &&
217+
requestCounter == 2 {
218+
requestCounter++
219+
rw.Write([]byte(updateServiceBody)) //nolint:errcheck
220+
} else if req.Method == http.MethodGet &&
221+
req.URL.String() == "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert" &&
222+
requestCounter == 3 {
223+
requestCounter++
224+
rw.Write([]byte(smcSSLCertBody)) //nolint:errcheck
225+
} else if req.Method == http.MethodPost && // SubmitTestEvent
226+
req.URL.String() == "/redfish/v1/UpdateService/Oem/Supermicro/SSLCert/Actions/SmcSSLCert.Upload" &&
227+
requestCounter == 4 {
228+
// TODO: Actually check if the request body is correct
229+
requestCounter++
230+
rw.Write([]byte(smcSSLCertUploadResponse)) //nolint:errcheck
231+
} else {
232+
t.Errorf("mock got unexpected %v request to path %v while request counter is %v",
233+
req.Method, req.URL.String(), requestCounter)
234+
}
235+
}))
236+
237+
// Close the server when test finishes
238+
defer server.Close()
239+
240+
c, err = gofish.Connect(gofish.ClientConfig{Endpoint: server.URL, HTTPClient: server.Client()})
241+
if err != nil {
242+
t.Errorf("failed to establish client to mock http server due to: %v", err)
64243
}
65244

66-
updateService, err := FromUpdateService(us)
245+
serviceRoot, err := gofish.ServiceRoot(c)
246+
if err != nil {
247+
t.Errorf("failed to get redfish service root due to: %v", err)
248+
}
249+
origUpdateService, err := serviceRoot.UpdateService()
250+
if err != nil {
251+
t.Errorf("failed to get update service due to: %v", err)
252+
}
253+
updateService, err := FromUpdateService(origUpdateService)
67254
if err != nil {
68-
t.Fatalf("error getting oem object: %v", err)
255+
t.Errorf("error getting OEM object: %v", err)
69256
}
70257

71258
if updateService.ID != "UpdateService" {
@@ -83,4 +270,14 @@ func TestSmcUpdateService(t *testing.T) {
83270
if updateService.ipmiConfig != "/redfish/v1/UpdateService/Oem/Supermicro/IPMIConfig" {
84271
t.Errorf("unexpected ipmi config link: %s", updateService.installTarget)
85272
}
273+
274+
cert, err := updateService.SSLCert()
275+
if err != nil {
276+
t.Errorf("Failed to get SSL certificate due to: %v", err)
277+
}
278+
279+
err = cert.Upload(strings.NewReader(sslCertFile), strings.NewReader(sslKeyFile))
280+
if err != nil {
281+
t.Errorf("Failed to upload SSL certificate due to: %v", err)
282+
}
86283
}

0 commit comments

Comments
 (0)