Skip to content
Open
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
96 changes: 96 additions & 0 deletions pkg/implementation/blinker/storcli2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package blinker

import (
"fmt"

"github.com/pkg/errors"

"github.com/scality/raidmgmt/pkg/domain/entities/physicaldrive"
"github.com/scality/raidmgmt/pkg/domain/ports"
"github.com/scality/raidmgmt/pkg/implementation/commandrunner"
"github.com/scality/raidmgmt/pkg/implementation/storcli2"
)

const (
// storcli2CmdStart and storcli2CmdStop begin and end a drive locate.
storcli2CmdStart = "start"
storcli2CmdStop = "stop"
// storcli2CmdLocate is the locate (blink) operation token.
storcli2CmdLocate = "locate"
// storcli2EnclosureSelector and storcli2NoEnclosureSelector address a single
// drive, with or without an enclosure component.
storcli2EnclosureSelector = "/c%d/e%s/s%s"
storcli2NoEnclosureSelector = "/c%d/s%s"
)

// StorCLI2 blinks physical drives through a storcli2/perccli2 command runner. A
// single implementation serves both binaries; the concrete runner is injected
// at construction time.
type StorCLI2 struct {
runner commandrunner.CommandRunner
}

var _ ports.Blinker = &StorCLI2{}

// NewStorCLI2 returns a blinker backed by the given storcli2 / perccli2 command
// runner.
func NewStorCLI2(runner commandrunner.CommandRunner) *StorCLI2 {
return &StorCLI2{
runner: runner,
}
}

// StartBlink starts locating (blinking) a physical drive.
func (s *StorCLI2) StartBlink(metadata *physicaldrive.Metadata) error {
if err := s.locate(metadata, storcli2CmdStart); err != nil {
return errors.Wrap(err, "failed to start blinking physical drive")
}

return nil
}

// StopBlink stops locating (blinking) a physical drive.
func (s *StorCLI2) StopBlink(metadata *physicaldrive.Metadata) error {
if err := s.locate(metadata, storcli2CmdStop); err != nil {
return errors.Wrap(err, "failed to stop blinking physical drive")
}

return nil
}

// locate runs "<start|stop> locate" on a drive selector and surfaces the
// in-JSON failure that storcli2 may report regardless of its exit code.
func (s *StorCLI2) locate(metadata *physicaldrive.Metadata, action string) error {
selector, err := storcli2SelectorPD(metadata)
if err != nil {
return errors.Wrap(err, "failed to build drive selector")
}

output, err := s.runner.Run([]string{selector, action, storcli2CmdLocate})
if err != nil {
return errors.Wrapf(err, "failed to run %s locate command", action)
}

if _, err := storcli2.Decode(output); err != nil {
return errors.Wrapf(err, "%s locate command failed", action)
}

return nil
}

// storcli2SelectorPD builds the storcli2 selector for a drive, choosing the
// enclosure or no-enclosure form from its parsed slot.
func storcli2SelectorPD(metadata *physicaldrive.Metadata) (string, error) {
slot, err := physicaldrive.ParseSlot(metadata.ID)
if err != nil {
return "", errors.Wrapf(err, "failed to parse slot %s", metadata.ID)
}

if slot.Enclosure != "" {
return fmt.Sprintf(
storcli2EnclosureSelector, metadata.CtrlMetadata.ID, slot.Enclosure, slot.Bay,
), nil
}

return fmt.Sprintf(storcli2NoEnclosureSelector, metadata.CtrlMetadata.ID, slot.Bay), nil
}
111 changes: 111 additions & 0 deletions pkg/implementation/blinker/storcli2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package blinker_test

import (
"os"
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/scality/raidmgmt/pkg/domain/entities/physicaldrive"
"github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller"
"github.com/scality/raidmgmt/pkg/implementation/blinker"
)

type MockCommandRunner struct {
mock.Mock
}

func (m *MockCommandRunner) Run(args []string) ([]byte, error) {
arguments := m.Called(args)

return arguments.Get(0).([]byte), arguments.Error(1)
}

// storcli2Fixture reads a storcli2 JSON fixture from the package testdata.
func storcli2Fixture(t *testing.T, name string) []byte {
t.Helper()

data, err := os.ReadFile("testdata/storcli2/" + name)
require.NoError(t, err)

return data
}

// storcli2PDMetadata builds physical-drive metadata with the given EID:Slt id.
func storcli2PDMetadata(id string) *physicaldrive.Metadata {
return &physicaldrive.Metadata{
CtrlMetadata: &raidcontroller.Metadata{ID: 0},
ID: id,
}
}

// TestStorCLI2Blink covers the start/stop locate happy paths, including the
// enclosure and no-enclosure selector forms.
func TestStorCLI2Blink(t *testing.T) {
t.Parallel()

tests := []struct {
name string
id string
selector string
action string
fixture string
start bool
}{
{name: "start with enclosure", id: "252:0", selector: "/c0/e252/s0", action: "start", fixture: "start.json", start: true},
{name: "stop with enclosure", id: "252:0", selector: "/c0/e252/s0", action: "stop", fixture: "stop.json", start: false},
{name: "start without enclosure", id: "5", selector: "/c0/s5", action: "start", fixture: "start.json", start: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

mockRunner := new(MockCommandRunner)
mockRunner.On("Run", []string{tt.selector, tt.action, "locate"}).
Return(storcli2Fixture(t, tt.fixture), nil)

b := blinker.NewStorCLI2(mockRunner)

var err error
if tt.start {
err = b.StartBlink(storcli2PDMetadata(tt.id))
} else {
err = b.StopBlink(storcli2PDMetadata(tt.id))
}

require.NoError(t, err)
mockRunner.AssertExpectations(t)
})
}
}

// TestStorCLI2BlinkCommandError pins that a runner failure is surfaced.
func TestStorCLI2BlinkCommandError(t *testing.T) {
t.Parallel()

mockRunner := new(MockCommandRunner)
mockRunner.On("Run", []string{"/c0/e252/s0", "start", "locate"}).
Return([]byte(nil), errors.New("boom"))

b := blinker.NewStorCLI2(mockRunner)

err := b.StartBlink(storcli2PDMetadata("252:0"))
require.Error(t, err)
}

// TestStorCLI2BlinkInvalidSlot pins that an unparseable drive id is rejected
// before any command is run.
func TestStorCLI2BlinkInvalidSlot(t *testing.T) {
t.Parallel()

mockRunner := new(MockCommandRunner)

b := blinker.NewStorCLI2(mockRunner)

err := b.StopBlink(storcli2PDMetadata(""))
require.Error(t, err)
mockRunner.AssertNotCalled(t, "Run")
}
Loading