diff --git a/providers/supermicro/floppy.go b/providers/supermicro/floppy.go new file mode 100644 index 00000000..72ee8434 --- /dev/null +++ b/providers/supermicro/floppy.go @@ -0,0 +1,154 @@ +package supermicro + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +var ( + errFloppyImageMounted = errors.New("floppy image is currently mounted") +) + +func (c *Client) floppyImageMounted(ctx context.Context) (bool, error) { + if err := c.openRedfish(ctx); err != nil { + return false, err + } + + inserted, err := c.redfish.InsertedVirtualMedia(ctx) + if err != nil { + return false, err + } + + for _, media := range inserted { + if strings.Contains(strings.ToLower(media), "floppy") { + return true, nil + } + } + + return false, nil +} + +func (c *Client) UploadFloppyImage(ctx context.Context, image io.Reader) error { + mounted, err := c.floppyImageMounted(ctx) + if err != nil { + return err + } + + if mounted { + return errFloppyImageMounted + } + + var payloadBuffer bytes.Buffer + + formParts := []struct { + name string + data io.Reader + }{ + { + name: "img_file", + data: image, + }, + { + name: "csrf-token", + data: bytes.NewBufferString(c.csrfToken), + }, + } + + payloadWriter := multipart.NewWriter(&payloadBuffer) + + for _, part := range formParts { + var partWriter io.Writer + + switch part.name { + case "img_file": + file, ok := part.data.(*os.File) + if !ok { + return errors.Wrap(ErrMultipartForm, "expected io.Reader for a floppy image file") + } + + if partWriter, err = payloadWriter.CreateFormFile(part.name, filepath.Base(file.Name())); err != nil { + return errors.Wrap(ErrMultipartForm, err.Error()) + } + + case "csrf-token": + // Add csrf token field + h := make(textproto.MIMEHeader) + // BMCs with newer firmware (>=1.74.09) accept the form with this name value + // h.Set("Content-Disposition", `form-data; name="CSRF-TOKEN"`) + // + // the BMCs running older firmware (<=1.23.06) versions expects the name value in this format + // and the newer firmware (>=1.74.09) seem to be backwards compatible with this name value format. + h.Set("Content-Disposition", `form-data; name="CSRF_TOKEN"`) + + if partWriter, err = payloadWriter.CreatePart(h); err != nil { + return errors.Wrap(ErrMultipartForm, err.Error()) + } + default: + return errors.Wrap(ErrMultipartForm, "unexpected form part: "+part.name) + } + + if _, err = io.Copy(partWriter, part.data); err != nil { + return err + } + } + payloadWriter.Close() + + resp, statusCode, err := c.query( + ctx, + "cgi/uimapin.cgi", + http.MethodPost, + bytes.NewReader(payloadBuffer.Bytes()), + map[string]string{"Content-Type": payloadWriter.FormDataContentType()}, + 0, + ) + + if err != nil { + return errors.Wrap(ErrMultipartForm, err.Error()) + } + + if statusCode != http.StatusOK { + return fmt.Errorf("non 200 response: %d %s", statusCode, resp) + } + + return nil +} + +func (c *Client) UnmountFloppyImage(ctx context.Context) error { + mounted, err := c.floppyImageMounted(ctx) + if err != nil { + return err + } + + if !mounted { + return nil + } + + resp, statusCode, err := c.query( + ctx, + "cgi/uimapout.cgi", + http.MethodPost, + nil, + nil, + 0, + ) + + if err != nil { + return err + } + + if statusCode != http.StatusOK { + return fmt.Errorf("non 200 response: %d %s", statusCode, resp) + } + + return nil +} diff --git a/providers/supermicro/supermicro.go b/providers/supermicro/supermicro.go index 3efb30af..ca03fc86 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -18,6 +18,7 @@ import ( "time" "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" @@ -40,6 +41,8 @@ var ( providers.FeatureScreenshot, providers.FeatureFirmwareInstall, providers.FeatureFirmwareInstallStatus, + providers.FeatureUploadFloppyImage, + providers.FeatureUnmountFloppyImage, } ) @@ -49,12 +52,15 @@ var ( // - screen capture // - bios firmware install // - bmc firmware install -// product: SYS-510T-MR, baseboard part number: X12STH-SYS +// +// product: SYS-510T-MR, baseboard part number: X12STH-SYS, X12SPO-NTF // - screen capture +// - floppy image mount // product: 6029P-E1CR12L, baseboard part number: X11DPH-T // . - screen capture // - bios firmware install // - bmc firmware install +// - floppy image mount type Config struct { HttpClient *http.Client @@ -93,7 +99,9 @@ type Client struct { port string csrfToken string model string + redfish *redfishwrapper.Client log logr.Logger + _ [32]byte } // New returns connection with a Supermicro client initialized @@ -163,6 +171,30 @@ func (c *Client) Open(ctx context.Context) (err error) { return nil } +func (c *Client) openRedfish(ctx context.Context) error { + if c.redfish != nil && c.redfish.SessionActive() == nil { + return nil + } + + rfclient := redfishwrapper.NewClient(c.host, "", c.user, c.pass) + if err := rfclient.Open(ctx); err != nil { + return err + } + + c.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`)) { @@ -205,6 +237,8 @@ func (c *Client) Close(ctx context.Context) error { return errors.Wrap(bmclibErrs.ErrLogoutFailed, strconv.Itoa(status)) } + c.closeRedfish(ctx) + return nil } @@ -383,6 +417,7 @@ func (c *Client) query(ctx context.Context, endpoint, method string, payload io. if cookie.Name == "SID" && cookie.Value != "" { req.AddCookie(cookie) } + } var reqDump []byte