-
Notifications
You must be signed in to change notification settings - Fork 0
feat(storcli2): cache options setter and JBOD setters #71
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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) | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
— Claude Code |
||||||||||||||
| 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 | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exported field
StorCLI2shadows the struct type name, which makes usage sites (e.g.s.StorCLI2.Run(...)) ambiguous to read. The existingphysicaldrivegetter.StorCLI2uses an unexportedrunnerfield for the same role — matching that convention would be more consistent and keep the command runner out of the public API surface.— Claude Code