diff --git a/cmd/base/list_options.go b/cmd/base/list_options.go index a7bd160..edb130c 100644 --- a/cmd/base/list_options.go +++ b/cmd/base/list_options.go @@ -341,3 +341,48 @@ func (o *BandwidthTypeOption[T]) ApplyToCollection(collection serverscom.Collect collection.SetParam("type", o.bandwidthType) } } + +type HasRaidControllerOption[T any] struct { + hasRaidController bool +} + +func (o *HasRaidControllerOption[T]) AddFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.hasRaidController, "has-raid-controller", false, + "Filter only servers with RAID controller") +} + +func (o *HasRaidControllerOption[T]) ApplyToCollection(collection serverscom.Collection[T]) { + if o.hasRaidController { + collection.SetParam("has_raid_controller", "true") + } +} + +type DriveMediaTypeOption[T any] struct { + mediaType string +} + +func (o *DriveMediaTypeOption[T]) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.mediaType, "media-type", "", + "Filter drives by media type (HDD, SSD") +} + +func (o *DriveMediaTypeOption[T]) ApplyToCollection(collection serverscom.Collection[T]) { + if o.mediaType != "" { + collection.SetParam("media_type", o.mediaType) + } +} + +type DriveInterfaceOption[T any] struct { + iface string +} + +func (o *DriveInterfaceOption[T]) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.iface, "interface", "", + "Filter drives by interface (SATA1, SATA2, SATA3, SAS, NVMe-PCIe)") +} + +func (o *DriveInterfaceOption[T]) ApplyToCollection(collection serverscom.Collection[T]) { + if o.iface != "" { + collection.SetParam("interface", o.iface) + } +} diff --git a/cmd/entities/drivemodels/drivemodels.go b/cmd/entities/drivemodels/drivemodels.go new file mode 100644 index 0000000..a330458 --- /dev/null +++ b/cmd/entities/drivemodels/drivemodels.go @@ -0,0 +1,38 @@ +package drivemodels + +import ( + "log" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/serverscom/srvctl/internal/output/entities" + "github.com/spf13/cobra" +) + +func NewCmd(cmdContext *base.CmdContext) *cobra.Command { + driveEntity, err := entities.Registry.GetEntityFromValue(serverscom.DriveModel{}) + if err != nil { + log.Fatal(err) + } + entitiesMap := make(map[string]entities.EntityInterface) + entitiesMap["drive-models"] = driveEntity + cmd := &cobra.Command{ + Use: "drive-models", + Short: "Manage drive models", + PersistentPreRunE: base.CombinePreRunE( + base.CheckFormatterFlags(cmdContext, entitiesMap), + base.CheckEmptyContexts(cmdContext), + ), + Args: base.NoArgs, + Run: base.UsageRun, + } + + cmd.AddCommand( + newListCmd(cmdContext), + newGetCmd(cmdContext), + ) + + base.AddFormatFlags(cmd) + + return cmd +} diff --git a/cmd/entities/drivemodels/drivemodels_test.go b/cmd/entities/drivemodels/drivemodels_test.go new file mode 100644 index 0000000..5bd2f74 --- /dev/null +++ b/cmd/entities/drivemodels/drivemodels_test.go @@ -0,0 +1,251 @@ +package drivemodels + +import ( + "errors" + "fmt" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/testutils" + "github.com/serverscom/srvctl/internal/mocks" + "go.uber.org/mock/gomock" +) + +var ( + fixtureBasePath = filepath.Join("..", "..", "..", "testdata", "entities", "drive-models") + testDriveModelID = int64(10) + testLocationID = int64(1) + testServerModelID = int64(100) + testDriveModelOption = serverscom.DriveModel{ + ID: testDriveModelID, + Name: "ssd-model-749", + Capacity: 100, + Interface: "SATA3", + FormFactor: "2.5", + MediaType: "SSD", + } +) + +func TestGetDriveModelOptionCmd(t *testing.T) { + testCases := []struct { + name string + id int64 + output string + flags []string + expectedOutput []byte + expectError bool + }{ + { + name: "get drive model in default format", + id: testDriveModelID, + flags: []string{"--location-id", fmt.Sprint(testLocationID), "--server-model-id", fmt.Sprint(testServerModelID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.txt")), + }, + { + name: "get drive model in JSON format", + id: testDriveModelID, + output: "json", + flags: []string{"--location-id", fmt.Sprint(testLocationID), "--server-model-id", fmt.Sprint(testServerModelID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + }, + { + name: "get drive model in YAML format", + id: testDriveModelID, + output: "yaml", + flags: []string{"--location-id", fmt.Sprint(testLocationID), "--server-model-id", fmt.Sprint(testServerModelID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.yaml")), + }, + { + name: "get drive model with service error", + id: testDriveModelID, + flags: []string{"--location-id", fmt.Sprint(testLocationID), "--server-model-id", fmt.Sprint(testServerModelID)}, + expectError: true, + }, + { + name: "get drive model missing required flags", + id: testDriveModelID, + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + locationsServiceHandler := mocks.NewMockLocationsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.Locations = locationsServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tc.expectError && len(tc.flags) > 0 { + err = errors.New("some error") + } + + if len(tc.flags) > 0 { + locationsServiceHandler.EXPECT(). + GetDriveModelOption(gomock.Any(), testLocationID, testServerModelID, tc.id). + Return(&testDriveModelOption, err) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + driveCmd := NewCmd(testCmdContext) + + args := []string{"drive-models", "get", fmt.Sprint(tc.id)} + if len(tc.flags) > 0 { + args = append(args, tc.flags...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(driveCmd). + WithArgs(args) + + cmd := builder.Build() + + execErr := cmd.Execute() + + if tc.expectError { + g.Expect(execErr).To(HaveOccurred()) + } else { + g.Expect(execErr).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestListDriveModelOptionsCmd(t *testing.T) { + d1 := testDriveModelOption + d2 := testDriveModelOption + d2.ID = testDriveModelID + 1 + d2.Name = "hdd-model-741" + d2.MediaType = "HDD" + + testCases := []struct { + name string + output string + args []string + expectedOutput []byte + expectError bool + configureMock func(*mocks.MockCollection[serverscom.DriveModel]) + }{ + { + name: "list all drive models", + output: "json", + args: []string{"-A", "--location-id", "1", "--server-model-id", "100"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_all.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.DriveModel]) { + mock.EXPECT(). + Collect(gomock.Any()). + Return([]serverscom.DriveModel{d1, d2}, nil) + }, + }, + { + name: "list drive models", + output: "json", + args: []string{"--location-id", "1", "--server-model-id", "100"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.DriveModel]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.DriveModel{d1}, nil) + }, + }, + { + name: "list drive models with template", + args: []string{"--template", "{{range .}}ID: {{.ID}} MediaType: {{.MediaType}}\n{{end}}", "--location-id", "1", "--server-model-id", "100"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_template.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.DriveModel]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.DriveModel{d1, d2}, nil) + }, + }, + { + name: "list drive models with pageView", + args: []string{"--page-view", "--location-id", "1", "--server-model-id", "100"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_pageview.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.DriveModel]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.DriveModel{d1, d2}, nil) + }, + }, + { + name: "list drive models with error", + args: []string{"--location-id", "1", "--server-model-id", "100"}, + expectError: true, + configureMock: func(mock *mocks.MockCollection[serverscom.DriveModel]) { + mock.EXPECT(). + List(gomock.Any()). + Return(nil, errors.New("some error")) + }, + }, + { + name: "list drive models missing required flags", + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + locationsServiceHandler := mocks.NewMockLocationsService(mockCtrl) + collectionHandler := mocks.NewMockCollection[serverscom.DriveModel](mockCtrl) + + locationsServiceHandler.EXPECT(). + DriveModelOptions(gomock.Any(), gomock.Any()). + Return(collectionHandler). + AnyTimes() + + collectionHandler.EXPECT(). + SetParam(gomock.Any(), gomock.Any()). + Return(collectionHandler). + AnyTimes() + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.Locations = locationsServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(collectionHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + driveCmd := NewCmd(testCmdContext) + + args := []string{"drive-models", "list"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(driveCmd). + WithArgs(args) + + cmd := builder.Build() + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} diff --git a/cmd/entities/drivemodels/get.go b/cmd/entities/drivemodels/get.go new file mode 100644 index 0000000..0822362 --- /dev/null +++ b/cmd/entities/drivemodels/get.go @@ -0,0 +1,54 @@ +package drivemodels + +import ( + "fmt" + "strconv" + + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newGetCmd(cmdContext *base.CmdContext) *cobra.Command { + var locationID int64 + var serverModelID int64 + + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a drive model for a server model", + Long: "Get a drive model for a server model by id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + driveModelID, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("drive model id should be integer") + } + + model, err := scClient.Locations.GetDriveModelOption(ctx, locationID, serverModelID, int64(driveModelID)) + if err != nil { + return err + } + + if model != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(model) + } + return nil + }, + } + + cmd.Flags().Int64Var(&locationID, "location-id", 0, "Location id (int, required)") + cmd.Flags().Int64Var(&serverModelID, "server-model-id", 0, "Server model id (int, required)") + _ = cmd.MarkFlagRequired("location-id") + _ = cmd.MarkFlagRequired("server-model-id") + + return cmd +} diff --git a/cmd/entities/drivemodels/list.go b/cmd/entities/drivemodels/list.go new file mode 100644 index 0000000..7af409c --- /dev/null +++ b/cmd/entities/drivemodels/list.go @@ -0,0 +1,33 @@ +package drivemodels + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newListCmd(cmdContext *base.CmdContext) *cobra.Command { + var locationID int64 + var serverModelID int64 + + factory := func(verbose bool, args ...string) serverscom.Collection[serverscom.DriveModel] { + scClient := cmdContext.GetClient().SetVerbose(verbose).GetScClient() + return scClient.Locations.DriveModelOptions(locationID, serverModelID) + } + + opts := base.NewListOptions( + &base.BaseListOptions[serverscom.DriveModel]{}, + &base.SearchPatternOption[serverscom.DriveModel]{}, + &base.DriveMediaTypeOption[serverscom.DriveModel]{}, + &base.DriveInterfaceOption[serverscom.DriveModel]{}, + ) + + cmd := base.NewListCmd("list", "drive-models", factory, cmdContext, opts...) + + cmd.Flags().Int64Var(&locationID, "location-id", 0, "Location ID (required)") + cmd.Flags().Int64Var(&serverModelID, "server-model-id", 0, "Server model ID (required)") + _ = cmd.MarkFlagRequired("location-id") + _ = cmd.MarkFlagRequired("server-model-id") + + return cmd +} diff --git a/cmd/entities/servermodels/get.go b/cmd/entities/servermodels/get.go new file mode 100644 index 0000000..bcbb66d --- /dev/null +++ b/cmd/entities/servermodels/get.go @@ -0,0 +1,51 @@ +package servermodels + +import ( + "fmt" + "strconv" + + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newGetCmd(cmdContext *base.CmdContext) *cobra.Command { + var locationID int64 + + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a server model", + Long: "Get a server model by id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + serverModelID, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("server model id should be integer") + } + + model, err := scClient.Locations.GetServerModelOption(ctx, locationID, int64(serverModelID)) + if err != nil { + return err + } + + if model != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(model) + } + return nil + }, + } + + cmd.Flags().Int64Var(&locationID, "location-id", 0, "Location id (int, required)") + _ = cmd.MarkFlagRequired("location-id") + + return cmd +} diff --git a/cmd/entities/servermodels/list.go b/cmd/entities/servermodels/list.go new file mode 100644 index 0000000..593fa05 --- /dev/null +++ b/cmd/entities/servermodels/list.go @@ -0,0 +1,29 @@ +package servermodels + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newListCmd(cmdContext *base.CmdContext) *cobra.Command { + var locationID int64 + + factory := func(verbose bool, args ...string) serverscom.Collection[serverscom.ServerModelOption] { + scClient := cmdContext.GetClient().SetVerbose(verbose).GetScClient() + return scClient.Locations.ServerModelOptions(locationID) + } + + opts := base.NewListOptions( + &base.BaseListOptions[serverscom.ServerModelOption]{}, + &base.SearchPatternOption[serverscom.ServerModelOption]{}, + &base.HasRaidControllerOption[serverscom.ServerModelOption]{}, + ) + + cmd := base.NewListCmd("list", "server-models", factory, cmdContext, opts...) + + cmd.Flags().Int64Var(&locationID, "location-id", 0, "Location ID (required)") + _ = cmd.MarkFlagRequired("location-id") + + return cmd +} diff --git a/cmd/entities/servermodels/servermodels.go b/cmd/entities/servermodels/servermodels.go new file mode 100644 index 0000000..903f252 --- /dev/null +++ b/cmd/entities/servermodels/servermodels.go @@ -0,0 +1,53 @@ +package servermodels + +import ( + "log" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/serverscom/srvctl/internal/output/entities" + "github.com/spf13/cobra" +) + +func NewCmd(cmdContext *base.CmdContext) *cobra.Command { + entitiesMap, err := getServerModelOptionsEntities() + if err != nil { + log.Fatal(err) + } + cmd := &cobra.Command{ + Use: "server-models", + Short: "Manage server models", + PersistentPreRunE: base.CombinePreRunE( + base.CheckFormatterFlags(cmdContext, entitiesMap), + base.CheckEmptyContexts(cmdContext), + ), + Args: base.NoArgs, + Run: base.UsageRun, + } + + cmd.AddCommand( + newListCmd(cmdContext), + newGetCmd(cmdContext), + ) + + base.AddFormatFlags(cmd) + + return cmd +} + +func getServerModelOptionsEntities() (map[string]entities.EntityInterface, error) { + result := make(map[string]entities.EntityInterface) + getEntity, err := entities.Registry.GetEntityFromValue(serverscom.ServerModelOptionDetail{}) + if err != nil { + return nil, err + } + listEntity, err := entities.Registry.GetEntityFromValue(serverscom.ServerModelOption{}) + if err != nil { + return nil, err + } + + result["get"] = getEntity + result["list"] = listEntity + + return result, nil +} diff --git a/cmd/entities/servermodels/servermodels_test.go b/cmd/entities/servermodels/servermodels_test.go new file mode 100644 index 0000000..13e6720 --- /dev/null +++ b/cmd/entities/servermodels/servermodels_test.go @@ -0,0 +1,275 @@ +package servermodels + +import ( + "errors" + "fmt" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/testutils" + "github.com/serverscom/srvctl/internal/mocks" + "go.uber.org/mock/gomock" +) + +var ( + fixtureBasePath = filepath.Join("..", "..", "..", "testdata", "entities", "server-models") + testLocationID = int64(1) + testServerModelID = int64(100) + + testServerModelOption = serverscom.ServerModelOption{ + ID: testServerModelID, + Name: "server-model-123", + CPUName: "Intel Xeon Silver 4214", + CPUCount: 2, + CPUCoresCount: 24, + CPUFrequency: 2200, + RAM: 64, + RAMType: "DDR4 ECC", + MaxRAM: 2048, + HasRAIDController: true, + RAIDControllerName: "PERC H740P", + DriveSlotsCount: 16, + } + + testServerModelOptionDetail = serverscom.ServerModelOptionDetail{ + ID: testServerModelID, + Name: "server-model-123", + CPUName: "Intel Xeon Silver 4214", + CPUCount: 2, + CPUCoresCount: 24, + CPUFrequency: 2200, + RAM: 64, + RAMType: "DDR4 ECC", + MaxRAM: 2048, + HasRAIDController: true, + RAIDControllerName: "PERC H740P", + DriveSlotsCount: 16, + DriveSlots: []serverscom.ServerModelDriveSlot{ + {Position: 1, Interface: "SAS", FormFactor: "2.5\"", DriveModelID: 101, HotSwappable: true}, + {Position: 2, Interface: "SAS", FormFactor: "2.5\"", DriveModelID: 102, HotSwappable: true}, + }, + } +) + +func TestGetServerModelOptionCmd(t *testing.T) { + testCases := []struct { + name string + id int64 + output string + flags []string + expectedOutput []byte + expectError bool + }{ + { + name: "get server model in default format", + id: testServerModelID, + flags: []string{"--location-id", fmt.Sprint(testLocationID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.txt")), + }, + { + name: "get server model in JSON format", + id: testServerModelID, + output: "json", + flags: []string{"--location-id", fmt.Sprint(testLocationID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + }, + { + name: "get server model in YAML format", + id: testServerModelID, + output: "yaml", + flags: []string{"--location-id", fmt.Sprint(testLocationID)}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.yaml")), + }, + { + name: "get server model with service error", + id: testServerModelID, + flags: []string{"--location-id", fmt.Sprint(testLocationID)}, + expectError: true, + }, + { + name: "get server model missing required flags", + id: testServerModelID, + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + locationsServiceHandler := mocks.NewMockLocationsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.Locations = locationsServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tc.expectError && len(tc.flags) > 0 { + err = errors.New("some error") + } + + if len(tc.flags) > 0 { + locationsServiceHandler.EXPECT(). + GetServerModelOption(gomock.Any(), testLocationID, tc.id). + Return(&testServerModelOptionDetail, err) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + serverCmd := NewCmd(testCmdContext) + + args := []string{"server-models", "get", fmt.Sprint(tc.id)} + if len(tc.flags) > 0 { + args = append(args, tc.flags...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(serverCmd). + WithArgs(args) + + cmd := builder.Build() + + execErr := cmd.Execute() + + if tc.expectError { + g.Expect(execErr).To(HaveOccurred()) + } else { + g.Expect(execErr).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestListServerModelOptionsCmd(t *testing.T) { + s1 := testServerModelOption + s2 := testServerModelOption + s2.ID = testServerModelID + 1 + s2.Name = "server-model-456" + + testCases := []struct { + name string + output string + args []string + expectedOutput []byte + expectError bool + configureMock func(*mocks.MockCollection[serverscom.ServerModelOption]) + }{ + { + name: "list all server models", + output: "json", + args: []string{"-A", "--location-id", "1"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_all.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.ServerModelOption]) { + mock.EXPECT(). + Collect(gomock.Any()). + Return([]serverscom.ServerModelOption{s1, s2}, nil) + }, + }, + { + name: "list server models", + output: "json", + args: []string{"--location-id", "1"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.ServerModelOption]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.ServerModelOption{s1}, nil) + }, + }, + { + name: "list server models with template", + args: []string{"--template", "{{range .}}ID: {{.ID}} Name: {{.Name}}\n{{end}}", "--location-id", "1"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_template.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.ServerModelOption]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.ServerModelOption{s1, s2}, nil) + }, + }, + { + name: "list server models with pageView", + args: []string{"--page-view", "--location-id", "1"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_pageview.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.ServerModelOption]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.ServerModelOption{s1, s2}, nil) + }, + }, + { + name: "list server models with error", + args: []string{"--location-id", "1"}, + expectError: true, + configureMock: func(mock *mocks.MockCollection[serverscom.ServerModelOption]) { + mock.EXPECT(). + List(gomock.Any()). + Return(nil, errors.New("some error")) + }, + }, + { + name: "list server models missing required flags", + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + locationsServiceHandler := mocks.NewMockLocationsService(mockCtrl) + collectionHandler := mocks.NewMockCollection[serverscom.ServerModelOption](mockCtrl) + + locationsServiceHandler.EXPECT(). + ServerModelOptions(gomock.Any()). + Return(collectionHandler). + AnyTimes() + + collectionHandler.EXPECT(). + SetParam(gomock.Any(), gomock.Any()). + Return(collectionHandler). + AnyTimes() + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.Locations = locationsServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(collectionHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + serverCmd := NewCmd(testCmdContext) + + args := []string{"server-models", "list"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(serverCmd). + WithArgs(args) + + cmd := builder.Build() + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 7dbd6ad..6133e26 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,12 +5,14 @@ import ( "github.com/serverscom/srvctl/cmd/config" "github.com/serverscom/srvctl/cmd/context" "github.com/serverscom/srvctl/cmd/entities/account" + "github.com/serverscom/srvctl/cmd/entities/drivemodels" "github.com/serverscom/srvctl/cmd/entities/hosts" "github.com/serverscom/srvctl/cmd/entities/invoices" "github.com/serverscom/srvctl/cmd/entities/k8s" loadbalancers "github.com/serverscom/srvctl/cmd/entities/load_balancers" "github.com/serverscom/srvctl/cmd/entities/locations" "github.com/serverscom/srvctl/cmd/entities/racks" + "github.com/serverscom/srvctl/cmd/entities/servermodels" sshkeys "github.com/serverscom/srvctl/cmd/entities/ssh-keys" "github.com/serverscom/srvctl/cmd/entities/ssl" "github.com/serverscom/srvctl/cmd/entities/uplinkbandwidths" @@ -56,6 +58,8 @@ func NewRootCmd(version string) *cobra.Command { cmd.AddCommand(k8s.NewCmd(cmdContext)) cmd.AddCommand(uplinkmodels.NewCmd(cmdContext)) cmd.AddCommand(uplinkbandwidths.NewCmd(cmdContext)) + cmd.AddCommand(servermodels.NewCmd(cmdContext)) + cmd.AddCommand(drivemodels.NewCmd(cmdContext)) return cmd } diff --git a/internal/output/entities/drivemodels.go b/internal/output/entities/drivemodels.go new file mode 100644 index 0000000..1864d53 --- /dev/null +++ b/internal/output/entities/drivemodels.go @@ -0,0 +1,29 @@ +package entities + +import ( + "log" + "reflect" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" +) + +var ( + DriveModelOptionType = reflect.TypeOf(serverscom.DriveModel{}) +) + +func RegisterDriveModelOptionDefinition() { + driveModelOptionEntity := &Entity{ + fields: []Field{ + {ID: "ID", Name: "ID", Path: "ID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Name", Name: "Name", Path: "Name", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Capacity", Name: "Capacity", Path: "Capacity", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Interface", Name: "Interface", Path: "Interface", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "FormFactor", Name: "FormFactor", Path: "FormFactor", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "MediaType", Name: "MediaType", Path: "MediaType", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + }, + eType: DriveModelOptionType, + } + if err := Registry.Register(driveModelOptionEntity); err != nil { + log.Fatal(err) + } +} diff --git a/internal/output/entities/hosts.go b/internal/output/entities/hosts.go index b2b60ea..9fcedea 100644 --- a/internal/output/entities/hosts.go +++ b/internal/output/entities/hosts.go @@ -120,8 +120,7 @@ func RegisterDedicatedServerDefinition() { {ID: "Updated", Name: "Updated", Path: "Updated", ListHandlerFunc: timeHandler, PageViewHandlerFunc: timeHandler, Default: true}, getConfigurationDetailsField(), }, - cmdDefaultFields: CmdDefaultFields, - eType: DedicatedServerType, + eType: DedicatedServerType, } if err := Registry.Register(serverEntity); err != nil { log.Fatal(err) @@ -154,8 +153,7 @@ func RegisterKubernetesBaremetalNodeDefinition() { {ID: "Updated", Name: "Updated", Path: "Updated", ListHandlerFunc: timeHandler, PageViewHandlerFunc: timeHandler, Default: true}, getConfigurationDetailsField(), }, - cmdDefaultFields: CmdDefaultFields, - eType: KubernetesBaremetalNodeType, + eType: KubernetesBaremetalNodeType, } if err := Registry.Register(serverEntity); err != nil { log.Fatal(err) @@ -185,8 +183,7 @@ func RegisterSBMServerDefinition() { {ID: "Updated", Name: "Updated", Path: "Updated", ListHandlerFunc: timeHandler, PageViewHandlerFunc: timeHandler, Default: true}, getConfigurationDetailsField(), }, - cmdDefaultFields: CmdDefaultFields, - eType: SBMServerType, + eType: SBMServerType, } if err := Registry.Register(serverEntity); err != nil { log.Fatal(err) diff --git a/internal/output/entities/init.go b/internal/output/entities/init.go index 020848e..e0e29a3 100644 --- a/internal/output/entities/init.go +++ b/internal/output/entities/init.go @@ -24,4 +24,6 @@ func init() { RegisterKubernetesClusterNodeDefinition() RegisterUplinkOptionDefinition() RegisterBandwidthOptionDefinition() + RegisterDriveModelOptionDefinition() + RegisterServerModelOptionDefinitions() } diff --git a/internal/output/entities/servermodels.go b/internal/output/entities/servermodels.go new file mode 100644 index 0000000..9a29b9e --- /dev/null +++ b/internal/output/entities/servermodels.go @@ -0,0 +1,79 @@ +package entities + +import ( + "log" + "reflect" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" +) + +var ( + ServerModelOptionType = reflect.TypeOf(serverscom.ServerModelOption{}) + ServerModelOptionDetailType = reflect.TypeOf(serverscom.ServerModelOptionDetail{}) +) + +func getServerModelDriveSlotsField() Field { + f := Field{ + ID: "DriveSlots", + Name: "DriveSlots", + Path: "DriveSlots", + PageViewHandlerFunc: structPVHandler, + } + childs := []Field{ + {ID: "Position", Name: "Position", Path: "Position", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Parent: &f}, + {ID: "Interface", Name: "Interface", Path: "Interface", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Parent: &f}, + {ID: "FormFactor", Name: "FormFactor", Path: "FormFactor", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Parent: &f}, + {ID: "DriveModelID", Name: "DriveModelID", Path: "DriveModelID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Parent: &f}, + {ID: "HotSwappable", Name: "HotSwappable", Path: "HotSwappable", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Parent: &f}, + } + f.ChildFields = append(f.ChildFields, childs...) + return f +} + +func RegisterServerModelOptionDefinitions() { + + // used for list cmd + serverModelOptionEntity := &Entity{ + fields: []Field{ + {ID: "ID", Name: "ID", Path: "ID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Name", Name: "Name", Path: "Name", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUName", Name: "CPUName", Path: "CPUName", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "CPUCount", Name: "CPUCount", Path: "CPUCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUCoresCount", Name: "CPUCoresCount", Path: "CPUCoresCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUFrequency", Name: "CPUFrequency", Path: "CPUFrequency", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "RAM", Name: "RAM", Path: "RAM", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RAMType", Name: "RAMType", Path: "RAMType", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "MaxRAM", Name: "MaxRAM", Path: "MaxRAM", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "HasRAIDController", Name: "HasRAIDController", Path: "HasRAIDController", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RAIDControllerName", Name: "RAIDControllerName", Path: "RAIDControllerName", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "DriveSlotsCount", Name: "DriveSlotsCount", Path: "DriveSlotsCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + }, + eType: ServerModelOptionType, + } + if err := Registry.Register(serverModelOptionEntity); err != nil { + log.Fatal(err) + } + + // used for get cmd + serverModelOptionDetailEntity := &Entity{ + fields: []Field{ + {ID: "ID", Name: "ID", Path: "ID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Name", Name: "Name", Path: "Name", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUName", Name: "CPUName", Path: "CPUName", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUCount", Name: "CPUCount", Path: "CPUCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUCoresCount", Name: "CPUCoresCount", Path: "CPUCoresCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "CPUFrequency", Name: "CPUFrequency", Path: "CPUFrequency", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RAM", Name: "RAM", Path: "RAM", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RAMType", Name: "RAMType", Path: "RAMType", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "MaxRAM", Name: "MaxRAM", Path: "MaxRAM", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "HasRAIDController", Name: "HasRAIDController", Path: "HasRAIDController", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RAIDControllerName", Name: "RAIDControllerName", Path: "RAIDControllerName", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "DriveSlotsCount", Name: "DriveSlotsCount", Path: "DriveSlotsCount", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + getServerModelDriveSlotsField(), + }, + eType: ServerModelOptionDetailType, + } + if err := Registry.Register(serverModelOptionDetailEntity); err != nil { + log.Fatal(err) + } +} diff --git a/testdata/entities/drive-models/get.json b/testdata/entities/drive-models/get.json new file mode 100644 index 0000000..119c9bc --- /dev/null +++ b/testdata/entities/drive-models/get.json @@ -0,0 +1,8 @@ +{ + "id": 10, + "name": "ssd-model-749", + "capacity": 100, + "interface": "SATA3", + "form_factor": "2.5", + "media_type": "SSD" +} \ No newline at end of file diff --git a/testdata/entities/drive-models/get.txt b/testdata/entities/drive-models/get.txt new file mode 100644 index 0000000..9d1b183 --- /dev/null +++ b/testdata/entities/drive-models/get.txt @@ -0,0 +1,2 @@ +ID Name Capacity Interface FormFactor MediaType +10 ssd-model-749 100 SATA3 2.5 SSD diff --git a/testdata/entities/drive-models/get.yaml b/testdata/entities/drive-models/get.yaml new file mode 100644 index 0000000..2ae32fa --- /dev/null +++ b/testdata/entities/drive-models/get.yaml @@ -0,0 +1,6 @@ +id: 10 +name: ssd-model-749 +capacity: 100 +interface: SATA3 +formfactor: "2.5" +mediatype: SSD diff --git a/testdata/entities/drive-models/list.json b/testdata/entities/drive-models/list.json new file mode 100644 index 0000000..05fe66f --- /dev/null +++ b/testdata/entities/drive-models/list.json @@ -0,0 +1,10 @@ +[ + { + "id": 10, + "name": "ssd-model-749", + "capacity": 100, + "interface": "SATA3", + "form_factor": "2.5", + "media_type": "SSD" + } +] \ No newline at end of file diff --git a/testdata/entities/drive-models/list_all.json b/testdata/entities/drive-models/list_all.json new file mode 100644 index 0000000..8612f8d --- /dev/null +++ b/testdata/entities/drive-models/list_all.json @@ -0,0 +1,18 @@ +[ + { + "id": 10, + "name": "ssd-model-749", + "capacity": 100, + "interface": "SATA3", + "form_factor": "2.5", + "media_type": "SSD" + }, + { + "id": 11, + "name": "hdd-model-741", + "capacity": 100, + "interface": "SATA3", + "form_factor": "2.5", + "media_type": "HDD" + } +] \ No newline at end of file diff --git a/testdata/entities/drive-models/list_pageview.txt b/testdata/entities/drive-models/list_pageview.txt new file mode 100644 index 0000000..2e48cf8 --- /dev/null +++ b/testdata/entities/drive-models/list_pageview.txt @@ -0,0 +1,13 @@ +ID: 10 +Name: ssd-model-749 +Capacity: 100 +Interface: SATA3 +FormFactor: 2.5 +MediaType: SSD +--- +ID: 11 +Name: hdd-model-741 +Capacity: 100 +Interface: SATA3 +FormFactor: 2.5 +MediaType: HDD diff --git a/testdata/entities/drive-models/list_template.txt b/testdata/entities/drive-models/list_template.txt new file mode 100644 index 0000000..9db6f64 --- /dev/null +++ b/testdata/entities/drive-models/list_template.txt @@ -0,0 +1,2 @@ +ID: 10 MediaType: SSD +ID: 11 MediaType: HDD diff --git a/testdata/entities/server-models/get.json b/testdata/entities/server-models/get.json new file mode 100644 index 0000000..c05f517 --- /dev/null +++ b/testdata/entities/server-models/get.json @@ -0,0 +1,30 @@ +{ + "id": 100, + "name": "server-model-123", + "cpu_name": "Intel Xeon Silver 4214", + "cpu_count": 2, + "cpu_cores_count": 24, + "cpu_frequency": 2200, + "ram": 64, + "ram_type": "DDR4 ECC", + "max_ram": 2048, + "has_raid_controller": true, + "raid_controller_name": "PERC H740P", + "drive_slots_count": 16, + "drive_slots": [ + { + "position": 1, + "interface": "SAS", + "form_factor": "2.5\"", + "drive_model_id": 101, + "hot_swappable": true + }, + { + "position": 2, + "interface": "SAS", + "form_factor": "2.5\"", + "drive_model_id": 102, + "hot_swappable": true + } + ] +} \ No newline at end of file diff --git a/testdata/entities/server-models/get.txt b/testdata/entities/server-models/get.txt new file mode 100644 index 0000000..c8b2328 --- /dev/null +++ b/testdata/entities/server-models/get.txt @@ -0,0 +1,2 @@ +ID Name CPUName CPUCount CPUCoresCount CPUFrequency RAM RAMType MaxRAM HasRAIDController RAIDControllerName DriveSlotsCount +100 server-model-123 Intel Xeon Silver 4214 2 24 2200 64 DDR4 ECC 2048 true PERC H740P 16 diff --git a/testdata/entities/server-models/get.yaml b/testdata/entities/server-models/get.yaml new file mode 100644 index 0000000..073a758 --- /dev/null +++ b/testdata/entities/server-models/get.yaml @@ -0,0 +1,23 @@ +id: 100 +name: server-model-123 +cpuname: Intel Xeon Silver 4214 +cpucount: 2 +cpucorescount: 24 +cpufrequency: 2200 +ram: 64 +ramtype: DDR4 ECC +maxram: 2048 +hasraidcontroller: true +raidcontrollername: PERC H740P +driveslotscount: 16 +driveslots: + - position: 1 + interface: SAS + formfactor: 2.5" + drivemodelid: 101 + hotswappable: true + - position: 2 + interface: SAS + formfactor: 2.5" + drivemodelid: 102 + hotswappable: true diff --git a/testdata/entities/server-models/list.json b/testdata/entities/server-models/list.json new file mode 100644 index 0000000..5e8df25 --- /dev/null +++ b/testdata/entities/server-models/list.json @@ -0,0 +1,16 @@ +[ + { + "id": 100, + "name": "server-model-123", + "cpu_name": "Intel Xeon Silver 4214", + "cpu_count": 2, + "cpu_cores_count": 24, + "cpu_frequency": 2200, + "ram": 64, + "ram_type": "DDR4 ECC", + "max_ram": 2048, + "has_raid_controller": true, + "raid_controller_name": "PERC H740P", + "drive_slots_count": 16 + } +] \ No newline at end of file diff --git a/testdata/entities/server-models/list_all.json b/testdata/entities/server-models/list_all.json new file mode 100644 index 0000000..2543f95 --- /dev/null +++ b/testdata/entities/server-models/list_all.json @@ -0,0 +1,30 @@ +[ + { + "id": 100, + "name": "server-model-123", + "cpu_name": "Intel Xeon Silver 4214", + "cpu_count": 2, + "cpu_cores_count": 24, + "cpu_frequency": 2200, + "ram": 64, + "ram_type": "DDR4 ECC", + "max_ram": 2048, + "has_raid_controller": true, + "raid_controller_name": "PERC H740P", + "drive_slots_count": 16 + }, + { + "id": 101, + "name": "server-model-456", + "cpu_name": "Intel Xeon Silver 4214", + "cpu_count": 2, + "cpu_cores_count": 24, + "cpu_frequency": 2200, + "ram": 64, + "ram_type": "DDR4 ECC", + "max_ram": 2048, + "has_raid_controller": true, + "raid_controller_name": "PERC H740P", + "drive_slots_count": 16 + } +] \ No newline at end of file diff --git a/testdata/entities/server-models/list_pageview.txt b/testdata/entities/server-models/list_pageview.txt new file mode 100644 index 0000000..77e1ad9 --- /dev/null +++ b/testdata/entities/server-models/list_pageview.txt @@ -0,0 +1,25 @@ +ID: 100 +Name: server-model-123 +CPUName: Intel Xeon Silver 4214 +CPUCount: 2 +CPUCoresCount: 24 +CPUFrequency: 2200 +RAM: 64 +RAMType: DDR4 ECC +MaxRAM: 2048 +HasRAIDController: true +RAIDControllerName: PERC H740P +DriveSlotsCount: 16 +--- +ID: 101 +Name: server-model-456 +CPUName: Intel Xeon Silver 4214 +CPUCount: 2 +CPUCoresCount: 24 +CPUFrequency: 2200 +RAM: 64 +RAMType: DDR4 ECC +MaxRAM: 2048 +HasRAIDController: true +RAIDControllerName: PERC H740P +DriveSlotsCount: 16 diff --git a/testdata/entities/server-models/list_template.txt b/testdata/entities/server-models/list_template.txt new file mode 100644 index 0000000..249cfef --- /dev/null +++ b/testdata/entities/server-models/list_template.txt @@ -0,0 +1,2 @@ +ID: 100 Name: server-model-123 +ID: 101 Name: server-model-456