diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 017c2dbc6..8fcd42df1 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -24,7 +24,7 @@ import ( "github.com/netapp/trident/cli/api" k8sclient "github.com/netapp/trident/cli/k8s_client" tridentconfig "github.com/netapp/trident/config" - "github.com/netapp/trident/internal/nodeprep/validation" + "github.com/netapp/trident/internal/nodeprep/protocol" . "github.com/netapp/trident/logging" "github.com/netapp/trident/utils" "github.com/netapp/trident/utils/crypto" @@ -425,7 +425,7 @@ func processInstallationArguments(_ *cobra.Command) { persistentObjectLabelKey = TridentPersistentObjectLabelKey persistentObjectLabelValue = TridentPersistentObjectLabelValue - nodePrep = validation.FormatProtocols(nodePrep) + nodePrep = protocol.FormatProtocols(nodePrep) } func validateInstallationArguments() error { @@ -436,7 +436,7 @@ func validateInstallationArguments() error { return fmt.Errorf("'%s' is not a valid namespace name; %s", TridentPodNamespace, labelFormat) } - if err := validation.ValidateProtocols(nodePrep); err != nil { + if err := protocol.ValidateProtocols(nodePrep); err != nil { return err } diff --git a/cmd/node_prep/main.go b/cmd/node_prep/main.go index 09c56524c..c2e8f3674 100644 --- a/cmd/node_prep/main.go +++ b/cmd/node_prep/main.go @@ -3,6 +3,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -24,7 +25,15 @@ func main() { "binary": os.Args[0], }).Info("Running Trident node preparation.") - nodeprep.NewNodePrep().PrepareNode(strings.Split(strings.ToLower(*flags.nodePrep), ",")) + ctx := context.Background() + + // This is a work-around for osutils because getting system information checks for this environment variable + // to decide if this is running inside a container. osutils should be refactored to not rely on this. + if err := os.Setenv("CSI_ENDPOINT", "unix://run/csi/socket"); err != nil { + Log().Fatal("Failed to set environment variable.") + } + + os.Exit(nodeprep.New().Prepare(ctx, strings.Split(strings.ToLower(*flags.nodePrep), ","))) } func initLogging(flags appFlags) { diff --git a/internal/nodeprep/execution/executor.go b/internal/nodeprep/execution/executor.go new file mode 100644 index 000000000..a28b2c8aa --- /dev/null +++ b/internal/nodeprep/execution/executor.go @@ -0,0 +1,24 @@ +package execution + +import ( + "context" + + "github.com/netapp/trident/internal/nodeprep/instruction" + . "github.com/netapp/trident/logging" +) + +func Execute(ctx context.Context, instructions []instruction.Instructions) (err error) { + for _, i := range instructions { + Log().WithField("instructions", i.GetName()).Info("Preparing node") + if err = i.PreCheck(ctx); err != nil { + return + } + if err = i.Apply(ctx); err != nil { + return + } + if err = i.PostCheck(ctx); err != nil { + return + } + } + return +} diff --git a/internal/nodeprep/execution/executor_test.go b/internal/nodeprep/execution/executor_test.go new file mode 100644 index 000000000..577f02335 --- /dev/null +++ b/internal/nodeprep/execution/executor_test.go @@ -0,0 +1,126 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package execution_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/execution" + "github.com/netapp/trident/internal/nodeprep/instruction" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_instruction" + "github.com/netapp/trident/utils/errors" +) + +func TestExecute(t *testing.T) { + type parameters struct { + getInstructions func(controller *gomock.Controller) []instruction.Instructions + assertError assert.ErrorAssertionFunc + } + + tests := map[string]parameters{ + "execute single instruction successfully": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction := mock_instruction.NewMockInstructions(controller) + mockInstruction.EXPECT().GetName().Return("mockInstruction") + mockInstruction.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction.EXPECT().Apply(gomock.Any()).Return(nil) + mockInstruction.EXPECT().PostCheck(gomock.Any()).Return(nil) + return []instruction.Instructions{mockInstruction} + }, + assertError: assert.NoError, + }, + "execute single instruction pre check failure": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction := mock_instruction.NewMockInstructions(controller) + mockInstruction.EXPECT().GetName().Return("mockInstruction") + mockInstruction.EXPECT().PreCheck(gomock.Any()).Return(errors.New("mock error")) + return []instruction.Instructions{mockInstruction} + }, + assertError: assert.Error, + }, + "execute single instruction apply failure": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction := mock_instruction.NewMockInstructions(controller) + mockInstruction.EXPECT().GetName().Return("mockInstruction") + mockInstruction.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction.EXPECT().Apply(gomock.Any()).Return(errors.New("mock error")) + return []instruction.Instructions{mockInstruction} + }, + assertError: assert.Error, + }, + "execute single instruction post check failure": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction := mock_instruction.NewMockInstructions(controller) + mockInstruction.EXPECT().GetName().Return("mockInstruction") + mockInstruction.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction.EXPECT().Apply(gomock.Any()).Return(nil) + mockInstruction.EXPECT().PostCheck(gomock.Any()).Return(errors.New("mock error")) + return []instruction.Instructions{mockInstruction} + }, + assertError: assert.Error, + }, + "execute multi instruction successfully": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction1 := mock_instruction.NewMockInstructions(controller) + mockInstruction1.EXPECT().GetName().Return("mockInstruction") + mockInstruction1.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction1.EXPECT().Apply(gomock.Any()).Return(nil) + mockInstruction1.EXPECT().PostCheck(gomock.Any()).Return(nil) + + mockInstruction2 := mock_instruction.NewMockInstructions(controller) + mockInstruction2.EXPECT().GetName().Return("mockInstruction") + mockInstruction2.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction2.EXPECT().Apply(gomock.Any()).Return(nil) + mockInstruction2.EXPECT().PostCheck(gomock.Any()).Return(nil) + + return []instruction.Instructions{mockInstruction1, mockInstruction2} + }, + assertError: assert.NoError, + }, + "execute multi instruction first instruction failure": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction1 := mock_instruction.NewMockInstructions(controller) + mockInstruction1.EXPECT().GetName().Return("mockInstruction") + mockInstruction1.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction1.EXPECT().Apply(gomock.Any()).Return(errors.New("mock error")) + + mockInstruction2 := mock_instruction.NewMockInstructions(controller) + + return []instruction.Instructions{mockInstruction1, mockInstruction2} + }, + assertError: assert.Error, + }, + "execute multi instruction second instruction failure": { + getInstructions: func(controller *gomock.Controller) []instruction.Instructions { + mockInstruction1 := mock_instruction.NewMockInstructions(controller) + mockInstruction1.EXPECT().GetName().Return("mockInstruction") + mockInstruction1.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction1.EXPECT().Apply(gomock.Any()).Return(nil) + mockInstruction1.EXPECT().PostCheck(gomock.Any()).Return(nil) + + mockInstruction2 := mock_instruction.NewMockInstructions(controller) + mockInstruction2.EXPECT().GetName().Return("mockInstruction") + mockInstruction2.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction2.EXPECT().Apply(gomock.Any()).Return(errors.New("mock error")) + + return []instruction.Instructions{mockInstruction1, mockInstruction2} + }, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + err := execution.Execute(context.TODO(), params.getInstructions(ctrl)) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} diff --git a/internal/nodeprep/instruction/default.go b/internal/nodeprep/instruction/default.go new file mode 100644 index 000000000..cb9f02593 --- /dev/null +++ b/internal/nodeprep/instruction/default.go @@ -0,0 +1,62 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package instruction + +import ( + "context" + + "github.com/netapp/trident/internal/nodeprep/step" + . "github.com/netapp/trident/logging" +) + +const defaultInstructionName = "default instructions" + +type Default struct { + name string + steps []step.Step +} + +func (r *Default) GetName() string { + if r.name == "" { + return defaultInstructionName + } + return r.name +} + +func (r *Default) GetSteps() []step.Step { + return r.steps +} + +func (r *Default) PreCheck(_ context.Context) error { + return nil +} + +func (r *Default) Apply(ctx context.Context) error { + Log().Infof("Applying %s", r.GetName()) + for _, s := range r.steps { + if err := executeStep(ctx, s); err != nil { + return err + } + } + return nil +} + +func executeStep(context context.Context, step step.Step) error { + Log().WithField("step", step.GetName()).Info("Executing step") + + if err := step.Apply(context); err != nil { + if step.IsRequired() { + return err + } + Log().WithError(err).WithField("step", step.GetName()). + Info("apply failed but step is not required, continuing") + } + + Log().WithField("step", step.GetName()). + Info("successfully applied step") + return nil +} + +func (r *Default) PostCheck(_ context.Context) error { + return nil +} diff --git a/internal/nodeprep/instruction/instructions.go b/internal/nodeprep/instruction/instructions.go new file mode 100644 index 000000000..558ffb03d --- /dev/null +++ b/internal/nodeprep/instruction/instructions.go @@ -0,0 +1,63 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package instruction + +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_instruction/mock_instruction.go github.com/netapp/trident/internal/nodeprep/instruction Instructions + +import ( + "context" + "fmt" + + "github.com/netapp/trident/internal/nodeprep/nodeinfo" + "github.com/netapp/trident/internal/nodeprep/protocol" + "github.com/netapp/trident/internal/nodeprep/step" + utilserrors "github.com/netapp/trident/utils/errors" +) + +// Planning for future distros and protocols which may not align easily with each other and need different instructions +// For example windows might need different instructions for iSCSI than linux or ROSA might be very different from amazon linux +// And some may not need package managers and some will + +type Key struct { + Protocol protocol.Protocol + Distro nodeinfo.Distro + PkgMgr nodeinfo.PkgMgr +} + +var instructionMap = map[Key]Instructions{} + +func init() { + instructionMap[Key{Protocol: protocol.ISCSI, Distro: nodeinfo.DistroAmzn, PkgMgr: nodeinfo.PkgMgrYum}] = newAmznYumISCSI() + instructionMap[Key{Protocol: protocol.ISCSI, Distro: nodeinfo.DistroUbuntu, PkgMgr: nodeinfo.PkgMgrApt}] = newDebianAptISCSI() +} + +type Instructions interface { + GetName() string + PreCheck(ctx context.Context) error + Apply(ctx context.Context) error + PostCheck(ctx context.Context) error + GetSteps() []step.Step +} + +// ScopedInstructions is used to mock out the default instructions for testing, +// this should not be called anywhere else other than tests +func ScopedInstructions(instructions map[Key]Instructions) func() { + existingInstructions := instructionMap + instructionMap = instructions + return func() { + instructionMap = existingInstructions + } +} + +func GetInstructions(nodeInfo *nodeinfo.NodeInfo, protocols []protocol.Protocol) ([]Instructions, error) { + instructions := make([]Instructions, 0) + for _, p := range protocols { + if instruction, ok := instructionMap[Key{Protocol: p, Distro: nodeInfo.Distro, PkgMgr: nodeInfo.PkgMgr}]; ok { + instructions = append(instructions, instruction) + } + } + if len(instructions) == 0 { + return nil, utilserrors.UnsupportedError(fmt.Sprintf("distribution %s for protocols %s is not supported", nodeInfo.Distro, protocols)) + } + return instructions, nil +} diff --git a/internal/nodeprep/instruction/iscsi.go b/internal/nodeprep/instruction/iscsi.go new file mode 100644 index 000000000..f73502a69 --- /dev/null +++ b/internal/nodeprep/instruction/iscsi.go @@ -0,0 +1,37 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package instruction + +import ( + "github.com/netapp/trident/internal/nodeprep/packagemanager" + "github.com/netapp/trident/internal/nodeprep/packagemanager/apt" + "github.com/netapp/trident/internal/nodeprep/packagemanager/yum" + "github.com/netapp/trident/internal/nodeprep/step" + "github.com/netapp/trident/internal/nodeprep/systemmanager" + "github.com/netapp/trident/internal/nodeprep/systemmanager/amzn" + "github.com/netapp/trident/internal/nodeprep/systemmanager/debian" +) + +type ISCSI struct { + Default +} + +func newDebianAptISCSI() (instruction Instructions) { + return newISCSI(apt.New(), debian.New()) +} + +func newAmznYumISCSI() (instruction Instructions) { + return newISCSI(yum.New(), amzn.New()) +} + +func newISCSI(packageManager packagemanager.PackageManager, systemManager systemmanager.SystemManager) (instruction *ISCSI) { + instruction = &ISCSI{} + instruction.name = "iscsi instructions" + // ordering of steps matter here, multipath must be configured before installing iscsi tools to be idempotent + instruction.steps = []step.Step{ + step.NewMultipathConfigureStep(packageManager), + step.NewInstallIscsiTools(packageManager), + step.NewEnableIscsiServices(systemManager), + } + return +} diff --git a/internal/nodeprep/mpathconfig/configuration.go b/internal/nodeprep/mpathconfig/configuration.go index f4cd77310..d7a3ff174 100644 --- a/internal/nodeprep/mpathconfig/configuration.go +++ b/internal/nodeprep/mpathconfig/configuration.go @@ -2,7 +2,7 @@ package mpathconfig -//go:generate mockgen -destination=../../../mocks/mock_nodeprep/mock_mpathconfig/mock_config.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfiguration +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_config.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfiguration import ( "bufio" diff --git a/internal/nodeprep/mpathconfig/section.go b/internal/nodeprep/mpathconfig/section.go index 184f6b486..7b3a2e262 100644 --- a/internal/nodeprep/mpathconfig/section.go +++ b/internal/nodeprep/mpathconfig/section.go @@ -2,7 +2,7 @@ package mpathconfig -//go:generate mockgen -destination=../../../mocks/mock_nodeprep/mock_mpathconfig/mock_section.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfigurationSection +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_section.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfigurationSection import ( "fmt" diff --git a/internal/nodeprep/nodeinfo/node_info.go b/internal/nodeprep/nodeinfo/node_info.go new file mode 100644 index 000000000..57db14ef2 --- /dev/null +++ b/internal/nodeprep/nodeinfo/node_info.go @@ -0,0 +1,99 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package nodeinfo + +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_nodeinfo.go github.com/netapp/trident/internal/nodeprep/nodeinfo Node + +import ( + "context" + "fmt" + + . "github.com/netapp/trident/logging" + "github.com/netapp/trident/utils" + "github.com/netapp/trident/utils/errors" + "github.com/netapp/trident/utils/models" +) + +type NodeInfo struct { + PkgMgr PkgMgr + Distro Distro + HostSystem models.HostSystem +} + +type ( + Distro = string + PkgMgr = string +) + +const ( + DistroUbuntu Distro = utils.Ubuntu + DistroAmzn Distro = "amzn" + + PkgMgrYum PkgMgr = "yum" + PkgMgrApt PkgMgr = "apt" + PkgMgrNone PkgMgr = "none" +) + +type Node interface { + GetInfo(ctx context.Context) (*NodeInfo, error) +} + +type NodeClient struct { + os OS + binary Binary +} + +func New() *NodeClient { + return NewDetailed(NewOSClient(), NewBinary()) +} + +func NewDetailed(os OS, binary Binary) *NodeClient { + return &NodeClient{ + os: os, + binary: binary, + } +} + +func (n *NodeClient) GetInfo(ctx context.Context) (*NodeInfo, error) { + hostSystem, err := n.os.GetHostSystemInfo(ctx) + if err != nil { + return nil, err + } + + Log().WithFields(LogFields{ + "HostSystem": hostSystem, + "OS": hostSystem.OS, + "distro": hostSystem.OS.Distro, + }).Info("Host system information") + + distro, err := supportedDistro(hostSystem.OS.Distro) + if err != nil { + return nil, err + } + + nodeType := &NodeInfo{ + PkgMgr: n.getPkgMgr(), + HostSystem: *hostSystem, + Distro: distro, + } + + return nodeType, nil +} + +func (n *NodeClient) getPkgMgr() PkgMgr { + if fullPath := n.binary.FindPath(PkgMgrYum); fullPath != "" { + return PkgMgrYum + } + if fullPath := n.binary.FindPath(PkgMgrApt); fullPath != "" { + return PkgMgrApt + } + return PkgMgrNone +} + +func supportedDistro(distro string) (Distro, error) { + switch distro { + case DistroUbuntu, DistroAmzn: + return distro, nil + } + return "", errors.UnsupportedError(fmt.Sprintf("%s is not a supported host distribution", distro)) +} diff --git a/internal/nodeprep/nodeinfo/node_info_test.go b/internal/nodeprep/nodeinfo/node_info_test.go new file mode 100644 index 000000000..a5f2ccb05 --- /dev/null +++ b/internal/nodeprep/nodeinfo/node_info_test.go @@ -0,0 +1,153 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package nodeinfo_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/nodeinfo" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_nodeinfo" + "github.com/netapp/trident/utils/errors" + "github.com/netapp/trident/utils/models" +) + +func TestNew(t *testing.T) { + node := nodeinfo.New() + assert.NotNil(t, node) +} + +func TestNewDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + os := mock_nodeinfo.NewMockOS(ctrl) + binary := mock_nodeinfo.NewMockBinary(ctrl) + node := nodeinfo.NewDetailed(os, binary) + assert.NotNil(t, node) +} + +func TestNode_GetInfo(t *testing.T) { + type parameters struct { + getOSClient func(controller *gomock.Controller) nodeinfo.OS + getBinaryClient func(controller *gomock.Controller) nodeinfo.Binary + expectedNodeInfo *nodeinfo.NodeInfo + assertError assert.ErrorAssertionFunc + } + + ubuntuHostSystemResponse := models.HostSystem{ + OS: models.SystemOS{ + Distro: nodeinfo.DistroUbuntu, + }, + } + + amazonHostSystemResponse := models.HostSystem{ + OS: models.SystemOS{ + Distro: nodeinfo.DistroAmzn, + }, + } + + fooHostSystemResponse := models.HostSystem{ + OS: models.SystemOS{ + Distro: "foo", + }, + } + + tests := map[string]parameters{ + "error getting host system info": { + getOSClient: func(controller *gomock.Controller) nodeinfo.OS { + os := mock_nodeinfo.NewMockOS(controller) + os.EXPECT().GetHostSystemInfo(gomock.Any()).Return(nil, errors.New("some error")) + return os + }, + getBinaryClient: func(controller *gomock.Controller) nodeinfo.Binary { + binary := mock_nodeinfo.NewMockBinary(controller) + return binary + }, + expectedNodeInfo: nil, + assertError: assert.Error, + }, + "node info returns unsupported distro": { + getOSClient: func(controller *gomock.Controller) nodeinfo.OS { + os := mock_nodeinfo.NewMockOS(controller) + os.EXPECT().GetHostSystemInfo(gomock.Any()).Return(&fooHostSystemResponse, nil) + return os + }, + getBinaryClient: func(controller *gomock.Controller) nodeinfo.Binary { + binary := mock_nodeinfo.NewMockBinary(controller) + return binary + }, + expectedNodeInfo: nil, + assertError: assert.Error, + }, + "node info returns supported distro ubuntu": { + getOSClient: func(controller *gomock.Controller) nodeinfo.OS { + os := mock_nodeinfo.NewMockOS(controller) + os.EXPECT().GetHostSystemInfo(gomock.Any()).Return(&ubuntuHostSystemResponse, nil) + return os + }, + getBinaryClient: func(controller *gomock.Controller) nodeinfo.Binary { + binary := mock_nodeinfo.NewMockBinary(controller) + binary.EXPECT().FindPath(nodeinfo.PkgMgrYum).Return("some path") + return binary + }, + expectedNodeInfo: &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrYum, + HostSystem: ubuntuHostSystemResponse, + Distro: nodeinfo.DistroUbuntu, + }, + assertError: assert.NoError, + }, + "node info returns supported distro amazon linux": { + getOSClient: func(controller *gomock.Controller) nodeinfo.OS { + os := mock_nodeinfo.NewMockOS(controller) + os.EXPECT().GetHostSystemInfo(gomock.Any()).Return(&amazonHostSystemResponse, nil) + return os + }, + getBinaryClient: func(controller *gomock.Controller) nodeinfo.Binary { + binary := mock_nodeinfo.NewMockBinary(controller) + binary.EXPECT().FindPath(nodeinfo.PkgMgrYum).Return("") + binary.EXPECT().FindPath(nodeinfo.PkgMgrApt).Return("some path") + return binary + }, + expectedNodeInfo: &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrApt, + HostSystem: amazonHostSystemResponse, + Distro: nodeinfo.DistroAmzn, + }, + assertError: assert.NoError, + }, + "node info on host with no package manager": { + getOSClient: func(controller *gomock.Controller) nodeinfo.OS { + os := mock_nodeinfo.NewMockOS(controller) + os.EXPECT().GetHostSystemInfo(gomock.Any()).Return(&ubuntuHostSystemResponse, nil) + return os + }, + getBinaryClient: func(controller *gomock.Controller) nodeinfo.Binary { + binary := mock_nodeinfo.NewMockBinary(controller) + binary.EXPECT().FindPath(nodeinfo.PkgMgrYum).Return("") + binary.EXPECT().FindPath(nodeinfo.PkgMgrApt).Return("") + return binary + }, + expectedNodeInfo: &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrNone, + HostSystem: ubuntuHostSystemResponse, + Distro: nodeinfo.DistroUbuntu, + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + node := nodeinfo.NewDetailed(params.getOSClient(ctrl), params.getBinaryClient(ctrl)) + nodeInfo, err := node.GetInfo(nil) + if params.assertError != nil { + params.assertError(t, err) + } + assert.Equal(t, params.expectedNodeInfo, nodeInfo) + }) + } +} diff --git a/internal/nodeprep/nodeinfo/utils.go b/internal/nodeprep/nodeinfo/utils.go new file mode 100644 index 000000000..727d7a37a --- /dev/null +++ b/internal/nodeprep/nodeinfo/utils.go @@ -0,0 +1,46 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package nodeinfo + +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_os.go github.com/netapp/trident/internal/nodeprep/nodeinfo OS +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_binary.go github.com/netapp/trident/internal/nodeprep/nodeinfo Binary + +import ( + "context" + + "github.com/netapp/trident/internal/chwrap" + "github.com/netapp/trident/utils" + "github.com/netapp/trident/utils/models" +) + +// the code in this file need to live under sub packages of utils. +// TODO remove this file once the refactoring is done. + +type OS interface { + GetHostSystemInfo(ctx context.Context) (*models.HostSystem, error) +} + +type Binary interface { + FindPath(binaryName string) string +} + +type OSClient struct{} + +func NewOSClient() *OSClient { + return &OSClient{} +} + +func (o *OSClient) GetHostSystemInfo(ctx context.Context) (*models.HostSystem, error) { + return utils.GetHostSystemInfo(ctx) +} + +type BinaryClient struct{} + +func NewBinary() *BinaryClient { + return &BinaryClient{} +} + +func (b *BinaryClient) FindPath(binaryName string) string { + _, fullPath := chwrap.FindBinary(binaryName) + return fullPath +} diff --git a/internal/nodeprep/nodeprep.go b/internal/nodeprep/nodeprep.go index 4ebb3d095..11a44ea2f 100644 --- a/internal/nodeprep/nodeprep.go +++ b/internal/nodeprep/nodeprep.go @@ -3,31 +3,30 @@ package nodeprep import ( - "github.com/netapp/trident/internal/chwrap" - . "github.com/netapp/trident/logging" - utilserrors "github.com/netapp/trident/utils/errors" -) + "context" -type PkgMgr = string - -const ( - PkgMgrYum PkgMgr = "yum" - PkgMgrApt PkgMgr = "apt" + "github.com/netapp/trident/internal/nodeprep/execution" + "github.com/netapp/trident/internal/nodeprep/instruction" + "github.com/netapp/trident/internal/nodeprep/nodeinfo" + "github.com/netapp/trident/internal/nodeprep/protocol" + . "github.com/netapp/trident/logging" ) type NodePrep struct { - findBinary func(string) (string, string) + node nodeinfo.Node } -func NewNodePrep() *NodePrep { - return NewNodePrepDetailed(chwrap.FindBinary) +func New() *NodePrep { + return NewDetailed(nodeinfo.New()) } -func NewNodePrepDetailed(findBinary func(string) (string, string)) *NodePrep { - return &NodePrep{findBinary: findBinary} +func NewDetailed(node nodeinfo.Node) *NodePrep { + return &NodePrep{ + node: node, + } } -func (n *NodePrep) PrepareNode(requestedProtocols []string) (exitCode int) { +func (n *NodePrep) Prepare(ctx context.Context, requestedProtocols []protocol.Protocol) (exitCode int) { Log().WithField("requestedProtocols", requestedProtocols).Info("Preparing node") defer func() { Log().WithField("exitCode", exitCode).Info("Node preparation complete") @@ -38,22 +37,22 @@ func (n *NodePrep) PrepareNode(requestedProtocols []string) (exitCode int) { return 0 } - pkgMgr, err := n.getPkgMgr() + nodeInfo, err := n.node.GetInfo(ctx) if err != nil { - Log().WithError(err).Error("Failed to determine package manager") + Log().WithError(err).Error("Failed to determine node information") return 1 } - Log().Info("Package manager found: ", pkgMgr) - - return 0 -} -func (n *NodePrep) getPkgMgr() (PkgMgr, error) { - if _, pkgMgr := n.findBinary(string(PkgMgrYum)); pkgMgr != "" { - return PkgMgrYum, nil + instructions, err := instruction.GetInstructions(nodeInfo, requestedProtocols) + if err != nil { + Log().WithError(err).Error("Failed to get instructions") + return 1 } - if _, pkgMgr := n.findBinary(string(PkgMgrApt)); pkgMgr != "" { - return PkgMgrApt, nil + + if err = execution.Execute(ctx, instructions); err != nil { + Log().WithError(err).Error("Failed to prepare node") + return 1 } - return "", utilserrors.UnsupportedError("a supported package manager was not found") + + return 0 } diff --git a/internal/nodeprep/nodeprep_test.go b/internal/nodeprep/nodeprep_test.go index 40ba01f8b..0f1be2834 100644 --- a/internal/nodeprep/nodeprep_test.go +++ b/internal/nodeprep/nodeprep_test.go @@ -3,80 +3,152 @@ package nodeprep_test import ( + "context" "testing" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" "github.com/netapp/trident/internal/nodeprep" + "github.com/netapp/trident/internal/nodeprep/instruction" + "github.com/netapp/trident/internal/nodeprep/nodeinfo" + "github.com/netapp/trident/internal/nodeprep/protocol" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_instruction" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_nodeinfo" + "github.com/netapp/trident/utils/errors" ) -func TestPrepareNodeProtocols(t *testing.T) { - type parameters struct { - protocols []string - expectedExitCode int - } - tests := map[string]parameters{ - "no protocols": { - protocols: nil, - expectedExitCode: 0, - }, - "happy path": { - protocols: []string{"iscsi"}, - expectedExitCode: 0, - }, - } +func TestNew(t *testing.T) { + nodePrep := nodeprep.New() + assert.NotNil(t, nodePrep) +} - for name, params := range tests { - t.Run(name, func(t *testing.T) { - exitCode := nodeprep.NewNodePrepDetailed(NewFindBinaryMock(nodeprep.PkgMgrYum).FindBinary).PrepareNode(params.protocols) - assert.Equal(t, params.expectedExitCode, exitCode) - }) - } +func TestNewDetailed(t *testing.T) { + node := mock_nodeinfo.NewMockNode(gomock.NewController(t)) + nodePrep := nodeprep.NewDetailed(node) + assert.NotNil(t, nodePrep) } -func TestPrepareNodePkgMgr(t *testing.T) { +func TestPrepareNode(t *testing.T) { type parameters struct { - findBinaryMock *FindBinaryMock + protocols []string + getNode func(controller *gomock.Controller) nodeinfo.Node + getInstructions func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions expectedExitCode int } + tests := map[string]parameters{ - "supported yum": { - findBinaryMock: NewFindBinaryMock(nodeprep.PkgMgrYum), + "no protocols": { + protocols: nil, + getNode: func(controller *gomock.Controller) nodeinfo.Node { + mockNode := mock_nodeinfo.NewMockNode(controller) + return mockNode + }, + getInstructions: func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions { + return nil + }, expectedExitCode: 0, }, - "supported apt": { - findBinaryMock: NewFindBinaryMock(nodeprep.PkgMgrApt), - expectedExitCode: 0, + "error getting node info": { + protocols: []string{protocol.ISCSI}, + getNode: func(controller *gomock.Controller) nodeinfo.Node { + mockNode := mock_nodeinfo.NewMockNode(controller) + mockNode.EXPECT().GetInfo(gomock.Any()). + Return(nil, errors.New("mock error")) + return mockNode + }, + getInstructions: func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions { + return nil + }, + expectedExitCode: 1, }, - "not supported": { - findBinaryMock: NewFindBinaryMock("other"), + "error getting instructions": { + protocols: []string{protocol.ISCSI}, + getNode: func(controller *gomock.Controller) nodeinfo.Node { + mockNode := mock_nodeinfo.NewMockNode(controller) + mockNode.EXPECT().GetInfo(gomock.Any()). + Return( + &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrNone, + Distro: nodeinfo.DistroUbuntu, + }, + nil, + ) + return mockNode + }, + getInstructions: func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions { + return nil + }, expectedExitCode: 1, }, - "empty": { - findBinaryMock: NewFindBinaryMock(""), + "error executing instructions": { + protocols: []string{protocol.ISCSI}, + getNode: func(controller *gomock.Controller) nodeinfo.Node { + mockNode := mock_nodeinfo.NewMockNode(controller) + mockNode.EXPECT().GetInfo(gomock.Any()). + Return( + &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrNone, + Distro: nodeinfo.DistroUbuntu, + }, + nil, + ) + return mockNode + }, + getInstructions: func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions { + key := instruction.Key{ + Protocol: protocol.ISCSI, + Distro: nodeinfo.DistroUbuntu, + PkgMgr: nodeinfo.PkgMgrNone, + } + mockInstruction := mock_instruction.NewMockInstructions(controller) + mockInstruction.EXPECT().GetName().Return("mock instruction") + mockInstruction.EXPECT().PreCheck(gomock.Any()).Return(nil) + mockInstruction.EXPECT().Apply(gomock.Any()).Return(errors.New("some error")) + instructions := map[instruction.Key]instruction.Instructions{ + key: mockInstruction, + } + return instructions + }, expectedExitCode: 1, }, + "happy path": { + protocols: []string{protocol.ISCSI}, + getNode: func(controller *gomock.Controller) nodeinfo.Node { + mockNode := mock_nodeinfo.NewMockNode(controller) + mockNode.EXPECT().GetInfo(gomock.Any()). + Return( + &nodeinfo.NodeInfo{ + PkgMgr: nodeinfo.PkgMgrNone, + Distro: nodeinfo.DistroUbuntu, + }, + nil, + ) + return mockNode + }, + getInstructions: func(controller *gomock.Controller) map[instruction.Key]instruction.Instructions { + key := instruction.Key{ + Protocol: protocol.ISCSI, + Distro: nodeinfo.DistroUbuntu, + PkgMgr: nodeinfo.PkgMgrNone, + } + return map[instruction.Key]instruction.Instructions{ + key: &instruction.Default{}, + } + }, + expectedExitCode: 0, + }, } for name, params := range tests { t.Run(name, func(t *testing.T) { - exitCode := nodeprep.NewNodePrepDetailed(params.findBinaryMock.FindBinary).PrepareNode([]string{"iscsi"}) - assert.Equal(t, params.expectedExitCode, exitCode) - }) - } -} - -type FindBinaryMock struct { - binary string -} + ctrl := gomock.NewController(t) -func NewFindBinaryMock(binary nodeprep.PkgMgr) *FindBinaryMock { - return &FindBinaryMock{binary: string(binary)} -} + defer instruction.ScopedInstructions(params.getInstructions(ctrl))() + node := nodeprep.NewDetailed(params.getNode(ctrl)) -func (f *FindBinaryMock) FindBinary(binary string) (string, string) { - if binary == f.binary { - return "/host", "/bin/" + binary + exitCode := node.Prepare(context.TODO(), params.protocols) + assert.Equal(t, params.expectedExitCode, exitCode) + }) } - return "", "" } diff --git a/internal/nodeprep/packagemanager/apt/apt.go b/internal/nodeprep/packagemanager/apt/apt.go new file mode 100644 index 000000000..dc4a6850c --- /dev/null +++ b/internal/nodeprep/packagemanager/apt/apt.go @@ -0,0 +1,105 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package apt + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/netapp/trident/logging" + "github.com/netapp/trident/utils/errors" + "github.com/netapp/trident/utils/exec" +) + +const ( + PackageLsscsi = "lsscsi" + PackageOpenIscsi = "open-iscsi" + PackageMultipathTools = "multipath-tools" +) + +const ( + defaultCommandTimeout = 10 * time.Second + defaultLogCommandOutput = true +) + +type Apt struct { + command exec.Command + commandTimeout time.Duration + logCommandOutput bool +} + +func New() *Apt { + return NewDetailed(exec.NewCommand(), defaultCommandTimeout, defaultLogCommandOutput) +} + +func NewDetailed(command exec.Command, commandTimeout time.Duration, logCommandOutput bool) *Apt { + return &Apt{ + command: command, + commandTimeout: commandTimeout, + logCommandOutput: logCommandOutput, + } +} + +func (a *Apt) MultipathToolsInstalled(ctx context.Context) bool { + err := a.validatePackageInstall(ctx, PackageMultipathTools) + return err == nil +} + +func (a *Apt) InstallIscsiRequirements(ctx context.Context) error { + if err := a.installPackageWithValidation(ctx, PackageLsscsi); err != nil { + Log().WithError(err).Error("Failed to install lsscsi package, continuing installation.") + } + if err := a.installPackageWithValidation(ctx, PackageOpenIscsi); err != nil { + return err + } + if err := a.installPackageWithValidation(ctx, PackageMultipathTools); err != nil { + return err + } + return nil +} + +func (a *Apt) installPackageWithValidation(ctx context.Context, packageName string) error { + output, err := a.installPackage(ctx, packageName) + if err != nil { + return errors.New(fmt.Sprintf("failed to install %s package: %v", packageName, err)) + } + + Log().WithFields(LogFields{ + "package": packageName, + "output": output, + }).Debug("Installed output for package.") + + if err = a.validatePackageInstall(ctx, packageName); err != nil { + return err + } + + Log().WithField("packageName", packageName).Info("Successfully installed package.") + return nil +} + +func (a *Apt) validatePackageInstall(ctx context.Context, packageName string) error { + output, err := a.getPackageInfo(ctx, packageName) + if err != nil { + return errors.New(fmt.Sprintf("failed to validate %s package install: %s", packageName, output)) + } + + if strings.Contains(output, fmt.Sprintf("Unable to locate package %s", packageName)) { + return fmt.Errorf("failed to install package %s: %s", packageName, output) + } + + return nil +} + +func (a *Apt) installPackage(ctx context.Context, packageName string) (string, error) { + output, err := a.command.ExecuteWithTimeout(ctx, "apt", + a.commandTimeout, a.logCommandOutput, "install", "-y", packageName) + return string(output), err +} + +func (a *Apt) getPackageInfo(ctx context.Context, packageName string) (string, error) { + output, err := a.command.ExecuteWithTimeout(ctx, "apt", + a.commandTimeout, a.logCommandOutput, "list", "--installed", packageName) + return string(output), err +} diff --git a/internal/nodeprep/packagemanager/apt/apt_test.go b/internal/nodeprep/packagemanager/apt/apt_test.go new file mode 100644 index 000000000..2dc004c5d --- /dev/null +++ b/internal/nodeprep/packagemanager/apt/apt_test.go @@ -0,0 +1,283 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package apt_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/packagemanager/apt" + mockexec "github.com/netapp/trident/mocks/mock_utils/mock_exec" + "github.com/netapp/trident/utils/errors" +) + +func TestNew(t *testing.T) { + aptPackageManager := apt.New() + assert.NotNil(t, aptPackageManager) +} + +func TestNewDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + mockCommand := mockexec.NewMockCommand(ctrl) + aptPackageManager := apt.NewDetailed(mockCommand, 1*time.Second, false) + assert.NotNil(t, aptPackageManager) +} + +func TestApt_MultipathToolsInstalled(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mockexec.MockCommand + assertResponse assert.BoolAssertionFunc + } + const timeout = 1 * time.Second + const logCommandOutput = false + const alreadyInstalledOutput = "foo baz" + const notInstalledOutput = "Unable to locate package " + apt.PackageMultipathTools + + tests := map[string]parameters{ + "error executing command": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return(nil, errors.New("some error")) + return mockCommand + }, + assertResponse: assert.False, + }, + "multipath tools already installed": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(alreadyInstalledOutput), nil) + return mockCommand + }, + assertResponse: assert.True, + }, + "multipath tools not installed": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(notInstalledOutput), nil) + return mockCommand + }, + assertResponse: assert.False, + }, + } + + ctx := context.Background() + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + aptPackageManager := apt.NewDetailed(params.getCommand(ctrl), timeout, logCommandOutput) + + response := aptPackageManager.MultipathToolsInstalled(ctx) + if params.assertResponse != nil { + params.assertResponse(t, response) + } + }) + } +} + +func TestApt_InstallIscsiRequirements(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mockexec.MockCommand + assertError assert.ErrorAssertionFunc + } + + const timeout = 1 * time.Second + const logCommandOutput = false + const successfulpackageInstallOutput = "foo baz" + const failedPackageInstallOutput = "Unable to locate package %s" + + tests := map[string]parameters{ + "happy path: error installing lsscsi": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, errors.New("some error")) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(successfulpackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path: error getting lsscsi package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return(nil, errors.New("some error")) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(successfulpackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path: failure installing lsscsi": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(fmt.Sprintf(failedPackageInstallOutput, apt.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(successfulpackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(successfulpackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "error installing open-iscsi package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error getting open-iscsi package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "failure installing open-iscsi package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(fmt.Sprintf(failedPackageInstallOutput, apt.PackageOpenIscsi)), nil) + return mockCommand + }, + assertError: assert.Error, + }, + "error installing multipath-tools package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error getting multipath-tools package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "failure installing multipath-tools package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageOpenIscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageOpenIscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "install", + "-y", apt.PackageMultipathTools).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "apt", timeout, logCommandOutput, "list", + "--installed", apt.PackageMultipathTools).Return([]byte(fmt.Sprintf(failedPackageInstallOutput, apt.PackageMultipathTools)), nil) + return mockCommand + }, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + aptPackageManager := apt.NewDetailed(params.getCommand(ctrl), timeout, logCommandOutput) + + err := aptPackageManager.InstallIscsiRequirements(context.TODO()) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} diff --git a/internal/nodeprep/packagemanager/packagemanager.go b/internal/nodeprep/packagemanager/packagemanager.go index 58d3e4fb2..dfcaf3c83 100644 --- a/internal/nodeprep/packagemanager/packagemanager.go +++ b/internal/nodeprep/packagemanager/packagemanager.go @@ -2,14 +2,11 @@ package packagemanager -//go:generate mockgen -destination=../../../mocks/mock_nodeprep/mock_packagemanager/mock_packagemanager.go github.com/netapp/trident/internal/nodeprep/packagemanager PackageManager +import "context" -import "github.com/netapp/trident/internal/nodeprep/packagemanager/yum" +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_packagemanager/mock_packagemanager.go github.com/netapp/trident/internal/nodeprep/packagemanager PackageManager type PackageManager interface { - MultipathToolsInstalled() bool -} - -func Factory() PackageManager { - return yum.New() + MultipathToolsInstalled(context.Context) bool + InstallIscsiRequirements(ctx context.Context) error } diff --git a/internal/nodeprep/packagemanager/yum/yum.go b/internal/nodeprep/packagemanager/yum/yum.go index 9806c62c8..2085a8847 100644 --- a/internal/nodeprep/packagemanager/yum/yum.go +++ b/internal/nodeprep/packagemanager/yum/yum.go @@ -2,13 +2,104 @@ package yum -type Yum struct{} +import ( + "context" + "fmt" + "strings" + "time" -func (y *Yum) MultipathToolsInstalled() bool { - // TODO implement this function - return true + . "github.com/netapp/trident/logging" + "github.com/netapp/trident/utils/errors" + "github.com/netapp/trident/utils/exec" +) + +const ( + PackageLsscsi = "lsscsi" + PackageIscsiInitiatorUtils = "iscsi-initiator-utils" + PackageDeviceMapperMultipath = "device-mapper-multipath" +) + +const ( + defaultCommandTimeout = 10 * time.Second + defaultLogCommandOutput = true +) + +type Yum struct { + command exec.Command + commandTimeout time.Duration + logCommandOutput bool } func New() *Yum { - return &Yum{} + return NewDetailed(exec.NewCommand(), defaultCommandTimeout, defaultLogCommandOutput) +} + +func NewDetailed(command exec.Command, commandTimeout time.Duration, logCommandOutput bool) *Yum { + return &Yum{ + command: command, + commandTimeout: commandTimeout, + logCommandOutput: logCommandOutput, + } +} + +func (y *Yum) MultipathToolsInstalled(ctx context.Context) bool { + err := y.validatePackageInstall(ctx, PackageDeviceMapperMultipath) + return err == nil +} + +func (y *Yum) InstallIscsiRequirements(ctx context.Context) error { + if err := y.installPackageWithValidation(ctx, PackageLsscsi); err != nil { + Log().WithError(err).Error("Failed to install lsscsi package, continuing installation") + } + if err := y.installPackageWithValidation(ctx, PackageIscsiInitiatorUtils); err != nil { + return err + } + if err := y.installPackageWithValidation(ctx, PackageDeviceMapperMultipath); err != nil { + return err + } + return nil +} + +func (y *Yum) installPackageWithValidation(ctx context.Context, packageName string) error { + output, err := y.installPackage(ctx, packageName) + if err != nil { + return errors.New(fmt.Sprintf("failed to install %s package: %v", packageName, err)) + } + + Log().WithFields(LogFields{ + "package": packageName, + "output": output, + }).Debug("install output for package") + + if err = y.validatePackageInstall(ctx, packageName); err != nil { + return err + } + + Log().WithField("packageName", packageName).Info("Successfully installed package") + return nil +} + +func (y *Yum) validatePackageInstall(ctx context.Context, packageName string) error { + output, err := y.getPackageInfo(ctx, packageName) + if err != nil { + return errors.New(fmt.Sprintf("failed to validate %s package install: %s", packageName, err)) + } + + if !strings.Contains(output, packageName) { + return errors.New(fmt.Sprintf("failed to install %s package : %s", packageName, output)) + } + + return nil +} + +func (y *Yum) installPackage(ctx context.Context, packageName string) (string, error) { + output, err := y.command.ExecuteWithTimeout(ctx, "yum", + y.commandTimeout, y.logCommandOutput, "install", "-y", packageName) + return string(output), err +} + +func (y *Yum) getPackageInfo(ctx context.Context, packageName string) (string, error) { + output, err := y.command.ExecuteWithTimeout(ctx, "yum", + y.commandTimeout, y.logCommandOutput, "info", "--installed", packageName) + return string(output), err } diff --git a/internal/nodeprep/packagemanager/yum/yum_test.go b/internal/nodeprep/packagemanager/yum/yum_test.go index 3830232f9..45fe110b0 100644 --- a/internal/nodeprep/packagemanager/yum/yum_test.go +++ b/internal/nodeprep/packagemanager/yum/yum_test.go @@ -3,11 +3,17 @@ package yum_test import ( + "context" + "fmt" "testing" + "time" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" "github.com/netapp/trident/internal/nodeprep/packagemanager/yum" + mockexec "github.com/netapp/trident/mocks/mock_utils/mock_exec" + "github.com/netapp/trident/utils/errors" ) func TestNew(t *testing.T) { @@ -15,8 +21,280 @@ func TestNew(t *testing.T) { assert.NotNil(t, yumPackageManager) } +func TestNewDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + mockCommand := mockexec.NewMockCommand(ctrl) + yumPackageManager := yum.NewDetailed(mockCommand, 1*time.Second, false) + assert.NotNil(t, yumPackageManager) +} + func TestYum_MultipathToolsInstalled(t *testing.T) { - // TODO implement this test - yumPackageManager := yum.New() - assert.True(t, yumPackageManager.MultipathToolsInstalled()) + type parameters struct { + getCommand func(controller *gomock.Controller) *mockexec.MockCommand + assertResponse assert.BoolAssertionFunc + } + const timeout = 1 * time.Second + const logCommandOutput = false + const alreadyInstalledOutput = "foo baz " + yum.PackageDeviceMapperMultipath + " foo baz" + const notInstalledOutput = "foo baz foo baz" + + tests := map[string]parameters{ + "error executing command": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return(nil, errors.UnsupportedError("some error")) + return mockCommand + }, + assertResponse: assert.False, + }, + "multipath tools already installed": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(alreadyInstalledOutput), nil) + return mockCommand + }, + assertResponse: assert.True, + }, + "multipath tools not installed": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(gomock.Any(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(notInstalledOutput), nil) + return mockCommand + }, + assertResponse: assert.False, + }, + } + + ctx := context.Background() + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + yumPackageManager := yum.NewDetailed(params.getCommand(ctrl), timeout, logCommandOutput) + + response := yumPackageManager.MultipathToolsInstalled(ctx) + if params.assertResponse != nil { + params.assertResponse(t, response) + } + }) + } +} + +func TestApt_InstallIscsiRequirements(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mockexec.MockCommand + assertError assert.ErrorAssertionFunc + } + + const timeout = 1 * time.Second + const logCommandOutput = false + const successfulpackageInstallOutput = "foo baz %s foo baz" + const failedPackageInstallOutput = "foo baz" + + tests := map[string]parameters{ + "happy path: error installing lsscsi": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, errors.New("some error")) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageDeviceMapperMultipath)), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path: error getting lsscsi package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return(nil, errors.New("some error")) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(fmt. + Sprintf(successfulpackageInstallOutput, yum.PackageDeviceMapperMultipath)), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path: failure installing lsscsi": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(failedPackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageDeviceMapperMultipath)), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "happy path": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(fmt.Sprintf(successfulpackageInstallOutput, yum.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageDeviceMapperMultipath)), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "error installing open-iscsi package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error getting open-iscsi package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(successfulpackageInstallOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "failure installing open-iscsi package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(fmt.Sprintf(successfulpackageInstallOutput, + yum.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(failedPackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.Error, + }, + "error installing multipath-tools package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(fmt.Sprintf(successfulpackageInstallOutput, + yum.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error getting multipath-tools package info": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(fmt.Sprintf(successfulpackageInstallOutput, + yum.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "failure installing multipath-tools package": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageLsscsi).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageLsscsi).Return([]byte(fmt.Sprintf(successfulpackageInstallOutput, + yum.PackageLsscsi)), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageIscsiInitiatorUtils).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageIscsiInitiatorUtils).Return([]byte(fmt.Sprintf( + successfulpackageInstallOutput, yum.PackageIscsiInitiatorUtils)), + nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "install", + "-y", yum.PackageDeviceMapperMultipath).Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "yum", timeout, logCommandOutput, "info", + "--installed", yum.PackageDeviceMapperMultipath).Return([]byte(failedPackageInstallOutput), nil) + return mockCommand + }, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + yunPackageManager := yum.NewDetailed(params.getCommand(ctrl), timeout, logCommandOutput) + + err := yunPackageManager.InstallIscsiRequirements(context.TODO()) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } } diff --git a/internal/nodeprep/validation/validation.go b/internal/nodeprep/protocol/protocols.go similarity index 52% rename from internal/nodeprep/validation/validation.go rename to internal/nodeprep/protocol/protocols.go index b010f0c74..b9d7f85fe 100644 --- a/internal/nodeprep/validation/validation.go +++ b/internal/nodeprep/protocol/protocols.go @@ -1,15 +1,17 @@ // Copyright 2024 NetApp, Inc. All Rights Reserved. -package validation +package protocol import ( "fmt" "strings" - "github.com/netapp/trident/utils/errors" + utilserrors "github.com/netapp/trident/utils/errors" ) -const iscsi = "iscsi" +type Protocol = string + +const ISCSI Protocol = "iscsi" func FormatProtocols(requestedProtocols []string) (formattedProtocols []string) { for _, protocol := range requestedProtocols { @@ -23,10 +25,11 @@ func ValidateProtocols(requestedProtocols []string) error { return nil } if len(requestedProtocols) > 1 { - return errors.UnsupportedError(fmt.Sprintf("only one protocl (iSCSI) is supported at this time but node prep protocol set to '%s'", requestedProtocols)) + return utilserrors.UnsupportedError(fmt.Sprintf("only one protocol ("+ + "iSCSI) is supported at this time but node prep protocol set to '%s'", requestedProtocols)) } - if requestedProtocols[0] != iscsi { - return errors.UnsupportedError(fmt.Sprintf("'%s' is not a valid node prep protocol", requestedProtocols)) + if requestedProtocols[0] != string(ISCSI) { + return utilserrors.UnsupportedError(fmt.Sprintf("'%s' is not a valid node prep protocol", requestedProtocols)) } return nil diff --git a/internal/nodeprep/validation/validation_test.go b/internal/nodeprep/protocol/protocols_test.go similarity index 90% rename from internal/nodeprep/validation/validation_test.go rename to internal/nodeprep/protocol/protocols_test.go index af9711ef1..b2f866b50 100644 --- a/internal/nodeprep/validation/validation_test.go +++ b/internal/nodeprep/protocol/protocols_test.go @@ -1,13 +1,11 @@ -// Copyright 2024 NetApp, Inc. All Rights Reserved. - -package validation_test +package protocol_test import ( "testing" "github.com/stretchr/testify/assert" - "github.com/netapp/trident/internal/nodeprep/validation" + "github.com/netapp/trident/internal/nodeprep/protocol" ) func TestValidateProtocols(t *testing.T) { @@ -56,7 +54,7 @@ func TestValidateProtocols(t *testing.T) { for name, params := range tests { t.Run(name, func(t *testing.T) { - err := validation.ValidateProtocols(params.protocols) + err := protocol.ValidateProtocols(params.protocols) params.assertValid(t, err) }) } @@ -97,7 +95,7 @@ func TestFormatProtocols(t *testing.T) { for name, params := range tests { t.Run(name, func(t *testing.T) { - formattedProtocols := validation.FormatProtocols(params.protocols) + formattedProtocols := protocol.FormatProtocols(params.protocols) assert.Len(t, formattedProtocols, len(params.expectedOutput)) assert.Equal(t, formattedProtocols, params.expectedOutput) }) diff --git a/internal/nodeprep/stage/multipath.go b/internal/nodeprep/step/configure_multipathd.go similarity index 73% rename from internal/nodeprep/stage/multipath.go rename to internal/nodeprep/step/configure_multipathd.go index f766d6444..d8a5bf4dd 100644 --- a/internal/nodeprep/stage/multipath.go +++ b/internal/nodeprep/step/configure_multipathd.go @@ -1,11 +1,14 @@ // Copyright 2024 NetApp, Inc. All Rights Reserved. -package stage +package step import ( + "context" "fmt" - "os" + "github.com/spf13/afero" + + "github.com/netapp/trident/config" "github.com/netapp/trident/internal/nodeprep/mpathconfig" "github.com/netapp/trident/internal/nodeprep/packagemanager" ) @@ -15,33 +18,43 @@ type ( NewConfigurationFromFile func(filename string) (mpathconfig.MpathConfiguration, error) ) -type Configurator struct { +type MultipathConfigureStep struct { + DefaultStep multipathConfigurationLocation string newConfigurationFromFile NewConfigurationFromFile newConfiguration NewConfiguration packageManager packagemanager.PackageManager + os afero.Afero } -func NewConfigurator() *Configurator { - return NewConfiguratorDetailed(mpathconfig.MultiPathConfigurationLocation, mpathconfig.NewFromFile, - mpathconfig.New, packagemanager.Factory()) +func NewMultipathConfigureStep(packageManager packagemanager.PackageManager) *MultipathConfigureStep { + return NewMultipathConfigureStepDetailed(mpathconfig.MultiPathConfigurationLocation, mpathconfig.NewFromFile, + mpathconfig.New, packageManager, afero.Afero{Fs: afero.NewOsFs()}) } -func NewConfiguratorDetailed(multipathConfigurationLocation string, newConfigurationFromFile NewConfigurationFromFile, - newConfiguration NewConfiguration, packageManager packagemanager.PackageManager, -) *Configurator { - return &Configurator{ +func NewMultipathConfigureStepDetailed(multipathConfigurationLocation string, newConfigurationFromFile NewConfigurationFromFile, + newConfiguration NewConfiguration, packageManager packagemanager.PackageManager, os afero.Afero, +) *MultipathConfigureStep { + multipathConfigurationStep := &MultipathConfigureStep{ + DefaultStep: DefaultStep{Name: "multipath step", Required: true}, newConfigurationFromFile: newConfigurationFromFile, newConfiguration: newConfiguration, multipathConfigurationLocation: multipathConfigurationLocation, packageManager: packageManager, + os: os, + } + + if multipathConfigurationStep.fileExists(config.NamespaceFile) { + multipathConfigurationStep.multipathConfigurationLocation = fmt.Sprintf("/host%s", multipathConfigurationStep.multipathConfigurationLocation) } + + return multipathConfigurationStep } -func (c *Configurator) ConfigureMultipathDaemon() error { +func (c *MultipathConfigureStep) Apply(ctx context.Context) error { var mpathCfg mpathconfig.MpathConfiguration var err error - if c.packageManager.MultipathToolsInstalled() { + if c.packageManager.MultipathToolsInstalled(ctx) { if !c.multipathConfigurationExists() { return fmt.Errorf("found multipath tools already installed but no configuration file found; customer" + @@ -71,7 +84,7 @@ func (c *Configurator) ConfigureMultipathDaemon() error { return mpathCfg.SaveConfig(c.multipathConfigurationLocation) } -func (c *Configurator) UpdateConfiguration(mpathCfg mpathconfig.MpathConfiguration) error { +func (c *MultipathConfigureStep) UpdateConfiguration(mpathCfg mpathconfig.MpathConfiguration) error { defaultsSection, err := mpathCfg.GetSection(mpathconfig.DefaultsSectionName) if err != nil { if defaultsSection, err = configureDefaultSection(mpathCfg); err != nil { @@ -101,7 +114,7 @@ func (c *Configurator) UpdateConfiguration(mpathCfg mpathconfig.MpathConfigurati return nil } -func (c *Configurator) AddConfiguration(mpathCfg mpathconfig.MpathConfiguration) error { +func (c *MultipathConfigureStep) AddConfiguration(mpathCfg mpathconfig.MpathConfiguration) error { if _, err := configureDefaultSection(mpathCfg); err != nil { return err } @@ -125,8 +138,12 @@ func (c *Configurator) AddConfiguration(mpathCfg mpathconfig.MpathConfiguration) return nil } -func (c *Configurator) multipathConfigurationExists() bool { - _, err := os.Stat(c.multipathConfigurationLocation) +func (c *MultipathConfigureStep) multipathConfigurationExists() bool { + return c.fileExists(c.multipathConfigurationLocation) +} + +func (c *MultipathConfigureStep) fileExists(filename string) bool { + _, err := c.os.Stat(filename) return err == nil } diff --git a/internal/nodeprep/stage/multipath_test.go b/internal/nodeprep/step/configure_multipathd_test.go similarity index 78% rename from internal/nodeprep/stage/multipath_test.go rename to internal/nodeprep/step/configure_multipathd_test.go index e8284c043..eacd80af3 100644 --- a/internal/nodeprep/stage/multipath_test.go +++ b/internal/nodeprep/step/configure_multipathd_test.go @@ -1,34 +1,64 @@ // Copyright 2024 NetApp, Inc. All Rights Reserved. -package stage_test +package step_test import ( + "context" "os" "testing" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "github.com/netapp/trident/config" "github.com/netapp/trident/internal/nodeprep/mpathconfig" "github.com/netapp/trident/internal/nodeprep/packagemanager" - "github.com/netapp/trident/internal/nodeprep/stage" - "github.com/netapp/trident/mocks/mock_nodeprep/mock_mpathconfig" - "github.com/netapp/trident/mocks/mock_nodeprep/mock_packagemanager" + "github.com/netapp/trident/internal/nodeprep/step" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_mpathconfig" + "github.com/netapp/trident/mocks/mock_internal/mock_nodeprep/mock_packagemanager" "github.com/netapp/trident/utils/errors" ) -func TestNewConfigurator(t *testing.T) { - assert.NotNil(t, stage.NewConfigurator()) +func TestNewMultipathConfigureStep(t *testing.T) { + assert.NotNil(t, step.NewMultipathConfigureStep(nil)) } -func TestNewConfiguratorDetailed(t *testing.T) { - assert.NotNil(t, stage.NewConfiguratorDetailed(mpathconfig.MultiPathConfigurationLocation, nil, nil, nil)) +func TestNewMultipathConfigureStepDetailed(t *testing.T) { + type parameters struct { + getFileSystem func() afero.Fs + } + + tests := map[string]parameters{ + "running outside the container": { + getFileSystem: func() afero.Fs { + return afero.NewMemMapFs() + }, + }, + "running inside the container": { + getFileSystem: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create(config.NamespaceFile) + assert.NoError(t, err) + return fs + }, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + mpathConfigStep := step.NewMultipathConfigureStepDetailed(mpathconfig.MultiPathConfigurationLocation, nil, nil, + nil, afero.Afero{Fs: params.getFileSystem()}) + assert.NotNil(t, mpathConfigStep) + }) + } } -func TestConfigurator_AddConfiguration(t *testing.T) { +func TestMultipathConfigureStep_AddConfiguration(t *testing.T) { type parameters struct { - getConfiguration func() mpathconfig.MpathConfiguration - assertError assert.ErrorAssertionFunc + getConfiguration func() mpathconfig.MpathConfiguration + getPackageManager func() packagemanager.PackageManager + assertError assert.ErrorAssertionFunc } tests := map[string]parameters{ @@ -55,6 +85,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.NoError, }, "default section creation returns an error": { @@ -69,6 +104,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "setting property in the default section return an error": { @@ -84,6 +124,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding the blacklist section returns an error": { @@ -101,6 +146,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding device section to blacklist section returns an error": { @@ -119,6 +169,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding the vendor key to the device section in blacklist returns an error": { @@ -138,6 +193,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding the product key to the device section in blacklist returns an error": { @@ -158,6 +218,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding blacklist exception section return an error": { @@ -180,6 +245,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding device section to blacklist exception returns an error": { @@ -203,6 +273,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding the vendor key to the device section in the blacklist exception returns an error": { @@ -227,6 +302,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "adding the product key to the device section in the blacklist exception returns an error": { @@ -252,13 +332,18 @@ func TestConfigurator_AddConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, } for name, params := range tests { t.Run(name, func(t *testing.T) { - configurator := stage.NewConfigurator() + configurator := step.NewMultipathConfigureStep(params.getPackageManager()) err := configurator.AddConfiguration(params.getConfiguration()) if params.assertError != nil { @@ -268,10 +353,11 @@ func TestConfigurator_AddConfiguration(t *testing.T) { } } -func TestConfigurator_UpdateConfiguration(t *testing.T) { +func TestMultipathConfigureStep_UpdateConfiguration(t *testing.T) { type parameters struct { - getConfiguration func() mpathconfig.MpathConfiguration - assertError assert.ErrorAssertionFunc + getConfiguration func() mpathconfig.MpathConfiguration + getPackageManager func() packagemanager.PackageManager + assertError assert.ErrorAssertionFunc } tests := map[string]parameters{ @@ -290,9 +376,14 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.NoError, }, - "get property 'find_multipaths' from the defaults section returns an error": { + "get property 'find_multipaths' from the defaults section returns an error": { getConfiguration: func() mpathconfig.MpathConfiguration { mockCtrl := gomock.NewController(t) section := mock_mpathconfig.NewMockMpathConfigurationSection(mockCtrl) @@ -304,6 +395,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "configuration has 'find_multipaths' set to 'strict' in the defaults section": { @@ -318,6 +414,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "configuration has 'find_multipaths' set to 'yes' in the defaults section": { @@ -332,6 +433,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "configuration does not have property 'find_multipaths' in the defaults section": { @@ -345,6 +451,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "defaults section not present in the configuration": { @@ -366,7 +477,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, - assertError: assert.NoError, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, }, "defaults section not present: adding defaults section returns an error": { getConfiguration: func() mpathconfig.MpathConfiguration { @@ -381,6 +496,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "defaults section not present: setting property returns an error": { @@ -397,6 +517,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "blacklist exception section not present": { @@ -419,6 +544,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.NoError, }, "blacklist exception section not present: add section error": { @@ -437,6 +567,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "blacklist exception section not present: add device section error": { @@ -457,6 +592,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "blacklist exception section not present: error setting property vendor in device section": { @@ -478,6 +618,11 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, "blacklist exception section not present: error setting property product in device section": { @@ -500,13 +645,18 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { return mpathConfig }, + getPackageManager: func() packagemanager.PackageManager { + mockCtrl := gomock.NewController(t) + packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) + return packageManager + }, assertError: assert.Error, }, } for name, params := range tests { t.Run(name, func(t *testing.T) { - configurator := stage.NewConfigurator() + configurator := step.NewMultipathConfigureStep(params.getPackageManager()) err := configurator.UpdateConfiguration(params.getConfiguration()) if params.assertError != nil { @@ -516,7 +666,7 @@ func TestConfigurator_UpdateConfiguration(t *testing.T) { } } -func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { +func TestMultipathConfigureStep_Apply(t *testing.T) { type parameters struct { mpathConfigLocation string getMpathConfiguration func() mpathconfig.MpathConfiguration @@ -525,6 +675,8 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { assertError assert.ErrorAssertionFunc } + ctx := context.Background() + tests := map[string]parameters{ "multipath tools already installed and configuration file exists": { mpathConfigLocation: os.DevNull, @@ -536,7 +688,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(true) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(true) return packageManager }, assertError: assert.NoError, @@ -551,7 +703,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(true) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(true) return packageManager }, configConstructorError: errors.New("some error"), @@ -573,7 +725,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(true) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(true) return packageManager }, assertError: assert.Error, @@ -588,7 +740,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(true) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(true) return packageManager }, assertError: assert.Error, @@ -603,7 +755,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(false) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(false) return packageManager }, assertError: assert.NoError, @@ -618,7 +770,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(false) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(false) return packageManager }, configConstructorError: errors.New("some error"), @@ -639,7 +791,7 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { getPackageManager: func() packagemanager.PackageManager { mockCtrl := gomock.NewController(t) packageManager := mock_packagemanager.NewMockPackageManager(mockCtrl) - packageManager.EXPECT().MultipathToolsInstalled().Return(false) + packageManager.EXPECT().MultipathToolsInstalled(ctx).Return(false) return packageManager }, assertError: assert.Error, @@ -653,10 +805,14 @@ func TestConfigurator_ConfigureMultipathDaemon(t *testing.T) { err: params.configConstructorError, } - configurator := stage.NewConfiguratorDetailed(params.mpathConfigLocation, configConstructors.NewFromFile, - configConstructors.New, params.getPackageManager()) + fs := afero.NewMemMapFs() + _, err := fs.Create(os.DevNull) + assert.NoError(t, err) + configurator := step.NewMultipathConfigureStepDetailed(params.mpathConfigLocation, + configConstructors.NewFromFile, configConstructors.New, params.getPackageManager(), + afero.Afero{Fs: fs}) - err := configurator.ConfigureMultipathDaemon() + err = configurator.Apply(ctx) if params.assertError != nil { params.assertError(t, err) } diff --git a/internal/nodeprep/step/default.go b/internal/nodeprep/step/default.go new file mode 100644 index 000000000..89ffb18de --- /dev/null +++ b/internal/nodeprep/step/default.go @@ -0,0 +1,27 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package step + +import "context" + +const defaultStepName = "default step" + +type DefaultStep struct { + Name string + Required bool +} + +func (s *DefaultStep) GetName() string { + if s.Name == "" { + return defaultStepName + } + return s.Name +} + +func (s *DefaultStep) IsRequired() bool { + return s.Required +} + +func (s *DefaultStep) Apply(_ context.Context) error { + return nil +} diff --git a/internal/nodeprep/step/enable_iscsi_services.go b/internal/nodeprep/step/enable_iscsi_services.go new file mode 100644 index 000000000..8ba84b4a2 --- /dev/null +++ b/internal/nodeprep/step/enable_iscsi_services.go @@ -0,0 +1,26 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package step + +import ( + "context" + + "github.com/netapp/trident/internal/nodeprep/systemmanager" +) + +type EnableIscsiServices struct { + DefaultStep + systemManager systemmanager.SystemManager +} + +func NewEnableIscsiServices(systemManager systemmanager.SystemManager) *EnableIscsiServices { + step := &EnableIscsiServices{} + step.Name = "enable iSCSI services step" + step.Required = true + step.systemManager = systemManager + return step +} + +func (s *EnableIscsiServices) Apply(ctx context.Context) error { + return s.systemManager.EnableIscsiServices(ctx) +} diff --git a/internal/nodeprep/step/install_iscsi_tools.go b/internal/nodeprep/step/install_iscsi_tools.go new file mode 100644 index 000000000..896cf289a --- /dev/null +++ b/internal/nodeprep/step/install_iscsi_tools.go @@ -0,0 +1,26 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package step + +import ( + "context" + + "github.com/netapp/trident/internal/nodeprep/packagemanager" +) + +type InstallIscsiTools struct { + DefaultStep + packageManager packagemanager.PackageManager +} + +func NewInstallIscsiTools(packageManager packagemanager.PackageManager) *InstallIscsiTools { + step := &InstallIscsiTools{} + step.Name = "install iSCSI tools" + step.Required = true + step.packageManager = packageManager + return step +} + +func (s *InstallIscsiTools) Apply(ctx context.Context) error { + return s.packageManager.InstallIscsiRequirements(ctx) +} diff --git a/internal/nodeprep/step/steps.go b/internal/nodeprep/step/steps.go new file mode 100644 index 000000000..7118f8bda --- /dev/null +++ b/internal/nodeprep/step/steps.go @@ -0,0 +1,9 @@ +package step + +import "context" + +type Step interface { + GetName() string + IsRequired() bool + Apply(ctx context.Context) error +} diff --git a/internal/nodeprep/systemmanager/amzn/amzn.go b/internal/nodeprep/systemmanager/amzn/amzn.go new file mode 100644 index 000000000..42d5af67c --- /dev/null +++ b/internal/nodeprep/systemmanager/amzn/amzn.go @@ -0,0 +1,45 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package amzn + +import ( + "context" + "time" + + "github.com/netapp/trident/internal/nodeprep/systemmanager/systemctl" + "github.com/netapp/trident/utils/exec" +) + +const ( + ServiceIscsid = "iscsid" + ServiceMultipathd = "multipathd" +) + +const ( + defaultCommandTimeout = 10 * time.Second + defaultLogCommandOutput = true +) + +type Amzn struct { + *systemctl.Systemctl +} + +func New() *Amzn { + return NewDetailed(systemctl.NewSystemctlDetailed(exec.NewCommand(), defaultCommandTimeout, defaultLogCommandOutput)) +} + +func NewDetailed(systemctl *systemctl.Systemctl) *Amzn { + return &Amzn{ + Systemctl: systemctl, + } +} + +func (a *Amzn) EnableIscsiServices(ctx context.Context) error { + if err := a.EnableServiceWithValidation(ctx, ServiceIscsid); err != nil { + return err + } + if err := a.EnableServiceWithValidation(ctx, ServiceMultipathd); err != nil { + return err + } + return nil +} diff --git a/internal/nodeprep/systemmanager/amzn/amzn_test.go b/internal/nodeprep/systemmanager/amzn/amzn_test.go new file mode 100644 index 000000000..f341e1d9a --- /dev/null +++ b/internal/nodeprep/systemmanager/amzn/amzn_test.go @@ -0,0 +1,144 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package amzn_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/systemmanager/amzn" + "github.com/netapp/trident/internal/nodeprep/systemmanager/systemctl" + "github.com/netapp/trident/mocks/mock_utils/mock_exec" +) + +func TestNew(t *testing.T) { + amznClient := amzn.New() + assert.NotNil(t, amznClient) +} + +func TestNewDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + command := mock_exec.NewMockCommand(ctrl) + amznClient := amzn.NewDetailed(systemctl.NewSystemctlDetailed(command, 1*time.Second, true)) + assert.NotNil(t, amznClient) +} + +func TestAmzn_EnableIscsiServices(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mock_exec.MockCommand + assertError assert.ErrorAssertionFunc + } + + const commandTimeout = 1 * time.Second + const logCommandOutput = true + const activeStateActive = "ActiveState=active\n" + const activeStateInactive = "ActiveState=inactive\n" + + tests := map[string]parameters{ + "error enabling iscsi service": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "error validating that iscsi service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "failure validating that iscsi service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateInactive), nil) + return command + }, + assertError: assert.Error, + }, + "error enabling multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceMultipathd).Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "error validating that multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceMultipathd).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceMultipathd, "--property=ActiveState").Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "failure validating that multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceMultipathd).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceMultipathd, "--property=ActiveState").Return([]byte(activeStateInactive), nil) + return command + }, + assertError: assert.Error, + }, + "happy path": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", amzn.ServiceMultipathd).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", amzn.ServiceMultipathd, "--property=ActiveState").Return([]byte(activeStateActive), nil) + return command + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + amznClient := amzn.NewDetailed(systemctl.NewSystemctlDetailed(params.getCommand(ctrl), commandTimeout, logCommandOutput)) + err := amznClient.EnableIscsiServices(context.TODO()) + if params.assertError(t, err) { + params.assertError(t, err) + } + }) + } +} diff --git a/internal/nodeprep/systemmanager/debian/debian.go b/internal/nodeprep/systemmanager/debian/debian.go new file mode 100644 index 000000000..25c4184a5 --- /dev/null +++ b/internal/nodeprep/systemmanager/debian/debian.go @@ -0,0 +1,45 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package debian + +import ( + "context" + "time" + + "github.com/netapp/trident/internal/nodeprep/systemmanager/systemctl" + "github.com/netapp/trident/utils/exec" +) + +const ( + ServiceIscsid = "iscsid" + ServiceMultipathtools = "multipath-tools.service" +) + +const ( + defaultCommandTimeout = 10 * time.Second + defaultLogCommandOutput = true +) + +type Debian struct { + *systemctl.Systemctl +} + +func New() *Debian { + return NewDetailed(exec.NewCommand(), defaultCommandTimeout, defaultLogCommandOutput) +} + +func NewDetailed(command exec.Command, commandTimeout time.Duration, logCommandOutput bool) *Debian { + return &Debian{ + Systemctl: systemctl.NewSystemctlDetailed(command, commandTimeout, logCommandOutput), + } +} + +func (d *Debian) EnableIscsiServices(ctx context.Context) error { + if err := d.EnableServiceWithValidation(ctx, ServiceIscsid); err != nil { + return err + } + if err := d.EnableServiceWithValidation(ctx, ServiceMultipathtools); err != nil { + return err + } + return nil +} diff --git a/internal/nodeprep/systemmanager/debian/debian_test.go b/internal/nodeprep/systemmanager/debian/debian_test.go new file mode 100644 index 000000000..c11d5b797 --- /dev/null +++ b/internal/nodeprep/systemmanager/debian/debian_test.go @@ -0,0 +1,143 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package debian_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/systemmanager/debian" + "github.com/netapp/trident/mocks/mock_utils/mock_exec" +) + +func TestNew(t *testing.T) { + debianClient := debian.New() + assert.NotNil(t, debianClient) +} + +func TestNewDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + command := mock_exec.NewMockCommand(ctrl) + debianClient := debian.NewDetailed(command, 1*time.Second, true) + assert.NotNil(t, debianClient) +} + +func TestDebian_EnableIscsiServices(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mock_exec.MockCommand + assertError assert.ErrorAssertionFunc + } + + const commandTimeout = 1 * time.Second + const logCommandOutput = true + const activeStateActive = "ActiveState=active\n" + const activeStateInactive = "ActiveState=inactive\n" + + tests := map[string]parameters{ + "error enabling iscsi service": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "error validating that iscsi service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "failure validating that iscsi service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateInactive), nil) + return command + }, + assertError: assert.Error, + }, + "error enabling multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceMultipathtools).Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "error validating that multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceMultipathtools).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceMultipathtools, "--property=ActiveState").Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "failure validating that multipath service is enabled": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceMultipathtools).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceMultipathtools, "--property=ActiveState").Return([]byte(activeStateInactive), nil) + return command + }, + assertError: assert.Error, + }, + "happy path": { + getCommand: func(controller *gomock.Controller) *mock_exec.MockCommand { + command := mock_exec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceIscsid).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceIscsid, "--property=ActiveState").Return([]byte(activeStateActive), nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "enable", "--now", debian.ServiceMultipathtools).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, + logCommandOutput, "show", debian.ServiceMultipathtools, "--property=ActiveState").Return([]byte(activeStateActive), nil) + return command + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + debianClient := debian.NewDetailed(params.getCommand(ctrl), commandTimeout, logCommandOutput) + err := debianClient.EnableIscsiServices(context.TODO()) + if params.assertError(t, err) { + params.assertError(t, err) + } + }) + } +} diff --git a/internal/nodeprep/systemmanager/systemctl/systemctl.go b/internal/nodeprep/systemmanager/systemctl/systemctl.go new file mode 100644 index 000000000..08dafa0b8 --- /dev/null +++ b/internal/nodeprep/systemmanager/systemctl/systemctl.go @@ -0,0 +1,77 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package systemctl + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/netapp/trident/logging" + "github.com/netapp/trident/utils/errors" + "github.com/netapp/trident/utils/exec" +) + +const activeStateActive = "ActiveState=active" + +type Systemctl struct { + command exec.Command + commandTimeout time.Duration + logCommandOutput bool +} + +func NewSystemctlDetailed(command exec.Command, commandTimeout time.Duration, logCommandOutput bool) *Systemctl { + return &Systemctl{ + command: command, + commandTimeout: commandTimeout, + logCommandOutput: logCommandOutput, + } +} + +func (s *Systemctl) EnableServiceWithValidation(ctx context.Context, serviceName string) error { + output, err := s.enableService(ctx, serviceName) + if err != nil { + return errors.New(fmt.Sprintf("failed to enable %s: %v", serviceName, err)) + } + + Log().WithFields(LogFields{ + "service": serviceName, + "output": output, + }).Debug("activate output for package") + + if err = s.validateServiceEnabled(ctx, serviceName); err != nil { + return err + } + + Log().WithField("serviceName", serviceName).Info("Successfully enabled service") + + return nil +} + +func (s *Systemctl) validateServiceEnabled(ctx context.Context, serviceName string) error { + output, err := s.isServiceActive(ctx, serviceName) + if err != nil { + return errors.New(fmt.Sprintf("failed to validate if %s service is enabled: %s", serviceName, err)) + } + + if !strings.Contains(output, activeStateActive) { + return errors.New(fmt.Sprintf("failed to validate if %s service is enabled: %s", serviceName, output)) + } + + return nil +} + +func (s *Systemctl) enableService(ctx context.Context, serviceName string) (string, error) { + output, err := s.command.ExecuteWithTimeout(ctx, "systemctl", s.commandTimeout, s.logCommandOutput, "enable", + "--now", serviceName) + + return string(output), err +} + +func (s *Systemctl) isServiceActive(ctx context.Context, serviceName string) (string, error) { + output, err := s.command.ExecuteWithTimeout(ctx, "systemctl", s.commandTimeout, s.logCommandOutput, "show", + serviceName, "--property=ActiveState") + + return string(output), err +} diff --git a/internal/nodeprep/systemmanager/systemctl/systemctl_test.go b/internal/nodeprep/systemmanager/systemctl/systemctl_test.go new file mode 100644 index 000000000..1045a3d1b --- /dev/null +++ b/internal/nodeprep/systemmanager/systemctl/systemctl_test.go @@ -0,0 +1,92 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package systemctl_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/netapp/trident/internal/nodeprep/systemmanager/systemctl" + mockexec "github.com/netapp/trident/mocks/mock_utils/mock_exec" +) + +func TestNewSystemctlDetailed(t *testing.T) { + ctrl := gomock.NewController(t) + command := mockexec.NewMockCommand(ctrl) + systemctlClient := systemctl.NewSystemctlDetailed(command, 1*time.Second, true) + assert.NotNil(t, systemctlClient) +} + +func TestSystemctl_EnableServiceWithValidation(t *testing.T) { + type parameters struct { + getCommand func(controller *gomock.Controller) *mockexec.MockCommand + assertError assert.ErrorAssertionFunc + } + + const commandTimeout = 1 * time.Second + const logCommandOutput = true + const serviceName = "foo" + const activeStateInactive = "ActiveState=inactive\n" + const activeStateActive = "ActiveState=active\n" + + tests := map[string]parameters{ + "error enabling service": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + command := mockexec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "enable", "--now", serviceName).Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "error determining if service is active": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + command := mockexec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "enable", "--now", serviceName).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "show", serviceName, "--property=ActiveState").Return(nil, assert.AnError) + return command + }, + assertError: assert.Error, + }, + "unable to enable service": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + command := mockexec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "enable", "--now", serviceName).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "show", serviceName, "--property=ActiveState").Return([]byte(activeStateInactive), nil) + return command + }, + assertError: assert.Error, + }, + "happy path": { + getCommand: func(controller *gomock.Controller) *mockexec.MockCommand { + command := mockexec.NewMockCommand(controller) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "enable", "--now", serviceName).Return(nil, nil) + command.EXPECT().ExecuteWithTimeout(context.TODO(), "systemctl", commandTimeout, logCommandOutput, + "show", serviceName, "--property=ActiveState").Return([]byte(activeStateActive), nil) + return command + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + systemctlClient := systemctl.NewSystemctlDetailed(params.getCommand(ctrl), commandTimeout, logCommandOutput) + err := systemctlClient.EnableServiceWithValidation(context.TODO(), serviceName) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} diff --git a/internal/nodeprep/systemmanager/systemmanager.go b/internal/nodeprep/systemmanager/systemmanager.go new file mode 100644 index 000000000..be7addfeb --- /dev/null +++ b/internal/nodeprep/systemmanager/systemmanager.go @@ -0,0 +1,11 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package systemmanager + +import "context" + +//go:generate mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_systemmanager/mock_systemmanager.go github.com/netapp/trident/internal/nodeprep/systemmanager SystemManager + +type SystemManager interface { + EnableIscsiServices(context.Context) error +} diff --git a/mocks/mock_internal/mock_nodeprep/mock_instruction/mock_instruction.go b/mocks/mock_internal/mock_nodeprep/mock_instruction/mock_instruction.go new file mode 100644 index 000000000..40b0abd9a --- /dev/null +++ b/mocks/mock_internal/mock_nodeprep/mock_instruction/mock_instruction.go @@ -0,0 +1,111 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/internal/nodeprep/instruction (interfaces: Instructions) +// +// Generated by this command: +// +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_instruction/mock_instruction.go github.com/netapp/trident/internal/nodeprep/instruction Instructions +// + +// Package mock_instruction is a generated GoMock package. +package mock_instruction + +import ( + context "context" + reflect "reflect" + + step "github.com/netapp/trident/internal/nodeprep/step" + gomock "go.uber.org/mock/gomock" +) + +// MockInstructions is a mock of Instructions interface. +type MockInstructions struct { + ctrl *gomock.Controller + recorder *MockInstructionsMockRecorder +} + +// MockInstructionsMockRecorder is the mock recorder for MockInstructions. +type MockInstructionsMockRecorder struct { + mock *MockInstructions +} + +// NewMockInstructions creates a new mock instance. +func NewMockInstructions(ctrl *gomock.Controller) *MockInstructions { + mock := &MockInstructions{ctrl: ctrl} + mock.recorder = &MockInstructionsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInstructions) EXPECT() *MockInstructionsMockRecorder { + return m.recorder +} + +// Apply mocks base method. +func (m *MockInstructions) Apply(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Apply", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Apply indicates an expected call of Apply. +func (mr *MockInstructionsMockRecorder) Apply(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockInstructions)(nil).Apply), arg0) +} + +// GetName mocks base method. +func (m *MockInstructions) GetName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetName") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetName indicates an expected call of GetName. +func (mr *MockInstructionsMockRecorder) GetName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockInstructions)(nil).GetName)) +} + +// GetSteps mocks base method. +func (m *MockInstructions) GetSteps() []step.Step { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSteps") + ret0, _ := ret[0].([]step.Step) + return ret0 +} + +// GetSteps indicates an expected call of GetSteps. +func (mr *MockInstructionsMockRecorder) GetSteps() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSteps", reflect.TypeOf((*MockInstructions)(nil).GetSteps)) +} + +// PostCheck mocks base method. +func (m *MockInstructions) PostCheck(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostCheck", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostCheck indicates an expected call of PostCheck. +func (mr *MockInstructionsMockRecorder) PostCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostCheck", reflect.TypeOf((*MockInstructions)(nil).PostCheck), arg0) +} + +// PreCheck mocks base method. +func (m *MockInstructions) PreCheck(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreCheck", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// PreCheck indicates an expected call of PreCheck. +func (mr *MockInstructionsMockRecorder) PreCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreCheck", reflect.TypeOf((*MockInstructions)(nil).PreCheck), arg0) +} diff --git a/mocks/mock_nodeprep/mock_mpathconfig/mock_config.go b/mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_config.go similarity index 95% rename from mocks/mock_nodeprep/mock_mpathconfig/mock_config.go rename to mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_config.go index f61ea57ef..87a5ef536 100644 --- a/mocks/mock_nodeprep/mock_mpathconfig/mock_config.go +++ b/mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_config.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=../../../mocks/mock_nodeprep/mock_mpathconfig/mock_config.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfiguration +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_config.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfiguration // // Package mock_mpathconfig is a generated GoMock package. diff --git a/mocks/mock_nodeprep/mock_mpathconfig/mock_section.go b/mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_section.go similarity index 95% rename from mocks/mock_nodeprep/mock_mpathconfig/mock_section.go rename to mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_section.go index e4c0c932d..9b429a6c5 100644 --- a/mocks/mock_nodeprep/mock_mpathconfig/mock_section.go +++ b/mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_section.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=../../../mocks/mock_nodeprep/mock_mpathconfig/mock_section.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfigurationSection +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_mpathconfig/mock_section.go github.com/netapp/trident/internal/nodeprep/mpathconfig MpathConfigurationSection // // Package mock_mpathconfig is a generated GoMock package. diff --git a/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_binary.go b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_binary.go new file mode 100644 index 000000000..2b918d6a4 --- /dev/null +++ b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_binary.go @@ -0,0 +1,53 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/internal/nodeprep/nodeinfo (interfaces: Binary) +// +// Generated by this command: +// +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_binary.go github.com/netapp/trident/internal/nodeprep/nodeinfo Binary +// + +// Package mock_nodeinfo is a generated GoMock package. +package mock_nodeinfo + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockBinary is a mock of Binary interface. +type MockBinary struct { + ctrl *gomock.Controller + recorder *MockBinaryMockRecorder +} + +// MockBinaryMockRecorder is the mock recorder for MockBinary. +type MockBinaryMockRecorder struct { + mock *MockBinary +} + +// NewMockBinary creates a new mock instance. +func NewMockBinary(ctrl *gomock.Controller) *MockBinary { + mock := &MockBinary{ctrl: ctrl} + mock.recorder = &MockBinaryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBinary) EXPECT() *MockBinaryMockRecorder { + return m.recorder +} + +// FindPath mocks base method. +func (m *MockBinary) FindPath(arg0 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindPath", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// FindPath indicates an expected call of FindPath. +func (mr *MockBinaryMockRecorder) FindPath(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPath", reflect.TypeOf((*MockBinary)(nil).FindPath), arg0) +} diff --git a/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_nodeinfo.go b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_nodeinfo.go new file mode 100644 index 000000000..d0ea67344 --- /dev/null +++ b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_nodeinfo.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/internal/nodeprep/nodeinfo (interfaces: Node) +// +// Generated by this command: +// +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_nodeinfo.go github.com/netapp/trident/internal/nodeprep/nodeinfo Node +// + +// Package mock_nodeinfo is a generated GoMock package. +package mock_nodeinfo + +import ( + context "context" + reflect "reflect" + + nodeinfo "github.com/netapp/trident/internal/nodeprep/nodeinfo" + gomock "go.uber.org/mock/gomock" +) + +// MockNode is a mock of Node interface. +type MockNode struct { + ctrl *gomock.Controller + recorder *MockNodeMockRecorder +} + +// MockNodeMockRecorder is the mock recorder for MockNode. +type MockNodeMockRecorder struct { + mock *MockNode +} + +// NewMockNode creates a new mock instance. +func NewMockNode(ctrl *gomock.Controller) *MockNode { + mock := &MockNode{ctrl: ctrl} + mock.recorder = &MockNodeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNode) EXPECT() *MockNodeMockRecorder { + return m.recorder +} + +// GetInfo mocks base method. +func (m *MockNode) GetInfo(arg0 context.Context) (*nodeinfo.NodeInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInfo", arg0) + ret0, _ := ret[0].(*nodeinfo.NodeInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInfo indicates an expected call of GetInfo. +func (mr *MockNodeMockRecorder) GetInfo(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInfo", reflect.TypeOf((*MockNode)(nil).GetInfo), arg0) +} diff --git a/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_os.go b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_os.go new file mode 100644 index 000000000..565503a15 --- /dev/null +++ b/mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_os.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/internal/nodeprep/nodeinfo (interfaces: OS) +// +// Generated by this command: +// +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_nodeinfo/mock_os.go github.com/netapp/trident/internal/nodeprep/nodeinfo OS +// + +// Package mock_nodeinfo is a generated GoMock package. +package mock_nodeinfo + +import ( + context "context" + reflect "reflect" + + models "github.com/netapp/trident/utils/models" + gomock "go.uber.org/mock/gomock" +) + +// MockOS is a mock of OS interface. +type MockOS struct { + ctrl *gomock.Controller + recorder *MockOSMockRecorder +} + +// MockOSMockRecorder is the mock recorder for MockOS. +type MockOSMockRecorder struct { + mock *MockOS +} + +// NewMockOS creates a new mock instance. +func NewMockOS(ctrl *gomock.Controller) *MockOS { + mock := &MockOS{ctrl: ctrl} + mock.recorder = &MockOSMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOS) EXPECT() *MockOSMockRecorder { + return m.recorder +} + +// GetHostSystemInfo mocks base method. +func (m *MockOS) GetHostSystemInfo(arg0 context.Context) (*models.HostSystem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHostSystemInfo", arg0) + ret0, _ := ret[0].(*models.HostSystem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHostSystemInfo indicates an expected call of GetHostSystemInfo. +func (mr *MockOSMockRecorder) GetHostSystemInfo(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHostSystemInfo", reflect.TypeOf((*MockOS)(nil).GetHostSystemInfo), arg0) +} diff --git a/mocks/mock_nodeprep/mock_packagemanager/mock_packagemanager.go b/mocks/mock_internal/mock_nodeprep/mock_packagemanager/mock_packagemanager.go similarity index 59% rename from mocks/mock_nodeprep/mock_packagemanager/mock_packagemanager.go rename to mocks/mock_internal/mock_nodeprep/mock_packagemanager/mock_packagemanager.go index e24eae0d0..489dde614 100644 --- a/mocks/mock_nodeprep/mock_packagemanager/mock_packagemanager.go +++ b/mocks/mock_internal/mock_nodeprep/mock_packagemanager/mock_packagemanager.go @@ -3,13 +3,14 @@ // // Generated by this command: // -// mockgen -destination=../../../mocks/mock_nodeprep/mock_packagemanager/mock_packagemanager.go github.com/netapp/trident/internal/nodeprep/packagemanager PackageManager +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_packagemanager/mock_packagemanager.go github.com/netapp/trident/internal/nodeprep/packagemanager PackageManager // // Package mock_packagemanager is a generated GoMock package. package mock_packagemanager import ( + context "context" reflect "reflect" gomock "go.uber.org/mock/gomock" @@ -38,16 +39,30 @@ func (m *MockPackageManager) EXPECT() *MockPackageManagerMockRecorder { return m.recorder } +// InstallIscsiRequirements mocks base method. +func (m *MockPackageManager) InstallIscsiRequirements(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstallIscsiRequirements", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// InstallIscsiRequirements indicates an expected call of InstallIscsiRequirements. +func (mr *MockPackageManagerMockRecorder) InstallIscsiRequirements(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallIscsiRequirements", reflect.TypeOf((*MockPackageManager)(nil).InstallIscsiRequirements), arg0) +} + // MultipathToolsInstalled mocks base method. -func (m *MockPackageManager) MultipathToolsInstalled() bool { +func (m *MockPackageManager) MultipathToolsInstalled(arg0 context.Context) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MultipathToolsInstalled") + ret := m.ctrl.Call(m, "MultipathToolsInstalled", arg0) ret0, _ := ret[0].(bool) return ret0 } // MultipathToolsInstalled indicates an expected call of MultipathToolsInstalled. -func (mr *MockPackageManagerMockRecorder) MultipathToolsInstalled() *gomock.Call { +func (mr *MockPackageManagerMockRecorder) MultipathToolsInstalled(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MultipathToolsInstalled", reflect.TypeOf((*MockPackageManager)(nil).MultipathToolsInstalled)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MultipathToolsInstalled", reflect.TypeOf((*MockPackageManager)(nil).MultipathToolsInstalled), arg0) } diff --git a/mocks/mock_internal/mock_nodeprep/mock_systemmanager/mock_systemmanager.go b/mocks/mock_internal/mock_nodeprep/mock_systemmanager/mock_systemmanager.go new file mode 100644 index 000000000..4f65c566a --- /dev/null +++ b/mocks/mock_internal/mock_nodeprep/mock_systemmanager/mock_systemmanager.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/internal/nodeprep/systemmanager (interfaces: SystemManager) +// +// Generated by this command: +// +// mockgen -destination=../../../mocks/mock_internal/mock_nodeprep/mock_systemmanager/mock_systemmanager.go github.com/netapp/trident/internal/nodeprep/systemmanager SystemManager +// + +// Package mock_systemmanager is a generated GoMock package. +package mock_systemmanager + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockSystemManager is a mock of SystemManager interface. +type MockSystemManager struct { + ctrl *gomock.Controller + recorder *MockSystemManagerMockRecorder +} + +// MockSystemManagerMockRecorder is the mock recorder for MockSystemManager. +type MockSystemManagerMockRecorder struct { + mock *MockSystemManager +} + +// NewMockSystemManager creates a new mock instance. +func NewMockSystemManager(ctrl *gomock.Controller) *MockSystemManager { + mock := &MockSystemManager{ctrl: ctrl} + mock.recorder = &MockSystemManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSystemManager) EXPECT() *MockSystemManagerMockRecorder { + return m.recorder +} + +// EnableIscsiServices mocks base method. +func (m *MockSystemManager) EnableIscsiServices(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnableIscsiServices", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnableIscsiServices indicates an expected call of EnableIscsiServices. +func (mr *MockSystemManagerMockRecorder) EnableIscsiServices(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableIscsiServices", reflect.TypeOf((*MockSystemManager)(nil).EnableIscsiServices), arg0) +} diff --git a/operator/controllers/orchestrator/installer/installer.go b/operator/controllers/orchestrator/installer/installer.go index d02fc9f30..c7a42d4fe 100644 --- a/operator/controllers/orchestrator/installer/installer.go +++ b/operator/controllers/orchestrator/installer/installer.go @@ -19,7 +19,7 @@ import ( "github.com/netapp/trident/cli/api" k8sclient "github.com/netapp/trident/cli/k8s_client" commonconfig "github.com/netapp/trident/config" - "github.com/netapp/trident/internal/nodeprep/validation" + "github.com/netapp/trident/internal/nodeprep/protocol" . "github.com/netapp/trident/logging" netappv1 "github.com/netapp/trident/operator/crd/apis/netapp/v1" crdclient "github.com/netapp/trident/persistent_store/crd/client/clientset/versioned" @@ -389,7 +389,7 @@ func (i *Installer) setInstallationParams( logWorkflows = cr.Spec.LogWorkflows logLayers = cr.Spec.LogLayers - nodePrep = validation.FormatProtocols(cr.Spec.NodePrep) + nodePrep = protocol.FormatProtocols(cr.Spec.NodePrep) if cr.Spec.ProbePort != nil { probePort = strconv.FormatInt(*cr.Spec.ProbePort, 10) @@ -537,7 +537,7 @@ func (i *Installer) setInstallationParams( } // Perform nodePrep prechecks - if returnError = validation.ValidateProtocols(nodePrep); returnError != nil { + if returnError = protocol.ValidateProtocols(nodePrep); returnError != nil { return nil, nil, false, returnError }