Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support redfish VirtualMedia #307

Merged
merged 2 commits into from
Feb 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions bmc/virtual_media.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package bmc

import (
"context"
"fmt"

"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)

// VirtualMediaSetter controls the virtual media attached to a machine
type VirtualMediaSetter interface {
SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error)
}

// VirtualMediaProviders is an internal struct to correlate an implementation/provider and its name
type virtualMediaProviders struct {
name string
virtualMediaSetter VirtualMediaSetter
}

// setVirtualMedia sets the virtual media.
func setVirtualMedia(ctx context.Context, kind string, mediaURL string, b []virtualMediaProviders) (ok bool, metadata Metadata, err error) {
var metadataLocal Metadata
Loop:
for _, elem := range b {
if elem.virtualMediaSetter == nil {
continue
}
select {
case <-ctx.Done():
err = multierror.Append(err, ctx.Err())
break Loop
default:
metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name)
ok, setErr := elem.virtualMediaSetter.SetVirtualMedia(ctx, kind, mediaURL)
if setErr != nil {
err = multierror.Append(err, errors.WithMessagef(setErr, "provider: %v", elem.name))
continue
}
if !ok {
err = multierror.Append(err, fmt.Errorf("provider: %v, failed to set virtual media", elem.name))
continue
}
metadataLocal.SuccessfulProvider = elem.name
return ok, metadataLocal, nil
}
}
return ok, metadataLocal, multierror.Append(err, errors.New("failed to set virtual media"))
}

// SetVirtualMediaFromInterfaces identifies implementations of the virtualMediaSetter interface and passes the found implementations to the setVirtualMedia() wrapper
func SetVirtualMediaFromInterfaces(ctx context.Context, kind string, mediaURL string, generic []interface{}) (ok bool, metadata Metadata, err error) {
bdSetters := make([]virtualMediaProviders, 0)
for _, elem := range generic {
temp := virtualMediaProviders{name: getProviderName(elem)}
switch p := elem.(type) {
fintelia marked this conversation as resolved.
Show resolved Hide resolved
case VirtualMediaSetter:
temp.virtualMediaSetter = p
bdSetters = append(bdSetters, temp)
default:
e := fmt.Sprintf("not a VirtualMediaSetter implementation: %T", p)
err = multierror.Append(err, errors.New(e))
}
}
if len(bdSetters) == 0 {
return ok, metadata, multierror.Append(err, errors.New("no VirtualMediaSetter implementations found"))
}
return setVirtualMedia(ctx, kind, mediaURL, bdSetters)
}
121 changes: 121 additions & 0 deletions bmc/virtual_media_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package bmc

import (
"context"
"errors"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-multierror"
)

type virtualMediaTester struct {
MakeNotOK bool
MakeErrorOut bool
}

func (r *virtualMediaTester) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) {
if r.MakeErrorOut {
return ok, errors.New("setting virtual media failed")
}
if r.MakeNotOK {
return false, nil
}
return true, nil
}

func (r *virtualMediaTester) Name() string {
return "test provider"
}

func TestSetVirtualMedia(t *testing.T) {
testCases := map[string]struct {
kind string
mediaURL string
makeErrorOut bool
makeNotOk bool
want bool
err error
ctxTimeout time.Duration
}{
"success": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true},
"not ok return": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider, failed to set virtual media"), errors.New("failed to set virtual media")}}},
"error": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: setting virtual media failed"), errors.New("failed to set virtual media")}}},
"error context timeout": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to set virtual media")}}, ctxTimeout: time.Nanosecond * 1},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
testImplementation := virtualMediaTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk}
expectedResult := tc.want
if tc.ctxTimeout == 0 {
tc.ctxTimeout = time.Second * 3
}
ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout)
defer cancel()
result, _, err := setVirtualMedia(ctx, tc.kind, tc.mediaURL, []virtualMediaProviders{{"test provider", &testImplementation}})
if err != nil {
diff := cmp.Diff(err.Error(), tc.err.Error())
if diff != "" {
t.Fatal(diff)
}
} else {
diff := cmp.Diff(result, expectedResult)
if diff != "" {
t.Fatal(diff)
}
}
})
}
}

