From 0a9e833f5cab610f20abd6876d20a189bdd67dbb Mon Sep 17 00:00:00 2001 From: Guillaume Carre Date: Tue, 16 Jun 2026 17:05:29 +0200 Subject: [PATCH 1/2] feat(storcli2): cache options setter and JBOD setters Implement the storcli2/perccli2 write path for ARTESCA-17649: - SetLVCacheOptions on the logicalvolumemanager StorCLI2: diffs current vs desired and emits only the changed rdcache/wrcache flags as separate "set" commands (storcli2 rejects the combined syntax and dropped the IO policy). - EnableJBOD / DisableJBOD on the physicaldrivegetter StorCLI2 via "set jbod" / "set uconf" (storcli's "delete jbod" no longer parses). Both surface in-JSON failures through storcli2.Decode regardless of exit code. Widen the shared envelope's DetailedStatus.ErrCd to "any": storcli2 reports it as an int on failure and the string "-" on success, mirroring PID/VD. Issue: ARTESCA-17649 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../logicalvolumemanager/storcli2.go | 147 +++++++++++++ .../logicalvolumemanager/storcli2_test.go | 198 ++++++++++++++++++ .../physicaldrivegetter/storcli2_jbod.go | 61 ++++++ .../physicaldrivegetter/storcli2_jbod_test.go | 79 +++++++ pkg/implementation/storcli2/envelope.go | 4 +- 5 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 pkg/implementation/logicalvolumemanager/storcli2.go create mode 100644 pkg/implementation/logicalvolumemanager/storcli2_test.go create mode 100644 pkg/implementation/physicaldrivegetter/storcli2_jbod.go create mode 100644 pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go diff --git a/pkg/implementation/logicalvolumemanager/storcli2.go b/pkg/implementation/logicalvolumemanager/storcli2.go new file mode 100644 index 0000000..450de53 --- /dev/null +++ b/pkg/implementation/logicalvolumemanager/storcli2.go @@ -0,0 +1,147 @@ +package logicalvolumemanager + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/scality/raidmgmt/pkg/domain/entities/logicalvolume" + "github.com/scality/raidmgmt/pkg/domain/ports" + "github.com/scality/raidmgmt/pkg/implementation/commandrunner" + "github.com/scality/raidmgmt/pkg/implementation/storcli2" +) + +const ( + // storcli2CmdSet is the storcli2 "set" command token. + storcli2CmdSet = "set" + // storcli2VolumeSelector addresses a single virtual drive by its number. + storcli2VolumeSelector = "/c%d/v%s" + // storcli2RdCacheFlag and storcli2WrCacheFlag are the read/write cache flags + // of the "set" command. storcli2 has no IO policy flag. + storcli2RdCacheFlag = "rdcache=" + storcli2WrCacheFlag = "wrcache=" +) + +// StorCLI2 manages logical volumes through a storcli2/perccli2 command runner. +// A single implementation serves both binaries; the concrete runner is injected +// at construction time. The current state is read through an injected +// LogicalVolumesGetter so setters only emit the flags that actually change. +type StorCLI2 struct { + ports.LogicalVolumesGetter + + StorCLI2 commandrunner.CommandRunner +} + +var _ ports.LVCacheSetter = &StorCLI2{} + +// NewStorCLI2 returns a logical-volume manager backed by the given storcli2 / +// perccli2 command runner and logical-volume getter. +func NewStorCLI2( + runner commandrunner.CommandRunner, + logicalVolumesGetter ports.LogicalVolumesGetter, +) *StorCLI2 { + return &StorCLI2{ + LogicalVolumesGetter: logicalVolumesGetter, + StorCLI2: runner, + } +} + +// SetLVCacheOptions applies the desired cache options to a logical volume, +// emitting only the flags that differ from the current state. storcli2 rejects +// the combined cache syntax and dropped the IO policy, so the read and write +// policies are set through two independent "set" commands and the IO policy is +// ignored. +func (s *StorCLI2) SetLVCacheOptions( + metadata *logicalvolume.Metadata, + desired *logicalvolume.CacheOptions, +) error { + current, err := s.LogicalVolume(metadata) + if err != nil { + return errors.Wrapf(err, "failed to get logical volume %s", metadata.ID) + } + + options, err := storcli2CacheOptions(current.CacheOptions, desired) + if err != nil { + return errors.Wrap(err, "failed to resolve cache options") + } + + selector := fmt.Sprintf(storcli2VolumeSelector, metadata.CtrlMetadata.ID, metadata.ID) + + for _, option := range options { + if err := s.set(selector, option); err != nil { + return errors.Wrapf(err, "failed to set %s", option) + } + } + + return nil +} + +// storcli2CacheOptions returns the "set" options for the policies that differ +// between current and desired, one per command (storcli2 rejects the combined +// syntax). A changed but unsettable (unknown) policy is an error. +func storcli2CacheOptions(current, desired *logicalvolume.CacheOptions) ([]string, error) { + var options []string + + if desired.ReadPolicy != current.ReadPolicy { + token, ok := storcli2ReadCacheToken(desired.ReadPolicy) + if !ok { + return nil, errors.Errorf("unsettable read policy %q", desired.ReadPolicy) + } + + options = append(options, storcli2RdCacheFlag+token) + } + + if desired.WritePolicy != current.WritePolicy { + token, ok := storcli2WriteCacheToken(desired.WritePolicy) + if !ok { + return nil, errors.Errorf("unsettable write policy %q", desired.WritePolicy) + } + + options = append(options, storcli2WrCacheFlag+token) + } + + return options, nil +} + +// set runs a single "set" command on a volume selector and surfaces the in-JSON +// failure that storcli2 may report regardless of its exit code. +func (s *StorCLI2) set(selector, option string) error { + output, err := s.StorCLI2.Run([]string{selector, storcli2CmdSet, option}) + if err != nil { + return errors.Wrap(err, "failed to run set command") + } + + if _, err := storcli2.Decode(output); err != nil { + return errors.Wrap(err, "set command failed") + } + + return nil +} + +// storcli2ReadCacheToken maps a read policy to its "rdcache" token. An unknown +// policy is not settable and yields ok=false. +func storcli2ReadCacheToken(policy logicalvolume.ReadPolicy) (string, bool) { + switch policy { //nolint:exhaustive // unknown handled by the default + case logicalvolume.ReadPolicyReadAhead: + return "RA", true + case logicalvolume.ReadPolicyNoReadAhead: + return "NoRA", true + default: + return "", false + } +} + +// storcli2WriteCacheToken maps a write policy to its "wrcache" token. An unknown +// policy is not settable and yields ok=false. +func storcli2WriteCacheToken(policy logicalvolume.WritePolicy) (string, bool) { + switch policy { //nolint:exhaustive // unknown handled by the default + case logicalvolume.WritePolicyWriteThrough: + return "WT", true + case logicalvolume.WritePolicyWriteBack: + return "WB", true + case logicalvolume.WritePolicyAlwaysWriteBack: + return "AWB", true + default: + return "", false + } +} diff --git a/pkg/implementation/logicalvolumemanager/storcli2_test.go b/pkg/implementation/logicalvolumemanager/storcli2_test.go new file mode 100644 index 0000000..c570f6c --- /dev/null +++ b/pkg/implementation/logicalvolumemanager/storcli2_test.go @@ -0,0 +1,198 @@ +package logicalvolumemanager_test + +import ( + "os" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/pkg/domain/entities/logicalvolume" + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" + "github.com/scality/raidmgmt/pkg/implementation/logicalvolumemanager" +) + +// 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 +} + +func storcli2Metadata() *logicalvolume.Metadata { + return &logicalvolume.Metadata{ + CtrlMetadata: &raidcontroller.Metadata{ID: 0}, + ID: "25", + } +} + +func newStorCLI2LV(cache *logicalvolume.CacheOptions) *logicalvolume.LogicalVolume { + return &logicalvolume.LogicalVolume{ + Metadata: storcli2Metadata(), + CacheOptions: cache, + } +} + +// TestStorCLI2SetLVCacheOptions covers the only-changed-flag behavior: each +// policy is set through its own command, and an unchanged policy emits no +// command at all. +func TestStorCLI2SetLVCacheOptions(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + current *logicalvolume.CacheOptions + desired *logicalvolume.CacheOptions + // calls maps the expected "set" option to the fixture it returns. + calls map[string]string + }{ + { + name: "only read changed", + current: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyNoReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }, + desired: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }, + calls: map[string]string{"rdcache=RA": "cacheoptions/success_rdcache.json"}, + }, + { + name: "only write changed", + current: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }, + desired: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteThrough, + }, + calls: map[string]string{"wrcache=WT": "cacheoptions/success_wrcache.json"}, + }, + { + name: "both changed", + current: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyNoReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }, + desired: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteThrough, + }, + calls: map[string]string{ + "rdcache=RA": "cacheoptions/success_rdcache.json", + "wrcache=WT": "cacheoptions/success_wrcache.json", + }, + }, + { + name: "nothing changed", + current: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteThrough, + }, + desired: &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteThrough, + }, + calls: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockGetter := new(MockLogicalVolumesGetter) + + metadata := storcli2Metadata() + mockGetter.On("LogicalVolume", metadata).Return(newStorCLI2LV(tt.current), nil) + + for option, fixture := range tt.calls { + mockRunner.On("Run", []string{"/c0/v25", "set", option}). + Return(storcli2Fixture(t, fixture), nil) + } + + manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + + err := manager.SetLVCacheOptions(metadata, tt.desired) + require.NoError(t, err) + + mockRunner.AssertExpectations(t) + mockRunner.AssertNumberOfCalls(t, "Run", len(tt.calls)) + }) + } +} + +// TestStorCLI2SetLVCacheOptionsGetterError pins that a failure to read the +// current state aborts before any command is run. +func TestStorCLI2SetLVCacheOptionsGetterError(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockGetter := new(MockLogicalVolumesGetter) + + metadata := storcli2Metadata() + mockGetter.On("LogicalVolume", metadata). + Return((*logicalvolume.LogicalVolume)(nil), errors.New("boom")) + + manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + + err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + }) + require.Error(t, err) + mockRunner.AssertNotCalled(t, "Run") +} + +// TestStorCLI2SetLVCacheOptionsCommandError pins that an in-JSON command +// failure (here storcli's rejected combined syntax, kept as a plain-text +// failure fixture) is surfaced. +func TestStorCLI2SetLVCacheOptionsCommandError(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockGetter := new(MockLogicalVolumesGetter) + + metadata := storcli2Metadata() + mockGetter.On("LogicalVolume", metadata).Return(newStorCLI2LV(&logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyNoReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }), nil) + mockRunner.On("Run", []string{"/c0/v25", "set", "rdcache=RA"}). + Return(storcli2Fixture(t, "cacheoptions/combined_syntax_error.json"), nil) + + manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + + err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + WritePolicy: logicalvolume.WritePolicyWriteBack, + }) + require.Error(t, err) +} + +// TestStorCLI2SetLVCacheOptionsUnsettable pins that an unknown desired policy +// (e.g. round-tripped from getter output) is rejected rather than emitted. +func TestStorCLI2SetLVCacheOptionsUnsettable(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockGetter := new(MockLogicalVolumesGetter) + + metadata := storcli2Metadata() + mockGetter.On("LogicalVolume", metadata).Return(newStorCLI2LV(&logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyReadAhead, + }), nil) + + manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + + err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + ReadPolicy: logicalvolume.ReadPolicyUnknown, + }) + require.Error(t, err) + mockRunner.AssertNotCalled(t, "Run") +} diff --git a/pkg/implementation/physicaldrivegetter/storcli2_jbod.go b/pkg/implementation/physicaldrivegetter/storcli2_jbod.go new file mode 100644 index 0000000..d3b5197 --- /dev/null +++ b/pkg/implementation/physicaldrivegetter/storcli2_jbod.go @@ -0,0 +1,61 @@ +package physicaldrivegetter + +import ( + "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/storcli2" +) + +const ( + // storcli2CmdSet is the storcli2 "set" command token. + storcli2CmdSet = "set" + // storcli2JBODOption converts a drive to the JBOD state. + storcli2JBODOption = "jbod" + // storcli2UConfOption converts a drive back to the unconfigured state; + // storcli2 dropped storcli's "delete jbod". + storcli2UConfOption = "uconf" +) + +var _ ports.JBODSetter = &StorCLI2{} + +// EnableJBOD converts a drive to the JBOD state ("set jbod"). It changes only +// the drive state, not its status. +func (s *StorCLI2) EnableJBOD(metadata *physicaldrive.Metadata) error { + if err := s.setDriveState(metadata, storcli2JBODOption); err != nil { + return errors.Wrap(err, "failed to enable JBOD") + } + + return nil +} + +// DisableJBOD converts a JBOD drive back to the unconfigured state +// ("set uconf"); storcli's "delete jbod" no longer parses. +func (s *StorCLI2) DisableJBOD(metadata *physicaldrive.Metadata) error { + if err := s.setDriveState(metadata, storcli2UConfOption); err != nil { + return errors.Wrap(err, "failed to disable JBOD") + } + + return nil +} + +// setDriveState runs "set " on a drive selector and surfaces the in-JSON +// failure that storcli2 may report regardless of its exit code. +func (s *StorCLI2) setDriveState(metadata *physicaldrive.Metadata, state 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, storcli2CmdSet, state}) + if err != nil { + return errors.Wrapf(err, "failed to run set %s command", state) + } + + if _, err := storcli2.Decode(output); err != nil { + return errors.Wrapf(err, "set %s command failed", state) + } + + return nil +} diff --git a/pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go b/pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go new file mode 100644 index 0000000..600ddfd --- /dev/null +++ b/pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go @@ -0,0 +1,79 @@ +package physicaldrivegetter + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/pkg/domain/entities/physicaldrive" + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" +) + +// storcli2Success is a minimal success envelope; the captured JBOD fixtures are +// failures only (a success needs hardware, see ARTESCA-17649). +const storcli2Success = `{"Controllers":[{"Command Status":{"Status":"Success"}}]}` + +func storcli2PDMetadata() *physicaldrive.Metadata { + return &physicaldrive.Metadata{ + CtrlMetadata: &raidcontroller.Metadata{ID: 0}, + ID: "306:0", + } +} + +func TestStorCLI2EnableJBOD(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockRunner.On("Run", []string{"/c0/e306/s0", "set", "jbod"}). + Return([]byte(storcli2Success), nil) + + s := NewStorCLI2(mockRunner) + + require.NoError(t, s.EnableJBOD(storcli2PDMetadata())) + mockRunner.AssertExpectations(t) +} + +func TestStorCLI2DisableJBOD(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockRunner.On("Run", []string{"/c0/e306/s0", "set", "uconf"}). + Return([]byte(storcli2Success), nil) + + s := NewStorCLI2(mockRunner) + + require.NoError(t, s.DisableJBOD(storcli2PDMetadata())) + mockRunner.AssertExpectations(t) +} + +// TestStorCLI2EnableJBODFailure pins that the in-JSON failure payload reported +// regardless of exit code is surfaced as an error. +func TestStorCLI2EnableJBODFailure(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + mockRunner.On("Run", []string{"/c0/e306/s0", "set", "jbod"}). + Return(storcli2Fixture(t, "jbod/enable/fail.json"), nil) + + s := NewStorCLI2(mockRunner) + + err := s.EnableJBOD(storcli2PDMetadata()) + require.Error(t, err) + require.ErrorContains(t, err, "wrong state") +} + +// TestStorCLI2JBODSelectorError pins that an unparseable slot aborts before any +// command is run. +func TestStorCLI2JBODSelectorError(t *testing.T) { + t.Parallel() + + mockRunner := new(MockCommandRunner) + s := NewStorCLI2(mockRunner) + + err := s.EnableJBOD(&physicaldrive.Metadata{ + CtrlMetadata: &raidcontroller.Metadata{ID: 0}, + ID: "", + }) + require.Error(t, err) + mockRunner.AssertNotCalled(t, "Run") +} diff --git a/pkg/implementation/storcli2/envelope.go b/pkg/implementation/storcli2/envelope.go index 68e92e1..9ba3bd8 100644 --- a/pkg/implementation/storcli2/envelope.go +++ b/pkg/implementation/storcli2/envelope.go @@ -39,13 +39,15 @@ type ( // DetailedStatus carries per-target error details. storcli2 adds an // "ErrType" field and identifies the target either by "VD" or by // "EID:Slt"/"PID" (PID may be the int persistent id or the string "-"). + // ErrCd is likewise "any": an int error code on failure, the string "-" on + // success. DetailedStatus struct { VD any `json:"VD,omitempty"` EIDSlot string `json:"EID:Slt,omitempty"` PID any `json:"PID,omitempty"` Status string `json:"Status"` ErrType string `json:"ErrType,omitempty"` - ErrCd int `json:"ErrCd"` + ErrCd any `json:"ErrCd"` ErrMsg string `json:"ErrMsg"` } ) From 185c9090e739fbe76220d574fb22956419d94ce2 Mon Sep 17 00:00:00 2001 From: Guillaume Carre Date: Mon, 29 Jun 2026 19:55:40 +0200 Subject: [PATCH 2/2] refactor(storcli2): move JBOD and cache setters to per-port packages Relocate the storcli2 JBOD setter and LV cache setter into their own packages named after their RAIDController port interfaces (jbodsetter, lvcachesetter), instead of folding them into physicaldrivegetter and logicalvolumemanager. This matches the decomposed one-package-per-port pattern already used by the getters and blinker, and makes each setter self-contained. Also document the conventions so future work follows suit: - CLAUDE.md: package layout mirrors the ports. - DESIGN.md: per-port packages; adapter token mapping expresses the per-controller settable subset (fails closed on new enum values); read-before-write emits only changed flags. - tests/testdata-tools: repoint fixture paths to the new packages. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 5 ++ DESIGN.md | 27 ++++++++-- .../storcli2.go} | 42 +++++++++++++++- .../storcli2_test.go} | 24 ++++++++- .../testdata/storcli2/jbod/disable/fail.json | 0 .../testdata/storcli2/jbod/enable/fail.json | 0 .../storcli2.go | 20 +++++--- .../storcli2_test.go | 49 +++++++++++++++---- .../cacheoptions/combined_syntax_error.json | 0 .../cacheoptions/success_rdcache.json | 0 .../cacheoptions/success_wrcache.json | 0 tests/testdata-tools/README.md | 10 ++-- .../collect_storcli2_testdata.sh | 24 ++++----- 13 files changed, 160 insertions(+), 41 deletions(-) rename pkg/implementation/{physicaldrivegetter/storcli2_jbod.go => jbodsetter/storcli2.go} (58%) rename pkg/implementation/{physicaldrivegetter/storcli2_jbod_test.go => jbodsetter/storcli2_test.go} (81%) rename pkg/implementation/{physicaldrivegetter => jbodsetter}/testdata/storcli2/jbod/disable/fail.json (100%) rename pkg/implementation/{physicaldrivegetter => jbodsetter}/testdata/storcli2/jbod/enable/fail.json (100%) rename pkg/implementation/{logicalvolumemanager => lvcachesetter}/storcli2.go (83%) rename pkg/implementation/{logicalvolumemanager => lvcachesetter}/storcli2_test.go (80%) rename pkg/implementation/{logicalvolumemanager => lvcachesetter}/testdata/storcli2/cacheoptions/combined_syntax_error.json (100%) rename pkg/implementation/{logicalvolumemanager => lvcachesetter}/testdata/storcli2/cacheoptions/success_rdcache.json (100%) rename pkg/implementation/{logicalvolumemanager => lvcachesetter}/testdata/storcli2/cacheoptions/success_wrcache.json (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 0bf6271..38603df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,11 @@ It follows **Hexagonal Architecture** (see `DESIGN.md`): family (MegaRAID/storcli2, Dell PERC/perccli2, HPE Smart Array/ssacli, mdadm software RAID on RHEL8). +**Package layout mirrors the ports.** Each fine-grained port interface gets one +self-contained implementation package, named after it; a new operation goes in +the package for its port, not folded into another. New adapters follow this +decomposition rather than the older monolithic `raidcontroller/megaraid` package. + Key characteristics: - Adapters **shell out to vendor CLI tools** and parse their JSON/text output diff --git a/DESIGN.md b/DESIGN.md index 3a9d219..ab602ea 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -112,6 +112,21 @@ The main port is `RAIDController`, which composes several fine-grained interface Not all adapters support every operation. Unsupported operations return `ErrFunctionNotSupportedByImplementation`. +The same applies at the value level: an enum (e.g. `ReadPolicy`, `WritePolicy`) +is the **union** of what all controllers support, so each adapter handles a +**subset**. Adapters translate domain enum values to vendor CLI tokens through a +small mapping function that returns an "unsettable" signal (e.g. `(token string, +ok bool)`) for values it cannot express, failing closed via an exhaustive +`switch` default. This is distinct from entity `Validate()` (which guards +domain-level coherence): the mapping guards the adapter boundary, keeps vendor +vocabulary out of the domain, and rejects values a controller cannot realize — +including ones added later for other controllers. + +Relatedly, setters read current state before writing and emit only the changed +flags. This is not for idempotency but to minimize real mutations and to skip +fields the (lossy) getter reports as `Unknown` when the caller did not change +them — avoiding a spurious "unsettable" rejection on an untouched field. + ### Adapters #### MegaRAID / PERC (storcli, perccli) @@ -168,9 +183,11 @@ Design notes, verified against the StorCLI2 User Guide and a live MegaRAID option -- the IO policy of parsed volumes is always `Unknown`. The read path (controller, physical drive and logical volume getters), the -shared envelope/decoder and both command runners are implemented. The -remaining ports follow the same component pattern; the table below maps each -port operation to its storcli2 command (verified against the StorCLI2 User +cache and JBOD setters (`lvcachesetter`, `jbodsetter`), the shared +envelope/decoder and both command runners are implemented -- each in its own +package named after its port. The remaining ports follow the same component +pattern; the table below maps each port operation to its storcli2 command +(verified against the StorCLI2 User Guide, the official storcli-to-storcli2 command map of the MegaRAID 8 software guide, and the binary's own help -- the grammar differs from storcli in several places). @@ -193,8 +210,8 @@ since only the injected runner differs. Until then the components are wired individually. > **Note:** Part of the pre-staged write-path fixtures under -> `pkg/implementation/logicalvolumemanager/testdata/storcli2/` and -> `pkg/implementation/physicaldrivegetter/testdata/storcli2/jbod/` were +> `pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/` and +> `pkg/implementation/jbodsetter/testdata/storcli2/jbod/` were > captured with the storcli grammar and are plain-text syntax errors; they > must be regenerated with the commands above using > `tests/testdata-tools/collect_storcli2_testdata.sh` (DESTRUCTIVE mode). diff --git a/pkg/implementation/physicaldrivegetter/storcli2_jbod.go b/pkg/implementation/jbodsetter/storcli2.go similarity index 58% rename from pkg/implementation/physicaldrivegetter/storcli2_jbod.go rename to pkg/implementation/jbodsetter/storcli2.go index d3b5197..29f8d8d 100644 --- a/pkg/implementation/physicaldrivegetter/storcli2_jbod.go +++ b/pkg/implementation/jbodsetter/storcli2.go @@ -1,10 +1,13 @@ -package physicaldrivegetter +package jbodsetter 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" ) @@ -16,10 +19,30 @@ const ( // storcli2UConfOption converts a drive back to the unconfigured state; // storcli2 dropped storcli's "delete jbod". storcli2UConfOption = "uconf" + + // 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 sets the JBOD state of a physical drive 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.JBODSetter = &StorCLI2{} +// NewStorCLI2 returns a JBOD setter backed by the given storcli2 / perccli2 +// command runner. +func NewStorCLI2(runner commandrunner.CommandRunner) *StorCLI2 { + return &StorCLI2{ + runner: runner, + } +} + // EnableJBOD converts a drive to the JBOD state ("set jbod"). It changes only // the drive state, not its status. func (s *StorCLI2) EnableJBOD(metadata *physicaldrive.Metadata) error { @@ -59,3 +82,20 @@ func (s *StorCLI2) setDriveState(metadata *physicaldrive.Metadata, state string) 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 +} diff --git a/pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go b/pkg/implementation/jbodsetter/storcli2_test.go similarity index 81% rename from pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go rename to pkg/implementation/jbodsetter/storcli2_test.go index 600ddfd..60865cf 100644 --- a/pkg/implementation/physicaldrivegetter/storcli2_jbod_test.go +++ b/pkg/implementation/jbodsetter/storcli2_test.go @@ -1,14 +1,36 @@ -package physicaldrivegetter +package jbodsetter import ( + "os" "testing" + "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" ) +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 +} + // storcli2Success is a minimal success envelope; the captured JBOD fixtures are // failures only (a success needs hardware, see ARTESCA-17649). const storcli2Success = `{"Controllers":[{"Command Status":{"Status":"Success"}}]}` diff --git a/pkg/implementation/physicaldrivegetter/testdata/storcli2/jbod/disable/fail.json b/pkg/implementation/jbodsetter/testdata/storcli2/jbod/disable/fail.json similarity index 100% rename from pkg/implementation/physicaldrivegetter/testdata/storcli2/jbod/disable/fail.json rename to pkg/implementation/jbodsetter/testdata/storcli2/jbod/disable/fail.json diff --git a/pkg/implementation/physicaldrivegetter/testdata/storcli2/jbod/enable/fail.json b/pkg/implementation/jbodsetter/testdata/storcli2/jbod/enable/fail.json similarity index 100% rename from pkg/implementation/physicaldrivegetter/testdata/storcli2/jbod/enable/fail.json rename to pkg/implementation/jbodsetter/testdata/storcli2/jbod/enable/fail.json diff --git a/pkg/implementation/logicalvolumemanager/storcli2.go b/pkg/implementation/lvcachesetter/storcli2.go similarity index 83% rename from pkg/implementation/logicalvolumemanager/storcli2.go rename to pkg/implementation/lvcachesetter/storcli2.go index 450de53..5a2e6a7 100644 --- a/pkg/implementation/logicalvolumemanager/storcli2.go +++ b/pkg/implementation/lvcachesetter/storcli2.go @@ -1,4 +1,4 @@ -package logicalvolumemanager +package lvcachesetter import ( "fmt" @@ -22,10 +22,11 @@ const ( storcli2WrCacheFlag = "wrcache=" ) -// StorCLI2 manages logical volumes through a storcli2/perccli2 command runner. -// A single implementation serves both binaries; the concrete runner is injected -// at construction time. The current state is read through an injected -// LogicalVolumesGetter so setters only emit the flags that actually change. +// StorCLI2 sets cache options on a logical volume through a storcli2 / +// perccli2 command runner. A single implementation serves both binaries; the +// concrete runner is injected at construction time. The current state is read +// through an injected LogicalVolumesGetter so setters only emit the flags that +// actually change. type StorCLI2 struct { ports.LogicalVolumesGetter @@ -34,8 +35,8 @@ type StorCLI2 struct { var _ ports.LVCacheSetter = &StorCLI2{} -// NewStorCLI2 returns a logical-volume manager backed by the given storcli2 / -// perccli2 command runner and logical-volume getter. +// NewStorCLI2 returns a cache setter backed by the given storcli2 / perccli2 +// command runner and logical-volume getter. func NewStorCLI2( runner commandrunner.CommandRunner, logicalVolumesGetter ports.LogicalVolumesGetter, @@ -55,6 +56,11 @@ func (s *StorCLI2) SetLVCacheOptions( metadata *logicalvolume.Metadata, desired *logicalvolume.CacheOptions, ) error { + // Read-before-write is deliberate: it is not about idempotency (the "set" + // commands are idempotent) but about emitting only the changed flags. This + // minimizes real mutations and, crucially, skips fields the lossy getter + // reports as Unknown (which the token funcs reject as unsettable) when the + // caller did not actually change them. See DESIGN.md § Adapters. current, err := s.LogicalVolume(metadata) if err != nil { return errors.Wrapf(err, "failed to get logical volume %s", metadata.ID) diff --git a/pkg/implementation/logicalvolumemanager/storcli2_test.go b/pkg/implementation/lvcachesetter/storcli2_test.go similarity index 80% rename from pkg/implementation/logicalvolumemanager/storcli2_test.go rename to pkg/implementation/lvcachesetter/storcli2_test.go index c570f6c..1cb315f 100644 --- a/pkg/implementation/logicalvolumemanager/storcli2_test.go +++ b/pkg/implementation/lvcachesetter/storcli2_test.go @@ -1,17 +1,46 @@ -package logicalvolumemanager_test +package lvcachesetter_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/logicalvolume" "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" - "github.com/scality/raidmgmt/pkg/implementation/logicalvolumemanager" + "github.com/scality/raidmgmt/pkg/implementation/lvcachesetter" ) +type ( + MockCommandRunner struct { + mock.Mock + } + + MockLogicalVolumesGetter struct { + mock.Mock + } +) + +func (m *MockCommandRunner) Run(args []string) ([]byte, error) { + arguments := m.Called(args) + + return arguments.Get(0).([]byte), arguments.Error(1) +} + +func (m *MockLogicalVolumesGetter) LogicalVolumes(metadata *raidcontroller.Metadata) ([]*logicalvolume.LogicalVolume, error) { + arguments := m.Called(metadata) + + return arguments.Get(0).([]*logicalvolume.LogicalVolume), arguments.Error(1) +} + +func (m *MockLogicalVolumesGetter) LogicalVolume(metadata *logicalvolume.Metadata) (*logicalvolume.LogicalVolume, error) { + arguments := m.Called(metadata) + + return arguments.Get(0).(*logicalvolume.LogicalVolume), arguments.Error(1) +} + // storcli2Fixture reads a storcli2 JSON fixture from the package testdata. func storcli2Fixture(t *testing.T, name string) []byte { t.Helper() @@ -117,9 +146,9 @@ func TestStorCLI2SetLVCacheOptions(t *testing.T) { Return(storcli2Fixture(t, fixture), nil) } - manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + setter := lvcachesetter.NewStorCLI2(mockRunner, mockGetter) - err := manager.SetLVCacheOptions(metadata, tt.desired) + err := setter.SetLVCacheOptions(metadata, tt.desired) require.NoError(t, err) mockRunner.AssertExpectations(t) @@ -140,9 +169,9 @@ func TestStorCLI2SetLVCacheOptionsGetterError(t *testing.T) { mockGetter.On("LogicalVolume", metadata). Return((*logicalvolume.LogicalVolume)(nil), errors.New("boom")) - manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + setter := lvcachesetter.NewStorCLI2(mockRunner, mockGetter) - err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + err := setter.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ ReadPolicy: logicalvolume.ReadPolicyReadAhead, }) require.Error(t, err) @@ -166,9 +195,9 @@ func TestStorCLI2SetLVCacheOptionsCommandError(t *testing.T) { mockRunner.On("Run", []string{"/c0/v25", "set", "rdcache=RA"}). Return(storcli2Fixture(t, "cacheoptions/combined_syntax_error.json"), nil) - manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + setter := lvcachesetter.NewStorCLI2(mockRunner, mockGetter) - err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + err := setter.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ ReadPolicy: logicalvolume.ReadPolicyReadAhead, WritePolicy: logicalvolume.WritePolicyWriteBack, }) @@ -188,9 +217,9 @@ func TestStorCLI2SetLVCacheOptionsUnsettable(t *testing.T) { ReadPolicy: logicalvolume.ReadPolicyReadAhead, }), nil) - manager := logicalvolumemanager.NewStorCLI2(mockRunner, mockGetter) + setter := lvcachesetter.NewStorCLI2(mockRunner, mockGetter) - err := manager.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ + err := setter.SetLVCacheOptions(metadata, &logicalvolume.CacheOptions{ ReadPolicy: logicalvolume.ReadPolicyUnknown, }) require.Error(t, err) diff --git a/pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/combined_syntax_error.json b/pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/combined_syntax_error.json similarity index 100% rename from pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/combined_syntax_error.json rename to pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/combined_syntax_error.json diff --git a/pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/success_rdcache.json b/pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/success_rdcache.json similarity index 100% rename from pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/success_rdcache.json rename to pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/success_rdcache.json diff --git a/pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/success_wrcache.json b/pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/success_wrcache.json similarity index 100% rename from pkg/implementation/logicalvolumemanager/testdata/storcli2/cacheoptions/success_wrcache.json rename to pkg/implementation/lvcachesetter/testdata/storcli2/cacheoptions/success_wrcache.json diff --git a/tests/testdata-tools/README.md b/tests/testdata-tools/README.md index 1b27580..ebccb22 100644 --- a/tests/testdata-tools/README.md +++ b/tests/testdata-tools/README.md @@ -46,15 +46,15 @@ The script writes each fixture under its owning component package's | `physicaldrivegetter` | `testdata/storcli2/show/e3{06,20}sN.json` | `/c0/e3XX/sN show all` | safe | | `physicaldrivegetter` | `testdata/storcli2/show/e306s99_invalid.json` | drive not found | safe | | `physicaldrivegetter` | `testdata/storcli2/show/e320s11_UGood.json` | drive in unconfigured-good state | destructive | -| `physicaldrivegetter` | `testdata/storcli2/jbod/{enable,disable}/fail.json` | `set jbod` / `delete jbod` | destructive | +| `jbodsetter` | `testdata/storcli2/jbod/{enable,disable}/fail.json` | `set jbod` / `delete jbod` | destructive | | `logicalvolumegetter` | `testdata/storcli2/show/all.json` | `/c0/vall show all` | safe | | `logicalvolumegetter` | `testdata/storcli2/show/vN.json` | `/c0/vN show all` | safe | | `logicalvolumegetter` | `testdata/storcli2/show/v999_invalid.json` | VD not found | safe | | `logicalvolumemanager` | `testdata/storcli2/create/{success,fail}.json` | `add vd ...` | destructive | | `logicalvolumemanager` | `testdata/storcli2/delete/{success,fail_invalid,fail_vdNotExist}.json` | `delete` | destructive | | `logicalvolumemanager` | `testdata/storcli2/migrate/fail.json` | `start migrate ...` | destructive | -| `logicalvolumemanager` | `testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json` | `set wrcache/rdcache` | destructive | -| `logicalvolumemanager` | `testdata/storcli2/cacheoptions/combined_syntax_error.json` | v1 combined `set` syntax (rejected) | destructive | +| `lvcachesetter` | `testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json` | `set wrcache/rdcache` | destructive | +| `lvcachesetter` | `testdata/storcli2/cacheoptions/combined_syntax_error.json` | v1 combined `set` syntax (rejected) | destructive | | `blinker` | `testdata/storcli2/{start,stop}.json` | `start locate` / `stop locate` | destructive | The envelope / decoder unit tests in `pkg/implementation/storcli2` keep their own @@ -66,11 +66,11 @@ The following fixtures were captured as plain-text syntax errors rather than JSON, because `storcli2` changed the CLI grammar for these commands relative to storcli v1 (the script still uses the v1 syntax): -- `logicalvolumemanager/testdata/storcli2/cacheoptions/combined_syntax_error.json` +- `lvcachesetter/testdata/storcli2/cacheoptions/combined_syntax_error.json` (`unexpected TOKEN_WRITE_CACHE`) - `logicalvolumemanager/testdata/storcli2/migrate/fail.json` (`unexpected TOKEN_MIGRATE`) -- `physicaldrivegetter/testdata/storcli2/jbod/disable/fail.json` +- `jbodsetter/testdata/storcli2/jbod/disable/fail.json` (`unexpected TOKEN_JBOD`) These must be regenerated with the correct storcli2 syntax when the diff --git a/tests/testdata-tools/collect_storcli2_testdata.sh b/tests/testdata-tools/collect_storcli2_testdata.sh index 687b9f9..6999b6b 100755 --- a/tests/testdata-tools/collect_storcli2_testdata.sh +++ b/tests/testdata-tools/collect_storcli2_testdata.sh @@ -118,12 +118,12 @@ fi mkdir -p "${OUTPUT_DIR}/controllergetter/testdata/storcli2" mkdir -p "${OUTPUT_DIR}/physicaldrivegetter/testdata/storcli2/show" mkdir -p "${OUTPUT_DIR}/blinker/testdata/storcli2" -mkdir -p "${OUTPUT_DIR}/physicaldrivegetter/testdata/storcli2/jbod/enable" -mkdir -p "${OUTPUT_DIR}/physicaldrivegetter/testdata/storcli2/jbod/disable" +mkdir -p "${OUTPUT_DIR}/jbodsetter/testdata/storcli2/jbod/enable" +mkdir -p "${OUTPUT_DIR}/jbodsetter/testdata/storcli2/jbod/disable" mkdir -p "${OUTPUT_DIR}/logicalvolumegetter/testdata/storcli2/show" mkdir -p "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/create" mkdir -p "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/delete" -mkdir -p "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/cacheoptions" +mkdir -p "${OUTPUT_DIR}/lvcachesetter/testdata/storcli2/cacheoptions" mkdir -p "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/migrate" # Helper function to run a command and save output @@ -332,7 +332,7 @@ if [ "${DESTRUCTIVE}" = "true" ]; then # Used by: adapter.setJBOD(metadata, "set") → runner.Run(["/c0/e306/s0", "set", "jbod"]) run_and_save \ "Enable JBOD on e${FIRST_ENCLOSURE}/s${FIRST_SLOT} (expected failure - drive in VD)" \ - "${OUTPUT_DIR}/physicaldrivegetter/testdata/storcli2/jbod/enable/fail.json" \ + "${OUTPUT_DIR}/jbodsetter/testdata/storcli2/jbod/enable/fail.json" \ "${C}/e${FIRST_ENCLOSURE}/s${FIRST_SLOT}" set jbod # JBOD disable (expected failure when drive is in a VD) @@ -340,7 +340,7 @@ if [ "${DESTRUCTIVE}" = "true" ]; then # Used by: adapter.setJBOD(metadata, "delete") → runner.Run(["/c0/e306/s0", "delete", "jbod"]) run_and_save \ "Disable JBOD on e${FIRST_ENCLOSURE}/s${FIRST_SLOT} (expected failure - drive in VD)" \ - "${OUTPUT_DIR}/physicaldrivegetter/testdata/storcli2/jbod/disable/fail.json" \ + "${OUTPUT_DIR}/jbodsetter/testdata/storcli2/jbod/disable/fail.json" \ "${C}/e${FIRST_ENCLOSURE}/s${FIRST_SLOT}" delete jbod # Delete VD - failure case (VD doesn't exist) @@ -648,7 +648,7 @@ sys.exit(1) echo "" # --- Step 6: Cache options success --- - # logicalvolumemanager/testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json + # lvcachesetter/testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json # storcli2 uses separate commands for each cache option # (v1 combined syntax "set rdcache=RA wrcache=WT" does not work) # Try: storcli2 /c0/v{N} set wrcache=WT J @@ -656,19 +656,19 @@ sys.exit(1) # Used by: adapter.setLVCacheOptions() run_and_save \ "Set write cache on v${NEWEST_VD}" \ - "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/cacheoptions/success_wrcache.json" \ + "${OUTPUT_DIR}/lvcachesetter/testdata/storcli2/cacheoptions/success_wrcache.json" \ "${C}/v${NEWEST_VD}" set wrcache=WT run_and_save \ "Set read cache on v${NEWEST_VD}" \ - "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/cacheoptions/success_rdcache.json" \ + "${OUTPUT_DIR}/lvcachesetter/testdata/storcli2/cacheoptions/success_rdcache.json" \ "${C}/v${NEWEST_VD}" set rdcache=RA # Also try the v1 combined syntax, which storcli2 rejects with a # plain-text syntax error — captured for documentation purposes. run_and_save \ "Set cache options combined on v${NEWEST_VD} (expected syntax error in storcli2)" \ - "${OUTPUT_DIR}/logicalvolumemanager/testdata/storcli2/cacheoptions/combined_syntax_error.json" \ + "${OUTPUT_DIR}/lvcachesetter/testdata/storcli2/cacheoptions/combined_syntax_error.json" \ "${C}/v${NEWEST_VD}" set rdcache=RA wrcache=WT else echo " [ERROR] Could not determine newly created VD ID" @@ -705,15 +705,15 @@ else echo " The following files need DESTRUCTIVE mode to capture:" echo " - controllergetter/testdata/storcli2/c0_UGood.json (controller with UGood drive)" echo " - physicaldrivegetter/testdata/storcli2/show/e{EID}s{SLOT}_UGood.json" - echo " - physicaldrivegetter/testdata/storcli2/jbod/{enable,disable}/fail.json" + echo " - jbodsetter/testdata/storcli2/jbod/{enable,disable}/fail.json" echo " - blinker/testdata/storcli2/{start,stop}.json" echo " - logicalvolumemanager/testdata/storcli2/create/success.json" echo " - logicalvolumemanager/testdata/storcli2/create/fail.json" echo " - logicalvolumemanager/testdata/storcli2/delete/success.json" echo " - logicalvolumemanager/testdata/storcli2/delete/fail_invalid.json" echo " - logicalvolumemanager/testdata/storcli2/delete/fail_vdNotExist.json" - echo " - logicalvolumemanager/testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json" - echo " - logicalvolumemanager/testdata/storcli2/cacheoptions/combined_syntax_error.json" + echo " - lvcachesetter/testdata/storcli2/cacheoptions/success_{wrcache,rdcache}.json" + echo " - lvcachesetter/testdata/storcli2/cacheoptions/combined_syntax_error.json" echo " - logicalvolumemanager/testdata/storcli2/migrate/fail.json" echo "" fi