diff --git a/bmc/firmware.go b/bmc/firmware.go index 0ae01a4a..e235bf20 100644 --- a/bmc/firmware.go +++ b/bmc/firmware.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "os" "github.com/bmc-toolbox/bmclib/v2/constants" bconsts "github.com/bmc-toolbox/bmclib/v2/constants" @@ -314,7 +315,7 @@ func firmwareInstallSteps(ctx context.Context, component string, generic []firmw } type FirmwareUploader interface { - FirmwareUpload(ctx context.Context, component string, reader io.Reader) (uploadVerifyTaskID string, err error) + FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) } // firmwareUploaderProvider is an internal struct to correlate an implementation/provider and its name @@ -324,7 +325,7 @@ type firmwareUploaderProvider struct { } // FirmwareUploaderFromInterfaces identifies implementations of the FirmwareUploader interface and passes the found implementations to the firmwareUpload() wrapper. -func FirmwareUploadFromInterfaces(ctx context.Context, component string, reader io.Reader, generic []interface{}) (taskID string, metadata Metadata, err error) { +func FirmwareUploadFromInterfaces(ctx context.Context, component string, file *os.File, generic []interface{}) (taskID string, metadata Metadata, err error) { implementations := make([]firmwareUploaderProvider, 0) for _, elem := range generic { temp := firmwareUploaderProvider{name: getProviderName(elem)} @@ -347,10 +348,10 @@ func FirmwareUploadFromInterfaces(ctx context.Context, component string, reader ) } - return firmwareUpload(ctx, component, reader, implementations) + return firmwareUpload(ctx, component, file, implementations) } -func firmwareUpload(ctx context.Context, component string, reader io.Reader, generic []firmwareUploaderProvider) (taskID string, metadata Metadata, err error) { +func firmwareUpload(ctx context.Context, component string, file *os.File, generic []firmwareUploaderProvider) (taskID string, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range generic { @@ -364,7 +365,7 @@ func firmwareUpload(ctx context.Context, component string, reader io.Reader, gen return taskID, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) - taskID, vErr := elem.FirmwareUpload(ctx, component, reader) + taskID, vErr := elem.FirmwareUpload(ctx, component, file) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go index ea0f8604..454db942 100644 --- a/bmc/firmware_test.go +++ b/bmc/firmware_test.go @@ -3,6 +3,7 @@ package bmc import ( "context" "io" + "os" "testing" "time" @@ -304,7 +305,7 @@ type firmwareUploadTester struct { returnError error } -func (f *firmwareUploadTester) FirmwareUpload(ctx context.Context, component string, reader io.Reader) (uploadVerifyTaskID string, err error) { +func (f *firmwareUploadTester) FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) { return f.returnTaskID, f.returnError } @@ -316,7 +317,7 @@ func TestFirmwareUpload(t *testing.T) { testCases := []struct { testName string component string - reader io.Reader + file *os.File returnTaskID string returnError error ctxTimeout time.Duration @@ -336,7 +337,7 @@ func TestFirmwareUpload(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() - taskID, metadata, err := firmwareUpload(ctx, tc.component, tc.reader, []firmwareUploaderProvider{{tc.providerName, &testImplementation}}) + taskID, metadata, err := firmwareUpload(ctx, tc.component, tc.file, []firmwareUploaderProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return diff --git a/client.go b/client.go index a238bb17..a1917543 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "sync" "time" @@ -479,8 +480,8 @@ func (c *Client) FirmwareInstallSteps(ctx context.Context, component string) (ac } // FirmwareUpload just uploads the firmware for install, it returns a task ID to verify the upload status. -func (c *Client) FirmwareUpload(ctx context.Context, component string, reader io.Reader) (uploadVerifyTaskID string, err error) { - uploadVerifyTaskID, metadata, err := bmc.FirmwareUploadFromInterfaces(ctx, component, reader, c.Registry.GetDriverInterfaces()) +func (c *Client) FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) { + uploadVerifyTaskID, metadata, err := bmc.FirmwareUploadFromInterfaces(ctx, component, file, c.Registry.GetDriverInterfaces()) c.setMetadata(metadata) return uploadVerifyTaskID, err } diff --git a/go.mod b/go.mod index adef5785..33cbe024 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.31.0 github.com/sirupsen/logrus v1.9.3 - github.com/stmcginnis/gofish v0.14.1-0.20230920133920-77490fd98fa2 + github.com/stmcginnis/gofish v0.14.1-0.20231018151402-dddaff9168fb github.com/stretchr/testify v1.8.0 go.uber.org/goleak v1.2.1 golang.org/x/crypto v0.14.0 diff --git a/go.sum b/go.sum index 4fb41411..85dcade2 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 h1:t95Grn2 github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230/go.mod h1:t2EzW1qybnPDQ3LR/GgeF0GOzHUXT5IVMLP2gkW1cmc= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 h1:a0MBqYm44o0NcthLKCljZHe1mxlN6oahCQHHThnSwB4= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22/go.mod h1:/B7V22rcz4860iDqstGvia/2+IYWXf3/JdQCVd/1D2A= -github.com/bmc-toolbox/common v0.0.0-20230220061748-93ff001f4a1d h1:cQ30Wa8mhLzK1TSOG+g3FlneIsXtFgun61mmPwVPmD0= -github.com/bmc-toolbox/common v0.0.0-20230220061748-93ff001f4a1d/go.mod h1:SY//n1PJjZfbFbmAsB6GvEKbc7UXz3d30s3kWxfJQ/c= github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a h1:SjtoU9dE3bYfYnPXODCunMztjoDgnE3DVJCPLBqwz6Q= github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a/go.mod h1:SY//n1PJjZfbFbmAsB6GvEKbc7UXz3d30s3kWxfJQ/c= github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM= @@ -20,8 +18,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= @@ -45,12 +41,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -60,18 +52,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stmcginnis/gofish v0.14.1-0.20230920133920-77490fd98fa2 h1:R0N4G786trm1dHBwJftzaupRrwhY1T+rBrTBC8eqiRQ= -github.com/stmcginnis/gofish v0.14.1-0.20230920133920-77490fd98fa2/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI= +github.com/stmcginnis/gofish v0.14.1-0.20231018151402-dddaff9168fb h1:+BpzUuFIEAs71bTshedsUHAAq21VZWvuokbN9ABEQeQ= +github.com/stmcginnis/gofish v0.14.1-0.20231018151402-dddaff9168fb/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -82,31 +71,20 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20230127130021-4ca2cb1a16b7 h1:o7Ps2IYdzLRolS9/nadqeMSHpa9k8pu8u+VKBFUG7cQ= -golang.org/x/exp v0.0.0-20230127130021-4ca2cb1a16b7/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/redfishwrapper/firmware.go b/internal/redfishwrapper/firmware.go index 22815fd5..00c6031f 100644 --- a/internal/redfishwrapper/firmware.go +++ b/internal/redfishwrapper/firmware.go @@ -41,13 +41,7 @@ type RedfishUpdateServiceParameters struct { } // FirmwareUpload uploads and initiates the firmware install process -func (c *Client) FirmwareUpload(ctx context.Context, reader io.Reader, params *RedfishUpdateServiceParameters) (taskID string, err error) { - // limit to *os.File until theres a need for other types of readers - updateFile, ok := reader.(*os.File) - if !ok { - return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, "method expects an *os.File object") - } - +func (c *Client) FirmwareUpload(ctx context.Context, updateFile *os.File, params *RedfishUpdateServiceParameters) (taskID string, err error) { parameters, err := json.Marshal(params) if err != nil { return "", errors.Wrap(errUpdateParams, err.Error()) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index b384a8f9..8606bfdd 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -20,7 +20,6 @@ import ( "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" - "github.com/bmc-toolbox/bmclib/v2/internal" ) var ( diff --git a/providers/supermicro/docs/20230907_2-RedfishRefGuide.pdf b/providers/supermicro/docs/20230907_2-RedfishRefGuide.pdf new file mode 100644 index 00000000..17a6cd8e Binary files /dev/null and b/providers/supermicro/docs/20230907_2-RedfishRefGuide.pdf differ diff --git a/providers/supermicro/docs/x12.md b/providers/supermicro/docs/x12.md new file mode 100644 index 00000000..b487440f --- /dev/null +++ b/providers/supermicro/docs/x12.md @@ -0,0 +1,118 @@ +curl 'https://10.251.153.157/redfish/v1/UpdateService/upload' \ + -H 'Accept: */*' \ + -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \ + -H 'CSRF_TOKEN: p9lTd1+h0qsz/inooljtRbrja+1/z6nBRLuAKV6JJkM' \ + -H 'Connection: keep-alive' \ + -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytykIB3S8fDkno3cP' \ + -H 'Cookie: SID=1rnGhJ9HoMI6JpP' \ + -H 'Origin: https://10.251.153.157' \ + -H 'Referer: https://10.251.153.157/cgi/url_redirect.cgi?url_name=topmenu' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-origin' \ + -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' \ + -H 'X-Auth-Token: o5jk3881vldnk3hdkn20wu5kg8brl18r' \ + -H 'X-Requested-With: XMLHttpRequest' \ + -H 'sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + --data-raw $'------WebKitFormBoundarytykIB3S8fDkno3cP\r\nContent-Disposition: form-data; name="UpdateParameters"\r\n\r\n{"Targets":["/redfish/v1/Managers/1"],"@Redfish.OperationApplyTime":"OnStartUpdateRequest","Oem":{"Supermicro":{"BMC":{"PreserveCfg":true,"PreserveSdr":true,"PreserveSsl":true}}}}\r\n------WebKitFormBoundarytykIB3S8fDkno3cP\r\nContent-Disposition: form-data; name="UpdateFile"; filename="BMC_X12AST2600-F201MS_20220627_1.13.04_STDsp.bin"\r\nContent-Type: application/macbinary\r\n\r\n\r\n------WebKitFormBoundarytykIB3S8fDkno3cP--\r\n' \ + --compressed + + +// install parameters +{"Targets":["/redfish/v1/Managers/1"],"@Redfish.OperationApplyTime":"OnStartUpdateRequest","Oem":{"Supermicro":{"BMC":{"PreserveCfg":true,"PreserveSdr":true,"PreserveSsl":true}}}} + + ## look for task with name "BMC Verify" + +❯ curl 'https://10.251.153.157/redfish/v1/TaskService/Tasks/1' \ + -H 'Accept: application/json, text/javascript, */*; q=0.01' \ + -H 'CSRF_TOKEN: 10QMfkMegOzCe/WZZARLcs0cpxdDif8tSJcg5ZEnqVw' \ + -H 'Connection: keep-alive' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: SID=1rnGhJ9HoMI6JpP' \ + -H 'X-Auth-Token: o5jk3881vldnk3hdkn20wu5kg8brl18r' \ + --compressed \ + --insecure + + + {"@odata.type":"#Task.v1_4_3.Task","@odata.id":"/redfish/v1/TaskService/Tasks/1","Id":"1","Name":"BMC Verify","TaskState":"Completed","StartTime":"2023-10-06T07:53:25+00:00","EndTime":"2023-10-06T07:53:31+00:00","PercentComplete":100,"HidePayload":true,"TaskMonitor":"/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh","TaskStatus":"OK","Messages":[{"MessageId":"","RelatedProperties":[""],"Message":"","MessageArgs":[""],"Severity":""}],"Oem":{}} + + ## look for task with "BMC Update" + + { + "@odata.type": "#Task.v1_4_3.Task", + "@odata.id": "/redfish/v1/TaskService/Tasks/2", + "Id": "2", + "Name": "BMC Update", + "TaskState": "Running", + "StartTime": "2023-10-09T05:42:25+00:00", + "PercentComplete": 2, + "HidePayload": true, + "TaskMonitor": "/redfish/v1/TaskMonitor/MaiRrV41mtzxlYvKWrO72tK0LK0e1zL", + "TaskStatus": "OK", + "Messages": [ + { + "MessageId": "", + "RelatedProperties": [ + "" + ], + "Message": "", + "MessageArgs": [ + "" + ], + "Severity": "" + } + ], + "Oem": {} +} + + +curl 'https://10.251.153.157/cgi/upgrade_process.cgi' \ + -H 'Accept: */*' \ + -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \ + -H 'CSRF_TOKEN: +5/2t9ZcRuEzRg6MbTU2/j5Ils1VM2zf7uVImW/wVMI' \ + -H 'Connection: keep-alive' \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ + -H 'Cookie: SID=qK4ZDz2cNet9nor' \ + -H 'Origin: https://10.251.153.157' \ + -H 'Referer: https://10.251.153.157/cgi/url_redirect.cgi?url_name=topmenu' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-origin' \ + -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' \ + -H 'X-Requested-With: XMLHttpRequest' \ + -H 'sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + --data-raw 'fwtype=255' \ + --compressed \ + --insecure + + + { + "@odata.type": "#Task.v1_4_3.Task", + "@odata.id": "/redfish/v1/TaskService/Tasks/2", + "Id": "2", + "Name": "BMC Update", + "TaskState": "Running", + "StartTime": "2023-10-13T13:27:51+00:00", + "PercentComplete": 5, + "HidePayload": true, + "TaskMonitor": "/redfish/v1/TaskMonitor/MaiRrV41mtzxlYvKWrO72tK0LK0e1zL", + "TaskStatus": "OK", + "Messages": [ + { + "MessageId": "", + "RelatedProperties": [ + "" + ], + "Message": "", + "MessageArgs": [ + "" + ], + "Severity": "" + } + ], + "Oem": {} +} \ No newline at end of file diff --git a/providers/supermicro/errors.go b/providers/supermicro/errors.go index 14096bb9..c2dcf057 100644 --- a/providers/supermicro/errors.go +++ b/providers/supermicro/errors.go @@ -8,7 +8,10 @@ import ( ) var ( - ErrQueryFRUInfo = errors.New("FRU information query returned error") + ErrQueryFRUInfo = errors.New("FRU information query returned error") + ErrXMLAPIUnsupported = errors.New("XML API is unsupported") + ErrModelUnknown = errors.New("Model number unknown") + ErrModelUnsupported = errors.New("Model not supported") ) type UnexpectedResponseError struct { diff --git a/providers/supermicro/firmware.go b/providers/supermicro/firmware.go index 2d8e5ec6..68329082 100644 --- a/providers/supermicro/firmware.go +++ b/providers/supermicro/firmware.go @@ -2,80 +2,22 @@ package supermicro import ( "context" - "io" "os" "strings" "time" + "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" - "github.com/bmc-toolbox/common" "github.com/pkg/errors" ) -// FirmwareInstall uploads and initiates firmware update for the component -func (c *Client) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (jobID string, err error) { - if err := c.deviceSupported(ctx); err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - - var size int64 - if file, ok := reader.(*os.File); ok { - finfo, err := file.Stat() - if err != nil { - c.log.V(2).Error(err, "unable to determine file size") - } - - size = finfo.Size() - } - - // expect atleast 10 minutes left in the deadline to proceed with the update - d, _ := ctx.Deadline() - if time.Until(d) < 10*time.Minute { - return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) - } - - component = strings.ToUpper(component) - - switch component { - case common.SlugBIOS: - err = c.firmwareInstallBIOS(ctx, reader, size) - case common.SlugBMC: - err = c.firmwareInstallBMC(ctx, reader, size) - default: - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) - } - - if err != nil { - err = errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - - return jobID, err -} - -// FirmwareInstallStatus returns the status of the firmware install process -func (c *Client) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (string, error) { - component = strings.ToUpper(component) - - switch component { - case common.SlugBMC: - return c.statusBMCFirmwareInstall(ctx) - case common.SlugBIOS: - return c.statusBIOSFirmwareInstall(ctx) - default: - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "component unsupported: "+component) - } -} - -func (c *Client) deviceSupported(ctx context.Context) error { - errBoardPartNumUnknown := errors.New("baseboard part number unknown") - errBoardUnsupported := errors.New("feature not supported/implemented for device") - - // Its likely this works on all X11's +var ( + // Its likely the X11 code works on all X11's // for now, we list only the ones its been tested on. // // board part numbers // - supported := []string{ + supportedModels = []string{ "X11SCM-F", "X11DPH-T", "X11SCH-F", @@ -83,24 +25,55 @@ func (c *Client) deviceSupported(ctx context.Context) error { "X11DPG-SN", "X11DPT-B", "X11SSE-F", + "X12STH-SYS", } - data, err := c.fruInfo(ctx) - if err != nil { - return err + errUnexpectedModel = errors.New("unexpected device model") + errUploadTaskIDExpected = errors.New("expected an firmware upload taskID") +) + +// bmc client interface implementations methods +func (c *Client) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { + if err := c.serviceClient.supportsFirmwareInstall(ctx, c.bmc.deviceModel()); err != nil { + return nil, err } - if data.Board == nil || strings.TrimSpace(data.Board.PartNum) == "" { - return errors.Wrap(errBoardPartNumUnknown, "baseboard part number empty") + return c.bmc.firmwareInstallSteps(component) +} + +func (c *Client) FirmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { + if err := c.serviceClient.supportsFirmwareInstall(ctx, c.bmc.deviceModel()); err != nil { + return "", err } - c.model = strings.TrimSpace(data.Board.PartNum) + // // expect atleast 5 minutes left in the deadline to proceed with the upload + d, _ := ctx.Deadline() + if time.Until(d) < 5*time.Minute { + return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) + } - for _, b := range supported { - if strings.EqualFold(b, strings.TrimSpace(data.Board.PartNum)) { - return nil - } + return c.bmc.firmwareUpload(ctx, component, file) +} + +func (c *Client) FirmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { + if err := c.serviceClient.supportsFirmwareInstall(ctx, c.bmc.deviceModel()); err != nil { + return "", err } - return errors.Wrap(errBoardUnsupported, data.Board.PartNum) + // x11's don't return a upload Task ID, since the upload mechanism is not redfish + if !strings.HasPrefix(c.bmc.deviceModel(), "x11") && uploadTaskID == "" { + return "", errUploadTaskIDExpected + } + + return c.bmc.firmwareInstallUploaded(ctx, component, uploadTaskID) +} + +// FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. +func (c *Client) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state, status string, err error) { + if err := c.serviceClient.supportsFirmwareInstall(ctx, c.bmc.deviceModel()); err != nil { + return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) + } + + component = strings.ToUpper(component) + return c.bmc.firmwareTaskStatus(ctx, component, taskID) } diff --git a/providers/supermicro/firmware_bios_test.go b/providers/supermicro/firmware_bios_test.go index f85914a8..f4bdfb53 100644 --- a/providers/supermicro/firmware_bios_test.go +++ b/providers/supermicro/firmware_bios_test.go @@ -8,6 +8,7 @@ import ( "net/url" "testing" + "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) @@ -79,7 +80,10 @@ func Test_setComponentUpdateMisc(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.checkComponentUpdateMisc(context.Background(), tc.stage); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) @@ -165,7 +169,10 @@ func Test_setBIOSFirmwareInstallMode(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.setBMCFirmwareInstallMode(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) diff --git a/providers/supermicro/floppy.go b/providers/supermicro/floppy.go index 3cd3dceb..a1967fdf 100644 --- a/providers/supermicro/floppy.go +++ b/providers/supermicro/floppy.go @@ -24,7 +24,7 @@ func (c *Client) floppyImageMounted(ctx context.Context) (bool, error) { return false, err } - inserted, err := c.redfish.InsertedVirtualMedia(ctx) + inserted, err := c.serviceClient.redfish.InsertedVirtualMedia(ctx) if err != nil { return false, err } @@ -60,7 +60,7 @@ func (c *Client) MountFloppyImage(ctx context.Context, image io.Reader) error { }, { name: "csrf-token", - data: bytes.NewBufferString(c.csrfToken), + data: bytes.NewBufferString(c.serviceClient.csrfToken), }, } @@ -103,7 +103,7 @@ func (c *Client) MountFloppyImage(ctx context.Context, image io.Reader) error { } payloadWriter.Close() - resp, statusCode, err := c.query( + resp, statusCode, err := c.serviceClient.query( ctx, "cgi/uimapin.cgi", http.MethodPost, @@ -133,7 +133,7 @@ func (c *Client) UnmountFloppyImage(ctx context.Context) error { return nil } - resp, statusCode, err := c.query( + resp, statusCode, err := c.serviceClient.query( ctx, "cgi/uimapout.cgi", http.MethodPost, diff --git a/providers/supermicro/supermicro.go b/providers/supermicro/supermicro.go index 29e235a0..1ec2b367 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -5,7 +5,6 @@ import ( "context" "crypto/x509" "encoding/base64" - "encoding/xml" "fmt" "io" "net/http" @@ -17,9 +16,11 @@ import ( "strings" "time" + "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/bmclib/v2/providers" + "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" @@ -39,10 +40,11 @@ var ( // Features implemented Features = registrar.Features{ providers.FeatureScreenshot, - providers.FeatureFirmwareInstall, - providers.FeatureFirmwareInstallStatus, providers.FeatureMountFloppyImage, providers.FeatureUnmountFloppyImage, + providers.FeatureFirmwareUpload, + providers.FeatureFirmwareInstallUploaded, + providers.FeatureFirmwareTaskStatus, } ) @@ -92,24 +94,25 @@ func WithPort(port string) Option { // Connection details type Client struct { - client *http.Client - host string - user string - pass string - port string - csrfToken string - model string - redfish *redfishwrapper.Client - log logr.Logger - _ [32]byte + serviceClient *serviceClient + bmc bmcQueryor + log logr.Logger +} + +type bmcQueryor interface { + firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) + firmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) + firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) + firmwareTaskStatus(ctx context.Context, component, taskID string) (state, status string, err error) + // query device model from the bmc + queryDeviceModel(ctx context.Context) (model string, err error) + // returns the device model, that was queried previously with queryDeviceModel + deviceModel() (model string) + supportsInstall(component string) error } // New returns connection with a Supermicro client initialized func NewClient(host, user, pass string, log logr.Logger, opts ...Option) *Client { - if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { - host = "https://" + host - } - defaultConfig := &Config{ Port: "443", } @@ -118,13 +121,17 @@ func NewClient(host, user, pass string, log logr.Logger, opts ...Option) *Client opt(defaultConfig) } + serviceClient := newBmcServiceClient( + host, + defaultConfig.Port, + user, + pass, + httpclient.Build(defaultConfig.httpClientSetupFuncs...), + ) + return &Client{ - host: host, - user: user, - pass: pass, - port: defaultConfig.Port, - client: httpclient.Build(defaultConfig.httpClientSetupFuncs...), - log: log, + serviceClient: serviceClient, + log: log, } } @@ -132,13 +139,13 @@ func NewClient(host, user, pass string, log logr.Logger, opts ...Option) *Client func (c *Client) Open(ctx context.Context) (err error) { data := fmt.Sprintf( "name=%s&pwd=%s&check=00", - base64.StdEncoding.EncodeToString([]byte(c.user)), - base64.StdEncoding.EncodeToString([]byte(c.pass)), + base64.StdEncoding.EncodeToString([]byte(c.serviceClient.user)), + base64.StdEncoding.EncodeToString([]byte(c.serviceClient.pass)), ) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} - body, status, err := c.query(ctx, "cgi/login.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) + body, status, err := c.serviceClient.query(ctx, "cgi/login.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error()) } @@ -152,7 +159,7 @@ func (c *Client) Open(ctx context.Context) (err error) { return errors.Wrap(bmclibErrs.ErrLoginFailed, "unexpected response contents") } - contentsTopMenu, status, err := c.query(ctx, "cgi/url_redirect.cgi?url_name=topmenu", http.MethodGet, nil, nil, 0) + contentsTopMenu, status, err := c.serviceClient.query(ctx, "cgi/url_redirect.cgi?url_name=topmenu", http.MethodGet, nil, nil, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error()) } @@ -166,35 +173,65 @@ func (c *Client) Open(ctx context.Context) (err error) { return errors.Wrap(bmclibErrs.ErrLoginFailed, "could not parse CSRF-TOKEN from page") } - c.csrfToken = token + c.serviceClient.setCsrfToken(token) + + c.bmc, err = c.bmcQueryor(ctx) + if err != nil { + return errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error()) + } return nil } +func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) { + x11 := newX11Client(c.serviceClient, c.log) + x12 := newX12Client(c.serviceClient, c.log) + + var queryor bmcQueryor + + for _, bmc := range []bmcQueryor{x11, x12} { + var err error + + _, err = bmc.queryDeviceModel(ctx) + if err != nil { + if errors.Is(err, ErrXMLAPIUnsupported) { + continue + } + + return nil, errors.Wrap(ErrModelUnknown, err.Error()) + } + + queryor = bmc + break + } + + if queryor == nil { + return nil, errors.Wrap(ErrModelUnknown, "failed to setup query client") + } + + model := strings.ToLower(queryor.deviceModel()) + if !strings.HasPrefix(model, "x12") && !strings.HasPrefix(model, "x11") { + return nil, errors.Wrap(ErrModelUnsupported, model) + } + + return queryor, nil +} + func (c *Client) openRedfish(ctx context.Context) error { - if c.redfish != nil && c.redfish.SessionActive() == nil { + if c.serviceClient.redfish != nil && c.serviceClient.redfish.SessionActive() == nil { return nil } - rfclient := redfishwrapper.NewClient(c.host, "", c.user, c.pass) + rfclient := redfishwrapper.NewClient(c.serviceClient.host, "", c.serviceClient.user, c.serviceClient.pass) if err := rfclient.Open(ctx); err != nil { return err } - c.redfish = rfclient + c.serviceClient.redfish = rfclient return nil } -func (c *Client) closeRedfish(ctx context.Context) { - if c.redfish != nil { - // error not checked on purpose - _ = c.redfish.Close(ctx) - - c.redfish = nil - } -} - func parseToken(body []byte) string { var key string if bytes.Contains(body, []byte(`CSRF-TOKEN`)) { @@ -224,11 +261,11 @@ func parseToken(body []byte) string { // Close a connection to a Supermicro BMC using the vendor API. func (c *Client) Close(ctx context.Context) error { - if c.client == nil { + if c.serviceClient.client == nil { return nil } - _, status, err := c.query(ctx, "cgi/logout.cgi", http.MethodGet, nil, nil, 0) + _, status, err := c.serviceClient.query(ctx, "cgi/logout.cgi", http.MethodGet, nil, nil, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrLogoutFailed, err.Error()) } @@ -237,7 +274,14 @@ func (c *Client) Close(ctx context.Context) error { return errors.Wrap(bmclibErrs.ErrLogoutFailed, strconv.Itoa(status)) } - c.closeRedfish(ctx) + if c.serviceClient.redfish != nil { + err = c.serviceClient.redfish.Close(ctx) + if err != nil { + return errors.Wrap(bmclibErrs.ErrLogoutFailed, err.Error()) + } + + c.serviceClient.redfish = nil + } return nil } @@ -271,7 +315,7 @@ func (c *Client) fetchScreenPreview(ctx context.Context) ([]byte, error) { headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} endpoint := "cgi/url_redirect.cgi?url_name=Snapshot&url_type=img" - body, status, err := c.query(ctx, endpoint, http.MethodGet, nil, headers, 0) + body, status, err := c.serviceClient.query(ctx, endpoint, http.MethodGet, nil, headers, 0) if err != nil { return nil, errors.Wrap(bmclibErrs.ErrScreenshot, strconv.Itoa(status)) } @@ -288,7 +332,7 @@ func (c *Client) initScreenPreview(ctx context.Context) error { data := "op=sys_preview&_=" - body, status, err := c.query(ctx, "cgi/op.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) + body, status, err := c.serviceClient.query(ctx, "cgi/op.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } @@ -314,32 +358,6 @@ func (c *Client) PowerSet(ctx context.Context, state string) (ok bool, err error } } -func (c *Client) fruInfo(ctx context.Context) (*FruInfo, error) { - headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} - - payload := "op=FRU_INFO.XML&r=(0,0)&_=" - - body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBufferString(payload), headers, 0) - if err != nil { - return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) - } - - if status != 200 { - return nil, unexpectedResponseErr([]byte(payload), body, status) - } - - if !bytes.Contains(body, []byte(``)) { - return nil, unexpectedResponseErr([]byte(payload), body, status) - } - - data := &IPMI{} - if err := xml.Unmarshal(body, data); err != nil { - return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) - } - - return data.FruInfo, nil -} - // powerCycle using SMC XML API // // This method is only here for the case when firmware updates are being applied using this provider. @@ -350,7 +368,7 @@ func (c *Client) powerCycle(ctx context.Context) (bool, error) { "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } - body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) + body, status, err := c.serviceClient.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return false, err } @@ -362,7 +380,52 @@ func (c *Client) powerCycle(ctx context.Context) (bool, error) { return true, nil } -func (c *Client) query(ctx context.Context, endpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { +type serviceClient struct { + host string + port string + user string + pass string + csrfToken string + client *http.Client + redfish *redfishwrapper.Client +} + +func newBmcServiceClient(host, port, user, pass string, client *http.Client) *serviceClient { + if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { + host = "https://" + host + } + + return &serviceClient{host: host, port: port, user: user, pass: pass, client: client} +} + +func (c *serviceClient) setCsrfToken(t string) { + c.csrfToken = t +} + +func (c *serviceClient) redfishSession(ctx context.Context) (err error) { + c.redfish = redfishwrapper.NewClient(c.host, "", c.user, c.pass, redfishwrapper.WithHTTPClient(c.client)) + if err := c.redfish.Open(ctx); err != nil { + return err + } + + return nil +} + +func (c *serviceClient) supportsFirmwareInstall(ctx context.Context, model string) error { + if model == "" { + return errors.Wrap(ErrModelUnknown, "unable to determine firmware install compatibility") + } + + for _, s := range supportedModels { + if strings.EqualFold(s, model) { + return nil + } + } + + return errors.Wrap(ErrModelUnsupported, "firmware install not supported for: "+model) +} + +func (c *serviceClient) query(ctx context.Context, endpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { var body []byte var err error var req *http.Request diff --git a/providers/supermicro/supermicro_test.go b/providers/supermicro/supermicro_test.go index 43508a5f..2317a88a 100644 --- a/providers/supermicro/supermicro_test.go +++ b/providers/supermicro/supermicro_test.go @@ -3,6 +3,7 @@ package supermicro import ( "context" "io" + "log" "net/http" "net/http/httptest" "net/url" @@ -12,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_parseToken(t *testing.T) { +func TestParseToken(t *testing.T) { testcases := []struct { name string body []byte @@ -60,7 +61,7 @@ func Test_parseToken(t *testing.T) { } } -func Test_Open(t *testing.T) { +func TestOpen(t *testing.T) { type handlerFuncMap map[string]func(http.ResponseWriter, *http.Request) testcases := []struct { name string @@ -75,10 +76,13 @@ func Test_Open(t *testing.T) { "foo", "bar", handlerFuncMap{ + "/": func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, // first request to login "/cgi/login.cgi": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) - assert.Equal(t, r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + assert.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { @@ -118,6 +122,24 @@ func Test_Open(t *testing.T) { response := []byte(``) _, _ = w.Write(response) }, + // request for model + "/cgi/ipmi.cgi": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, http.MethodPost) + assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) + + b, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, `op=FRU_INFO.XML&r=(0,0)&_=`, string(b)) + + _, _ = w.Write([]byte(` + + + + `)) + }, }, }, { @@ -155,27 +177,27 @@ func Test_Open(t *testing.T) { server := httptest.NewTLSServer(mux) defer server.Close() + server.Config.ErrorLog = log.Default() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), tc.user, tc.pass, logr.Discard(), WithPort(parsedURL.Port())) - if err := client.Open(context.Background()); err != nil { - if tc.errorContains != "" { - assert.ErrorContains(t, err, tc.errorContains) - - return - } + err = client.Open(context.Background()) + if tc.errorContains != "" { + assert.ErrorContains(t, err, tc.errorContains) - assert.Nil(t, err) + return } + + assert.Nil(t, err) }) } } -func Test_Close(t *testing.T) { +func TestClose(t *testing.T) { testcases := []struct { name string errorContains string @@ -217,21 +239,21 @@ func Test_Close(t *testing.T) { } client := NewClient(parsedURL.Hostname(), tc.user, tc.pass, logr.Discard(), WithPort(parsedURL.Port())) - if err := client.Close(context.Background()); err != nil { - if tc.errorContains != "" { - assert.ErrorContains(t, err, tc.errorContains) - - return - } + err = client.Close(context.Background()) + if tc.errorContains != "" { + assert.ErrorContains(t, err, tc.errorContains) - assert.Nil(t, err) + return } + + assert.Nil(t, err) + assert.Nil(t, client.serviceClient.redfish) }) } } -func Test_initScreenPreview(t *testing.T) { +func TestInitScreenPreview(t *testing.T) { testcases := []struct { name string errorContains string @@ -282,20 +304,19 @@ func Test_initScreenPreview(t *testing.T) { } client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) - if err := client.initScreenPreview(context.Background()); err != nil { - if tc.errorContains != "" { - assert.ErrorContains(t, err, tc.errorContains) + err = client.initScreenPreview(context.Background()) + if tc.errorContains != "" { + assert.ErrorContains(t, err, tc.errorContains) + return + } - return - } + assert.Nil(t, err) - assert.Nil(t, err) - } }) } } -func Test_fetchScreenPreview(t *testing.T) { +func TestFetchScreenPreview(t *testing.T) { testcases := []struct { name string expectImage []byte @@ -343,11 +364,9 @@ func Test_fetchScreenPreview(t *testing.T) { client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) image, err := client.fetchScreenPreview(context.Background()) - if err != nil { - if tc.errorContains != "" { - assert.ErrorContains(t, err, tc.errorContains) - return - } + if tc.errorContains != "" { + assert.ErrorContains(t, err, tc.errorContains) + return } assert.Nil(t, err) diff --git a/providers/supermicro/x11.go b/providers/supermicro/x11.go new file mode 100644 index 00000000..705c29f2 --- /dev/null +++ b/providers/supermicro/x11.go @@ -0,0 +1,145 @@ +package supermicro + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "net/http" + "os" + "strings" + + "github.com/bmc-toolbox/bmclib/v2/constants" + bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" + "github.com/bmc-toolbox/common" + "github.com/go-logr/logr" + "github.com/pkg/errors" + "golang.org/x/exp/slices" +) + +type x11 struct { + *serviceClient + model string + log logr.Logger +} + +func newX11Client(client *serviceClient, logger logr.Logger) bmcQueryor { + return &x11{ + serviceClient: client, + log: logger, + } +} + +func (c *x11) deviceModel() string { + return c.model +} + +func (c *x11) queryDeviceModel(ctx context.Context) (string, error) { + errBoardPartNumUnknown := errors.New("baseboard part number unknown") + data, err := c.fruInfo(ctx) + if err != nil { + if strings.Contains(err.Error(), "404") { + return "", ErrXMLAPIUnsupported + } + + return "", err + } + + partNum := strings.TrimSpace(data.Board.PartNum) + + if data.Board == nil || partNum == "" { + return "", errors.Wrap(errBoardPartNumUnknown, "baseboard part number empty") + } + + c.model = common.FormatProductName(partNum) + + return c.model, nil +} + +func (c *x11) fruInfo(ctx context.Context) (*FruInfo, error) { + headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} + + payload := "op=FRU_INFO.XML&r=(0,0)&_=" + + body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBufferString(payload), headers, 0) + if err != nil { + return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) + } + + if status != 200 { + return nil, unexpectedResponseErr([]byte(payload), body, status) + } + + if !bytes.Contains(body, []byte(``)) { + return nil, unexpectedResponseErr([]byte(payload), body, status) + } + + data := &IPMI{} + if err := xml.Unmarshal(body, data); err != nil { + return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) + } + + return data.FruInfo, nil +} + +func (c *x11) supportsInstall(component string) error { + errComponentNotSupported := fmt.Errorf("component %s on device %s not supported", component, c.model) + + supported := []string{common.SlugBIOS, common.SlugBMC} + if !slices.Contains(supported, strings.ToUpper(component)) { + return errComponentNotSupported + } + + return nil +} + +func (c *x11) firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) { + if err := c.supportsInstall(component); err != nil { + return nil, err + } + + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepUpload, + constants.FirmwareInstallStepInstallUploaded, + constants.FirmwareInstallStepInstallStatus, + }, nil +} + +func (c *x11) firmwareUpload(ctx context.Context, component string, file *os.File) (string, error) { + component = strings.ToUpper(component) + + switch component { + case common.SlugBIOS: + return "", c.firmwareUploadBIOS(ctx, file) + case common.SlugBMC: + return "", c.firmwareUploadBMC(ctx, file) + } + + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) +} + +func (c *x11) firmwareInstallUploaded(ctx context.Context, component, _ string) (string, error) { + component = strings.ToUpper(component) + + switch component { + case common.SlugBIOS: + return "", c.firmwareInstallUploadedBIOS(ctx) + case common.SlugBMC: + return "", c.initiateBMCFirmwareInstall(ctx) + } + + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallUploaded, "component unsupported: "+component) +} + +func (c *x11) firmwareTaskStatus(ctx context.Context, component, _ string) (state, status string, err error) { + component = strings.ToUpper(component) + + switch component { + case common.SlugBIOS: + return c.statusBIOSFirmwareInstall(ctx) + case common.SlugBMC: + return c.statusBMCFirmwareInstall(ctx) + } + + return "", "", errors.Wrap(bmclibErrs.ErrFirmwareTaskStatus, "component unsupported: "+component) +} diff --git a/providers/supermicro/firmware_bios.go b/providers/supermicro/x11_firmware_bios.go similarity index 82% rename from providers/supermicro/firmware_bios.go rename to providers/supermicro/x11_firmware_bios.go index 65936fb5..e6085af4 100644 --- a/providers/supermicro/firmware_bios.go +++ b/providers/supermicro/x11_firmware_bios.go @@ -19,7 +19,7 @@ import ( "github.com/pkg/errors" ) -func (c *Client) firmwareInstallBIOS(ctx context.Context, reader io.Reader, fileSize int64) error { +func (c *x11) firmwareUploadBIOS(ctx context.Context, reader io.Reader) error { var err error c.log.V(2).Info("set firmware install mode", "ip", c.host, "component", "BIOS", "model", c.model) @@ -51,30 +51,24 @@ func (c *Client) firmwareInstallBIOS(ctx context.Context, reader io.Reader, file c.log.V(2).Info("verifying uploaded firmware", "ip", c.host, "component", "BIOS", "model", c.model) // 3. BMC verifies the uploaded firmware version - err = c.verifyBIOSFirmwareVersion(ctx) - if err != nil { - return err - } + return c.verifyBIOSFirmwareVersion(ctx) +} +func (c *x11) firmwareInstallUploadedBIOS(ctx context.Context) error { c.log.V(2).Info("initiating firmware install", "ip", c.host, "component", "BIOS", "model", c.model) // pre install requisite - err = c.setBIOSOp(ctx) + err := c.setBIOSOp(ctx) if err != nil { return err } // 4. Run the firmware install process - err = c.initiateBIOSFirmwareInstall(ctx) - if err != nil { - return err - } - - return nil + return c.initiateBIOSFirmwareInstall(ctx) } // checks component update status -func (c *Client) checkComponentUpdateMisc(ctx context.Context, stage string) error { +func (c *x11) checkComponentUpdateMisc(ctx context.Context, stage string) error { var payload, expectResponse []byte switch stage { @@ -124,7 +118,7 @@ func (c *Client) checkComponentUpdateMisc(ctx context.Context, stage string) err return nil } -func (c *Client) setBIOSFirmwareInstallMode(ctx context.Context) error { +func (c *x11) setBIOSFirmwareInstallMode(ctx context.Context) error { payload := []byte(`op=BIOS_UPLOAD.XML&r=(0,0)&_=`) @@ -168,7 +162,7 @@ func (c *Client) setBIOSFirmwareInstallMode(ctx context.Context) error { } -func (c *Client) setBiosUpdateStart(ctx context.Context) error { +func (c *x11) setBiosUpdateStart(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_START.XML&r=(1,0)&_=`) headers := map[string]string{ @@ -197,7 +191,7 @@ func (c *Client) setBiosUpdateStart(ctx context.Context) error { // // OO8+cjamaZZOMf6ZiGDY3Lw+7O20r5lR8aI8ByuTo3E // ------WebKitFormBoundaryXIAavwG4xzohdB6k-- -func (c *Client) uploadBIOSFirmware(ctx context.Context, fwReader io.Reader) error { +func (c *x11) uploadBIOSFirmware(ctx context.Context, fwReader io.Reader) error { var payloadBuffer bytes.Buffer var err error @@ -274,7 +268,7 @@ func (c *Client) uploadBIOSFirmware(ctx context.Context, fwReader io.Reader) err return nil } -func (c *Client) verifyBIOSFirmwareVersion(ctx context.Context) error { +func (c *x11) verifyBIOSFirmwareVersion(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_CHECK.XML&r=(0,0)&_=`) expectResponse := []byte(``) @@ -306,7 +300,7 @@ func (c *Client) verifyBIOSFirmwareVersion(ctx context.Context) error { return nil } -func (c *Client) setBIOSOp(ctx context.Context) error { +func (c *x11) setBIOSOp(ctx context.Context) error { payload := []byte(`op=BIOS_OPTION.XML&_=`) expectResponse := []byte(``) @@ -326,7 +320,7 @@ func (c *Client) setBIOSOp(ctx context.Context) error { return nil } -func (c *Client) initiateBIOSFirmwareInstall(ctx context.Context) error { +func (c *x11) initiateBIOSFirmwareInstall(ctx context.Context) error { // save all current SMBIOS, NVRAM, ME configuration payload := []byte(`op=main_biosupdate&_=`) expectResponse := []byte(`ok`) @@ -356,7 +350,7 @@ func (c *Client) initiateBIOSFirmwareInstall(ctx context.Context) error { return nil } -func (c *Client) setBIOSUpdateDone(ctx context.Context) error { +func (c *x11) setBIOSUpdateDone(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_DONE.XML&r=(1,0)&_=`) headers := map[string]string{ @@ -377,64 +371,73 @@ func (c *Client) setBIOSUpdateDone(ctx context.Context) error { } // statusBIOSFirmwareInstall returns the status of the firmware install process -func (c *Client) statusBIOSFirmwareInstall(ctx context.Context) (string, error) { +func (c *x11) statusBIOSFirmwareInstall(ctx context.Context) (state, status string, err error) { payload := []byte(`fwtype=1&_`) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} - resp, status, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) + resp, httpStatus, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) + return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } - if status != http.StatusOK { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected status code: "+strconv.Itoa(status)) + if httpStatus != http.StatusOK { + return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected http status code: "+strconv.Itoa(httpStatus)) + } + + // if theres html or no xml in the response, the session expired + // at the end of the install the BMC resets itself and the response is in HTML. + if bytes.Contains(resp, []byte(``)) || !bytes.Contains(resp, []byte(``)) { + // reopen session here, check firmware install status + return constants.FirmwareInstallUnknown, "session expired/unexpected response", bmclibErrs.ErrSessionExpired } + // as long as the response is xml, the firmware install is running + part := strings.Split(string(resp), "")[1] + percent := strings.Split(part, "")[0] + percent += "%" + switch { // 1% indicates the file has been uploaded and the firmware install is not yet initiated case bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("1")): - return constants.FirmwareInstallFailed, bmclibErrs.ErrBMCColdResetRequired + return constants.FirmwareInstallFailed, percent, bmclibErrs.ErrBMCColdResetRequired // 0% along with the check on the component endpoint indicates theres no update in progress case (bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("0"))): if err := c.checkComponentUpdateMisc(ctx, "postUpdate"); err != nil { if errors.Is(err, bmclibErrs.ErrHostPowercycleRequired) { - return constants.FirmwareInstallPowerCyleHost, nil + return constants.FirmwareInstallPowerCycleHost, percent, nil } } - return constants.FirmwareInstallComplete, nil + return constants.FirmwareInstallComplete, "all done!", nil // status 0 and 100% indicates the update is complete and requires a few post update calls case bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("100")): + // TODO: create a new bmc method FirmwarePostInstall() // notifies the BMC the BIOS update is done if err := c.setBIOSUpdateDone(ctx); err != nil { - return "", err + return "", "", err } // tells the BMC it can get out of the BIOS update mode if err := c.checkComponentUpdateMisc(ctx, "postUpdate"); err != nil { if errors.Is(err, bmclibErrs.ErrHostPowercycleRequired) { - return constants.FirmwareInstallPowerCyleHost, nil + return constants.FirmwareInstallPowerCycleHost, percent, nil } - return constants.FirmwareInstallPowerCyleHost, err + return constants.FirmwareInstallPowerCycleHost, percent, err } - return constants.FirmwareInstallPowerCyleHost, nil + return constants.FirmwareInstallPowerCycleHost, percent, nil // status 8 and percent 0 indicates its initializing the update case bytes.Contains(resp, []byte("8")) && bytes.Contains(resp, []byte("0")): - return constants.FirmwareInstallRunning, nil + return constants.FirmwareInstallRunning, percent, nil // status 8 and any other percent value indicates its running case bytes.Contains(resp, []byte("8")) && bytes.Contains(resp, []byte("")): - return constants.FirmwareInstallRunning, nil - - case bytes.Contains(resp, []byte(``)): - return constants.FirmwareInstallUnknown, bmclibErrs.ErrSessionExpired - - default: - return constants.FirmwareInstallUnknown, nil + return constants.FirmwareInstallRunning, percent, nil } + + return constants.FirmwareInstallUnknown, "", nil } diff --git a/providers/supermicro/firmware_bmc.go b/providers/supermicro/x11_firmware_bmc.go similarity index 78% rename from providers/supermicro/firmware_bmc.go rename to providers/supermicro/x11_firmware_bmc.go index b142a9a0..6dfbd2b4 100644 --- a/providers/supermicro/firmware_bmc.go +++ b/providers/supermicro/x11_firmware_bmc.go @@ -25,14 +25,11 @@ var ( ErrMultipartForm = errors.New("multipart form error") ) -// firmwareInstallBMC uploads and installs firmware for the BMC component -func (c *Client) firmwareInstallBMC(ctx context.Context, reader io.Reader, fileSize int64) error { - var err error - +func (c *x11) firmwareUploadBMC(ctx context.Context, reader io.Reader) error { c.log.V(2).Info("setting device to firmware install mode", "ip", c.host, "component", "BMC", "model", c.model) // 1. set the device to flash mode - prepares the flash - err = c.setBMCFirmwareInstallMode(ctx) + err := c.setBMCFirmwareInstallMode(ctx) if err != nil { return err } @@ -48,23 +45,10 @@ func (c *Client) firmwareInstallBMC(ctx context.Context, reader io.Reader, fileS c.log.V(2).Info("verifying uploaded firmware", "ip", c.host, "component", "BMC", "model", c.model) // 3. BMC verifies the uploaded firmware version - err = c.verifyBMCFirmwareVersion(ctx) - if err != nil { - return err - } - - c.log.V(2).Info("initiating firmware install", "ip", c.host, "component", "BMC", "model", c.model) - - // 4. Run the firmware install process - err = c.initiateBMCFirmwareInstall(ctx) - if err != nil { - return err - } - - return nil + return c.verifyBMCFirmwareVersion(ctx) } -func (c *Client) setBMCFirmwareInstallMode(ctx context.Context) error { +func (c *x11) setBMCFirmwareInstallMode(ctx context.Context) error { payload := []byte(`op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`) headers := map[string]string{ @@ -118,7 +102,7 @@ func (c *Client) setBMCFirmwareInstallMode(ctx context.Context) error { // // JhVe1BUiWzOVQdvXUKn7ClsQ5xffq8StMOxG7ZNlpKs // -----------------------------348113760313214626342869148824-- -func (c *Client) uploadBMCFirmware(ctx context.Context, fwReader io.Reader) error { +func (c *x11) uploadBMCFirmware(ctx context.Context, fwReader io.Reader) error { var payloadBuffer bytes.Buffer var err error @@ -195,7 +179,7 @@ func (c *Client) uploadBMCFirmware(ctx context.Context, fwReader io.Reader) erro return nil } -func (c *Client) verifyBMCFirmwareVersion(ctx context.Context) error { +func (c *x11) verifyBMCFirmwareVersion(ctx context.Context) error { errUnexpectedResponse := errors.New("unexpected response") payload := []byte(`op=UPLOAD_FW_VERSION.XML&r=(0,0)&_=`) @@ -221,7 +205,7 @@ func (c *Client) verifyBMCFirmwareVersion(ctx context.Context) error { } // initiate BMC firmware install process -func (c *Client) initiateBMCFirmwareInstall(ctx context.Context) error { +func (c *x11) initiateBMCFirmwareInstall(ctx context.Context) error { // preserve all configuration, sensor data and SSL certs(?) during upgrade payload := "op=main_fwupdate&preserve_config=1&preserve_sdr=1&preserve_ssl=1" @@ -254,42 +238,43 @@ func (c *Client) initiateBMCFirmwareInstall(ctx context.Context) error { } // statusBMCFirmwareInstall returns the status of the firmware install process -func (c *Client) statusBMCFirmwareInstall(ctx context.Context) (string, error) { +func (c *x11) statusBMCFirmwareInstall(ctx context.Context) (state, status string, err error) { payload := []byte(`fwtype=0&_`) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} - resp, status, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) + resp, httpStatus, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) + return constants.FirmwareInstallUnknown, "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } - if status != http.StatusOK { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected status code: "+strconv.Itoa(status)) + if httpStatus != http.StatusOK { + return constants.FirmwareInstallUnknown, "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected http status code: "+strconv.Itoa(httpStatus)) + } + + // if theres html or no xml in the response, the session expired + // at the end of the install the BMC resets itself and the response is in HTML. + if bytes.Contains(resp, []byte(``)) || !bytes.Contains(resp, []byte(``)) { + // reopen session here, check firmware install status + return constants.FirmwareInstallUnknown, "session expired/unexpected response", bmclibErrs.ErrSessionExpired } // as long as the response is xml, the firmware install is running - // at the end of the install the BMC resets itself and the response is in HTML - // - switch { + part := strings.Split(string(resp), "")[1] + percent := strings.Split(part, "")[0] + percent += "%" + switch percent { // TODO: - // - look up model on device and limit the parent methods to tested models. - // - fix up percent value checks, html indicates session has been terminated // X11DPH-T - returns percent 0 all the time // // 0% indicates its either not running or complete - case bytes.Contains(resp, []byte("0")) || bytes.Contains(resp, []byte("100")): - return constants.FirmwareInstallComplete, nil + case "0%", "100%": + return constants.FirmwareInstallComplete, percent, nil // until 2% its initializing - case bytes.Contains(resp, []byte(`1`)) || bytes.Contains(resp, []byte(`2`)): - return constants.FirmwareInstallInitializing, nil + case "1%", "2%": + return constants.FirmwareInstallInitializing, percent, nil // any other percent value indicates its active - case bytes.Contains(resp, []byte(``)): - return constants.FirmwareInstallRunning, nil - case bytes.Contains(resp, []byte(``)): - // reopen session here, check firmware install status - return constants.FirmwareInstallUnknown, bmclibErrs.ErrSessionExpired default: - return constants.FirmwareInstallUnknown, nil + return constants.FirmwareInstallRunning, percent, nil } } diff --git a/providers/supermicro/firmware_bmc_test.go b/providers/supermicro/x11_firmware_bmc_test.go similarity index 86% rename from providers/supermicro/firmware_bmc_test.go rename to providers/supermicro/x11_firmware_bmc_test.go index dbbb2f0c..b6b0ad13 100644 --- a/providers/supermicro/firmware_bmc_test.go +++ b/providers/supermicro/x11_firmware_bmc_test.go @@ -14,11 +14,12 @@ import ( "testing" "github.com/bmc-toolbox/bmclib/v2/constants" + "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) -func Test_setBMCFirmwareInstallMode(t *testing.T) { +func TestX11SetBMCFirmwareInstallMode(t *testing.T) { testcases := []struct { name string errorContains string @@ -90,7 +91,9 @@ func Test_setBMCFirmwareInstallMode(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.setBMCFirmwareInstallMode(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) @@ -104,7 +107,7 @@ func Test_setBMCFirmwareInstallMode(t *testing.T) { } } -func Test_uploadBMCFirmware(t *testing.T) { +func TestX11UploadBMCFirmware(t *testing.T) { testcases := []struct { name string errorContains string @@ -181,8 +184,10 @@ func Test_uploadBMCFirmware(t *testing.T) { defer os.Remove(binPath) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) - client.csrfToken = "foobar" + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.uploadBMCFirmware(context.Background(), fwReader); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) @@ -196,7 +201,7 @@ func Test_uploadBMCFirmware(t *testing.T) { } } -func Test_verifyBMCFirmwareVersion(t *testing.T) { +func TestX11VerifyBMCFirmwareVersion(t *testing.T) { testcases := []struct { name string errorContains string @@ -260,8 +265,10 @@ func Test_verifyBMCFirmwareVersion(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) - client.csrfToken = "foobar" + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.verifyBMCFirmwareVersion(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) @@ -275,7 +282,7 @@ func Test_verifyBMCFirmwareVersion(t *testing.T) { } } -func Test_initiateBMCFirmwareInstall(t *testing.T) { +func TestX11InitiateBMCFirmwareInstall(t *testing.T) { testcases := []struct { name string errorContains string @@ -339,8 +346,10 @@ func Test_initiateBMCFirmwareInstall(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) - client.csrfToken = "foobar" + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + if err := client.initiateBMCFirmwareInstall(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) @@ -354,9 +363,10 @@ func Test_initiateBMCFirmwareInstall(t *testing.T) { } } -func Test_statusBMCFirmwareInstall(t *testing.T) { +func TestX11StatusBMCFirmwareInstall(t *testing.T) { testcases := []struct { name string + expectState string expectStatus string errorContains string endpoint string @@ -365,6 +375,7 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { { "state complete 0", constants.FirmwareInstallComplete, + "0%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { @@ -389,6 +400,7 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { { "state complete 100", constants.FirmwareInstallComplete, + "100%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { @@ -413,6 +425,7 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { { "state initializing", constants.FirmwareInstallInitializing, + "1%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { @@ -437,6 +450,7 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { { "status running", constants.FirmwareInstallRunning, + "95%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { @@ -461,6 +475,7 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { { "status unknown", constants.FirmwareInstallUnknown, + "", "session expired", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { @@ -494,18 +509,22 @@ func Test_statusBMCFirmwareInstall(t *testing.T) { t.Fatal(err) } - client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) - client.csrfToken = "foobar" - if gotStatus, err := client.statusBMCFirmwareInstall(context.Background()); err != nil { + serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) + serviceClient.csrfToken = "foobar" + client := &x11{serviceClient: serviceClient, log: logr.Discard()} + + gotState, gotStatus, err := client.statusBMCFirmwareInstall(context.Background()) + if err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } - - assert.Nil(t, err) - assert.Equal(t, tc.expectStatus, gotStatus) } + + assert.Nil(t, err) + assert.Equal(t, tc.expectState, gotState) + assert.Equal(t, tc.expectStatus, gotStatus) }) } } diff --git a/providers/supermicro/x12.go b/providers/supermicro/x12.go new file mode 100644 index 00000000..e027d858 --- /dev/null +++ b/providers/supermicro/x12.go @@ -0,0 +1,303 @@ +package supermicro + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/bmc-toolbox/bmclib/v2/constants" + brrs "github.com/bmc-toolbox/bmclib/v2/errors" + rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" + "github.com/bmc-toolbox/common" + "github.com/go-logr/logr" + "github.com/pkg/errors" + "github.com/stmcginnis/gofish/redfish" + "golang.org/x/exp/slices" +) + +type x12 struct { + *serviceClient + model string + log logr.Logger +} + +func newX12Client(client *serviceClient, logger logr.Logger) bmcQueryor { + return &x12{ + serviceClient: client, + log: logger, + } +} + +func (c *x12) deviceModel() string { + return c.model +} + +func (c *x12) queryDeviceModel(ctx context.Context) (string, error) { + if err := c.redfishSession(ctx); err != nil { + return "", err + } + + _, model, err := c.redfish.DeviceVendorModel(ctx) + if err != nil { + return "", err + } + + if model == "" { + return "", errors.Wrap(ErrModelUnknown, "empty value") + } + + c.model = common.FormatProductName(model) + + return c.model, nil +} + +var ( + errUploadTaskIDEmpty = errors.New("firmware upload request returned empty firmware upload verify TaskID") +) + +func (c *x12) supportsInstall(component string) error { + errComponentNotSupported := fmt.Errorf("component %s on device %s not supported", component, c.model) + + supported := []string{common.SlugBIOS, common.SlugBMC} + if !slices.Contains(supported, strings.ToUpper(component)) { + return errComponentNotSupported + } + + return nil +} + +func (c *x12) firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) { + if err := c.supportsInstall(component); err != nil { + return nil, err + } + + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepUpload, + constants.FirmwareInstallStepUploadStatus, + constants.FirmwareInstallStepInstallUploaded, + constants.FirmwareInstallStepInstallStatus, + }, nil +} + +// upload firmware +func (c *x12) firmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", err + } + + err = c.firmwareTaskActive(ctx, component) + if err != nil { + return "", err + } + + targetID, err := c.redfishOdataID(ctx, component) + if err != nil { + return "", err + } + + params, err := c.redfishParameters(component, targetID) + if err != nil { + return "", err + } + + taskID, err = c.redfish.FirmwareUpload(ctx, file, params) + if err != nil { + if strings.Contains(err.Error(), "OemFirmwareAlreadyInUpdateMode") { + return "", errors.Wrap(brrs.ErrBMCColdResetRequired, "BMC currently in update mode, either continue the update OR if no update is currently running - reset the BMC") + } + + return "", errors.Wrap(err, "error in firmware upload") + } + + if taskID == "" { + return "", errUploadTaskIDEmpty + } + + return taskID, nil +} + +// returns an error when a bmc firmware install is active +func (c *x12) firmwareTaskActive(ctx context.Context, component string) error { + tasks, err := c.redfish.Tasks(ctx) + if err != nil { + return errors.Wrap(err, "error querying redfish tasks") + } + + for _, t := range tasks { + t := t + + if stateFinalized(t.TaskState) { + continue + } + + if err := noTasksRunning(component, t); err != nil { + return err + } + } + + return nil +} + +// noTasksRunning returns an error if a firmware related task was found active +func noTasksRunning(component string, t *redfish.Task) error { + errTaskActive := errors.New("A firmware task was found active for component: " + component) + + const ( + // The redfish task name when the BMC is verifies the uploaded BMC firmware. + verifyBMCFirmware = "BMC Verify" + // The redfish task name when the BMC is installing the uploaded BMC firmware. + updateBMCFirmware = "BMC Update" + // The redfish task name when the BMC is verifies the uploaded BIOS firmware. + verifyBIOSFirmware = "BIOS Verify" + // The redfish task name when the BMC is installing the uploaded BIOS firmware. + updateBIOSFirmware = "BIOS Update" + ) + + var verifyTaskName, updateTaskName string + + switch strings.ToUpper(component) { + case common.SlugBMC: + verifyTaskName = verifyBMCFirmware + updateTaskName = updateBMCFirmware + case common.SlugBIOS: + verifyTaskName = verifyBIOSFirmware + updateTaskName = updateBIOSFirmware + } + + taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) + + switch t.Name { + case verifyTaskName: + return errors.Wrap(errTaskActive, taskInfo) + case updateTaskName: + return errors.Wrap(errTaskActive, taskInfo) + default: + return nil + } +} + +func stateFinalized(s redfish.TaskState) bool { + finalized := []redfish.TaskState{ + redfish.CompletedTaskState, + redfish.CancelledTaskState, + redfish.InterruptedTaskState, + redfish.ExceptionTaskState, + } + + return slices.Contains(finalized, s) +} + +// redfish OEM parameter structs +type BIOS struct { + PreserveME bool `json:"PreserveME"` + PreserveNVRAM bool `json:"PreserveNVRAM"` + PreserveSMBIOS bool `json:"PreserveSMBIOS"` + PreserveOA bool `json:"PreserveOA"` + PreserveSETUPCONF bool `json:"PreserveSETUPCONF"` + PreserveSETUPPWD bool `json:"PreserveSETUPPWD"` + PreserveSECBOOTKEY bool `json:"PreserveSECBOOTKEY"` + PreserveBOOTCONF bool `json:"PreserveBOOTCONF"` +} + +type BMC struct { + PreserveCfg bool `json:"PreserveCfg"` + PreserveSdr bool `json:"PreserveSdr"` + PreserveSsl bool `json:"PreserveSsl"` +} + +type Supermicro struct { + *BIOS `json:"BIOS,omitempty"` + *BMC `json:"BMC,omitempty"` +} + +type OEM struct { + Supermicro `json:"Supermicro"` +} + +func (c *x12) redfishParameters(component, targetODataID string) (*rfw.RedfishUpdateServiceParameters, error) { + errUnsupported := errors.New("redfish parameters for x12 hardware component not supported: " + component) + + oem := OEM{} + + switch strings.ToUpper(component) { + case common.SlugBIOS: + oem.Supermicro.BIOS = &BIOS{ + PreserveME: false, + PreserveNVRAM: false, + PreserveSMBIOS: true, + PreserveOA: true, + PreserveSETUPCONF: true, + PreserveSETUPPWD: true, + PreserveSECBOOTKEY: true, + PreserveBOOTCONF: true, + } + case common.SlugBMC: + oem.Supermicro.BMC = &BMC{ + PreserveCfg: true, + PreserveSdr: true, + PreserveSsl: true, + } + default: + return nil, errUnsupported + } + + b, err := json.Marshal(oem) + if err != nil { + return nil, errors.Wrap(err, "error preparing redfish parameters") + } + + return &rfw.RedfishUpdateServiceParameters{ + OperationApplyTime: constants.OnStartUpdateRequest, + Targets: []string{targetODataID}, + Oem: b, + }, nil +} + +func (c *x12) redfishOdataID(ctx context.Context, component string) (string, error) { + errUnsupported := errors.New("unable to return redfish OData ID for unsupported component: " + component) + + switch strings.ToUpper(component) { + case common.SlugBMC: + return c.redfish.ManagerOdataID(ctx) + case common.SlugBIOS: + // hardcoded since SMCs without the DCMS license will throw license errors + return "/redfish/v1/Systems/1/Bios", nil + //return c.redfish.SystemsBIOSOdataID(ctx) + } + + return "", errUnsupported +} + +func (c *x12) firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", err + } + + task, err := c.redfish.Task(ctx, uploadTaskID) + if err != nil { + e := fmt.Sprintf("error querying redfish tasks for firmware upload taskID: %s, err: %s", uploadTaskID, err.Error()) + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, e) + } + + taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", task.ID, task.TaskState, task.TaskStatus) + + if task.TaskState != redfish.CompletedTaskState { + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) + } + + if task.TaskStatus != "OK" { + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) + } + + return c.redfish.StartUpdateForUploadedFirmware(ctx) +} + +func (c *x12) firmwareTaskStatus(ctx context.Context, component, taskID string) (state, status string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", "", errors.Wrap(brrs.ErrFirmwareTaskStatus, err.Error()) + } + + return c.redfish.TaskStatus(ctx, taskID) +}