func TestSetVirtualMediaFromInterfaces(t *testing.T) {
testCases := map[string]struct {
kind string
mediaURL string
err error
badImplementation bool
want bool
withName bool
}{
"success": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true},
"success with metadata": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true, withName: true},
"no implementations found": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a VirtualMediaSetter implementation: *struct {}"), errors.New("no VirtualMediaSetter implementations found")}}},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
var generic []interface{}
if tc.badImplementation {
badImplementation := struct{}{}
generic = []interface{}{&badImplementation}
} else {
testImplementation := virtualMediaTester{}
generic = []interface{}{&testImplementation}
}
expectedResult := tc.want
result, metadata, err := SetVirtualMediaFromInterfaces(context.Background(), tc.kind, tc.mediaURL, generic)
if err != nil {
if tc.err != nil {
diff := cmp.Diff(err.Error(), tc.err.Error())
if diff != "" {
t.Fatal(diff)
}
} else {
t.Fatal(err)
}
} else {
diff := cmp.Diff(result, expectedResult)
if diff != "" {
t.Fatal(diff)
}
}
if tc.withName {
if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" {
t.Fatal(diff)
}
}
})
}
}
10 changes: 10 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ func (c *Client) SetBootDevice(ctx context.Context, bootDevice string, setPersis
return ok, err
}

// SetVirtualMedia controls the virtual media simulated by the BMC as being connected to the
// server. Specifically, the method ejects any currently attached virtual media, and then if
// mediaURL isn't empty, attaches a virtual media device of type kind whose contents are
// streamed from the indicated URL.
func (c *Client) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) {
ok, metadata, err := bmc.SetVirtualMediaFromInterfaces(ctx, kind, mediaURL, c.Registry.GetDriverInterfaces())
c.setMetadata(metadata)
return ok, err
}

// ResetBMC pass through to library function
func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err error) {
ok, metadata, err := bmc.ResetBMCFromInterfaces(ctx, resetType, c.Registry.GetDriverInterfaces())
Expand Down
76 changes: 76 additions & 0 deletions internal/redfishwrapper/virtual_media.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package redfishwrapper

import (
"context"
"fmt"

"github.com/pkg/errors"
rf "github.com/stmcginnis/gofish/redfish"
)

// Set the virtual media attached to the system, or just eject everything if mediaURL is empty.
func (c *Client) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) {
managers, err := c.Managers(ctx)
if err != nil {
return false, err
}

var mediaKind rf.VirtualMediaType
switch kind {
case "CD":
mediaKind = rf.CDMediaType
case "Floppy":
mediaKind = rf.FloppyMediaType
case "USBStick":
mediaKind = rf.USBStickMediaType
case "DVD":
mediaKind = rf.DVDMediaType
default:
return false, errors.New("invalid media type")
}

for _, manager := range managers {
virtualMedia, err := manager.VirtualMedia()
if err != nil {
return false, err
}
for _, media := range virtualMedia {
if media.Inserted {
err = media.EjectMedia()
joelrebel marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false, err
}
}
}
}

// An empty mediaURL means eject everything, so if that's the case we're done. Otherwise, we
// need to insert the media.
if mediaURL != "" {
setMedia := false
for _, manager := range managers {
virtualMedia, err := manager.VirtualMedia()
if err != nil {
return false, err
}

for _, media := range virtualMedia {
for _, t := range media.MediaTypes {
if t == mediaKind {
err = media.InsertMedia(mediaURL, true, true)
if err != nil {
return false, err
}
setMedia = true
break
}
}
}
}
if !setMedia {
return false, fmt.Errorf("media kind %s not supported", kind)
}
}

return true, nil
}
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
FeatureBmcReset registrar.Feature = "bmcreset"
// FeatureBootDeviceSet means an implementation the next boot device
FeatureBootDeviceSet registrar.Feature = "bootdeviceset"
// FeaturesVirtualMedia means an implementation can manage virtual media devices
FeatureVirtualMedia registrar.Feature = "virtualmedia"
// FeatureFirmwareInstall means an implementation that initiates the firmware install process
FeatureFirmwareInstall registrar.Feature = "firmwareinstall"
// FeatureFirmwareInstallSatus means an implementation that returns the firmware install status
Expand Down
6 changes: 6 additions & 0 deletions providers/redfish/redfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
providers.FeatureUserUpdate,
providers.FeatureUserDelete,
providers.FeatureBootDeviceSet,
providers.FeatureVirtualMedia,
providers.FeatureInventoryRead,
providers.FeatureFirmwareInstall,
providers.FeatureFirmwareInstallStatus,
Expand Down Expand Up @@ -150,3 +151,8 @@ func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error)
func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) {
return c.redfishwrapper.SystemBootDeviceSet(ctx, bootDevice, setPersistent, efiBoot)
}

// SetVirtualMedia sets the virtual media
func (c *Conn) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) {
return c.redfishwrapper.SetVirtualMedia(ctx, kind, mediaURL)
}