diff --git a/bmc/floppy.go b/bmc/floppy.go new file mode 100644 index 00000000..891a1131 --- /dev/null +++ b/bmc/floppy.go @@ -0,0 +1,150 @@ +package bmc + +import ( + "context" + "fmt" + "io" + + bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" +) + +// FloppyImageMounter defines methods to upload a floppy image +type FloppyImageMounter interface { + MountFloppyImage(ctx context.Context, image io.Reader) (err error) +} + +// floppyImageUploaderProvider is an internal struct to correlate an implementation/provider and its name +type floppyImageUploaderProvider struct { + name string + impl FloppyImageMounter +} + +// mountFloppyImage is a wrapper method to invoke methods for the FloppyImageMounter interface +func mountFloppyImage(ctx context.Context, image io.Reader, p []floppyImageUploaderProvider) (metadata Metadata, err error) { + var metadataLocal Metadata + + for _, elem := range p { + if elem.impl == nil { + continue + } + + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + + return metadata, err + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + uploadErr := elem.impl.MountFloppyImage(ctx, image) + if uploadErr != nil { + err = multierror.Append(err, errors.WithMessagef(uploadErr, "provider: %v", elem.name)) + continue + } + + metadataLocal.SuccessfulProvider = elem.name + return metadataLocal, nil + } + } + + return metadataLocal, multierror.Append(err, errors.New("failed to mount floppy image")) +} + +// MountFloppyImageFromInterfaces identifies implementations of the FloppyImageMounter interface and passes the found implementations to the mountFloppyImage() wrapper +func MountFloppyImageFromInterfaces(ctx context.Context, image io.Reader, p []interface{}) (metadata Metadata, err error) { + providers := make([]floppyImageUploaderProvider, 0) + for _, elem := range p { + temp := floppyImageUploaderProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case FloppyImageMounter: + temp.impl = p + providers = append(providers, temp) + default: + e := fmt.Sprintf("not a FloppyImageMounter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + + if len(providers) == 0 { + return metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + "no FloppyImageMounter implementations found", + ), + ) + + } + + return mountFloppyImage(ctx, image, providers) +} + +// FloppyImageMounter defines methods to unmount a floppy image +type FloppyImageUnmounter interface { + UnmountFloppyImage(ctx context.Context) (err error) +} + +// floppyImageUnmounterProvider is an internal struct to correlate an implementation/provider and its name +type floppyImageUnmounterProvider struct { + name string + impl FloppyImageUnmounter +} + +// unmountFloppyImage is a wrapper method to invoke methods for the FloppyImageUnmounter interface +func unmountFloppyImage(ctx context.Context, p []floppyImageUnmounterProvider) (metadata Metadata, err error) { + var metadataLocal Metadata + + for _, elem := range p { + if elem.impl == nil { + continue + } + + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + + return metadata, err + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + uploadErr := elem.impl.UnmountFloppyImage(ctx) + if uploadErr != nil { + err = multierror.Append(err, errors.WithMessagef(uploadErr, "provider: %v", elem.name)) + continue + } + + metadataLocal.SuccessfulProvider = elem.name + return metadataLocal, nil + } + } + + return metadataLocal, multierror.Append(err, errors.New("failed to unmount floppy image")) +} + +// MountFloppyImageFromInterfaces identifies implementations of the FloppyImageUnmounter interface and passes the found implementations to the unmountFloppyImage() wrapper +func UnmountFloppyImageFromInterfaces(ctx context.Context, p []interface{}) (metadata Metadata, err error) { + providers := make([]floppyImageUnmounterProvider, 0) + for _, elem := range p { + temp := floppyImageUnmounterProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case FloppyImageUnmounter: + temp.impl = p + providers = append(providers, temp) + default: + e := fmt.Sprintf("not a FloppyImageUnmounter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + + if len(providers) == 0 { + return metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + "no FloppyImageUnmounter implementations found", + ), + ) + } + + return unmountFloppyImage(ctx, providers) +} diff --git a/bmc/floppy_test.go b/bmc/floppy_test.go new file mode 100644 index 00000000..3107b8c9 --- /dev/null +++ b/bmc/floppy_test.go @@ -0,0 +1,114 @@ +package bmc + +import ( + "context" + "io" + "testing" + "time" + + bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" + "github.com/stretchr/testify/assert" +) + +type mountFloppyImageTester struct { + returnError error +} + +func (p *mountFloppyImageTester) MountFloppyImage(ctx context.Context, reader io.Reader) (err error) { + return p.returnError +} + +func (p *mountFloppyImageTester) Name() string { + return "foo" +} + +func TestMountFloppyFromInterfaces(t *testing.T) { + testCases := []struct { + testName string + image io.Reader + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + badImplementation bool + }{ + {"success with metadata", nil, nil, 5 * time.Second, "foo", 1, false}, + {"failure with bad implementation", nil, bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := &mountFloppyImageTester{returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + metadata, err := MountFloppyImageFromInterfaces(context.Background(), tc.image, generic) + if tc.returnError != nil { + assert.ErrorContains(t, err, tc.returnError.Error()) + return + } + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, tc.returnError, err) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + }) + } +} + +type unmountFloppyImageTester struct { + returnError error +} + +func (p *unmountFloppyImageTester) UnmountFloppyImage(ctx context.Context) (err error) { + return p.returnError +} + +func (p *unmountFloppyImageTester) Name() string { + return "foo" +} + +func TestUnmountFloppyFromInterfaces(t *testing.T) { + testCases := []struct { + testName string + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + badImplementation bool + }{ + {"success with metadata", nil, 5 * time.Second, "foo", 1, false}, + {"failure with bad implementation", bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := &unmountFloppyImageTester{returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + metadata, err := UnmountFloppyImageFromInterfaces(context.Background(), generic) + if tc.returnError != nil { + assert.ErrorContains(t, err, tc.returnError.Error()) + return + } + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, tc.returnError, err) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + }) + } +} diff --git a/client.go b/client.go index c305fce2..4f62e6ad 100644 --- a/client.go +++ b/client.go @@ -450,5 +450,20 @@ func (c *Client) Screenshot(ctx context.Context) (image []byte, fileType string, func (c *Client) ClearSystemEventLog(ctx context.Context) (err error) { metadata, err := bmc.ClearSystemEventLogFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) + + return err +} + +func (c *Client) MountFloppyImage(ctx context.Context, image io.Reader) (err error) { + metadata, err := bmc.MountFloppyImageFromInterfaces(ctx, image, c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + + return err +} + +func (c *Client) UnmountFloppyImage(ctx context.Context) (err error) { + metadata, err := bmc.UnmountFloppyImageFromInterfaces(ctx, c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + return err } diff --git a/examples/floppy-image/doc.go b/examples/floppy-image/doc.go new file mode 100644 index 00000000..c89443f9 --- /dev/null +++ b/examples/floppy-image/doc.go @@ -0,0 +1,19 @@ +/* +inventory is an example commmand that utilizes the 'v1' bmclib interface +methods to upload and mount, unmount a floppy image. + + # mount image + $ go run examples/floppy-image/main.go \ + -host 10.1.2.3 \ + -user ADMIN \ + -password hunter2 \ + -image /tmp/floppy.img + + # un-mount image + $ go run examples/floppy-image/main.go \ + -host 10.1.2.3 \ + -user ADMIN \ + -password hunter2 \ + -unmount +*/ +package main diff --git a/examples/floppy-image/main.go b/examples/floppy-image/main.go new file mode 100644 index 00000000..60eb29e0 --- /dev/null +++ b/examples/floppy-image/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "crypto/x509" + "flag" + "log" + "os" + "time" + + bmclib "github.com/bmc-toolbox/bmclib/v2" + "github.com/bombsimon/logrusr/v2" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + user := flag.String("user", "", "Username to login with") + pass := flag.String("password", "", "Username to login with") + host := flag.String("host", "", "BMC hostname to connect to") + imagePath := flag.String("image", "", "The .img file to be uploaded") + unmountImage := flag.Bool("unmount", false, "Unmount floppy image") + + withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") + certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") + flag.Parse() + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.New(l) + + clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} + + if *withSecureTLS { + var pool *x509.CertPool + if *certPoolFile != "" { + pool = x509.NewCertPool() + data, err := os.ReadFile(*certPoolFile) + if err != nil { + l.Fatal(err) + } + pool.AppendCertsFromPEM(data) + } + // a nil pool uses the system certs + clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) + } + + cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) + err := cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + defer cl.Close(ctx) + + if *unmountImage { + if err := cl.UnmountFloppyImage(ctx); err != nil { + log.Fatal(err) + } + + return + } + + // open file handle + fh, err := os.Open(*imagePath) + if err != nil { + l.Fatal(err) + } + defer fh.Close() + + err = cl.MountFloppyImage(ctx, fh) + if err != nil { + l.Fatal(err) + } + + l.WithField("img", *imagePath).Info("image mounted successfully") +} diff --git a/internal/redfishwrapper/virtual_media.go b/internal/redfishwrapper/virtual_media.go index 4954eb3f..562cd8fa 100644 --- a/internal/redfishwrapper/virtual_media.go +++ b/internal/redfishwrapper/virtual_media.go @@ -74,3 +74,27 @@ func (c *Client) SetVirtualMedia(ctx context.Context, kind string, mediaURL stri return true, nil } + +func (c *Client) InsertedVirtualMedia(ctx context.Context) ([]string, error) { + managers, err := c.Managers(ctx) + if err != nil { + return nil, err + } + + var inserted []string + + for _, manager := range managers { + virtualMedia, err := manager.VirtualMedia() + if err != nil { + return nil, err + } + + for _, media := range virtualMedia { + if media.Inserted { + inserted = append(inserted, media.ID) + } + } + } + + return inserted, nil +} diff --git a/providers/providers.go b/providers/providers.go index 28ffd83a..b32749b4 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -23,6 +23,12 @@ const ( FeatureBootDeviceSet registrar.Feature = "bootdeviceset" // FeaturesVirtualMedia means an implementation can manage virtual media devices FeatureVirtualMedia registrar.Feature = "virtualmedia" + // FeatureMountFloppyImage means an implementation uploads a floppy image for mounting as virtual media. + // + // note: This is differs from FeatureVirtualMedia which is limited to accepting a URL to download the image from. + FeatureMountFloppyImage registrar.Feature = "mountFloppyImage" + // FeatureUnmountFloppyImage means an implementation removes a floppy image that was previously uploaded. + FeatureUnmountFloppyImage registrar.Feature = "unmountFloppyImage" // FeatureFirmwareInstall means an implementation that initiates the firmware install process FeatureFirmwareInstall registrar.Feature = "firmwareinstall" // FeatureFirmwareInstallSatus means an implementation that returns the firmware install status diff --git a/providers/supermicro/floppy.go b/providers/supermicro/floppy.go new file mode 100644 index 00000000..3cd3dceb --- /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) MountFloppyImage(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..29e235a0 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.FeatureMountFloppyImage, + 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