Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions cmd/entities/hosts/features.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package hosts

import (
"context"
"fmt"

serverscom "github.com/serverscom/serverscom-go-client/pkg"
"github.com/serverscom/srvctl/cmd/base"
"github.com/spf13/cobra"
Expand All @@ -20,3 +23,117 @@ func newListEBMFeaturesCmd(cmdContext *base.CmdContext) *cobra.Command {

return cmd
}

type featureSetFlags struct {
Feature string
State string
IPXEConfig string
AuthMethods []string
SSHKeyFingerprints []string
}

func newEBMFeatureSetCmd(cmdContext *base.CmdContext) *cobra.Command {
flags := &featureSetFlags{}

cmd := &cobra.Command{
Use: "feature-set <id>",
Short: "Activate or deactivate a feature on an enterprise bare metal server",
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()
id := args[0]

var (
result *serverscom.DedicatedServerFeature
err error
)

switch flags.State {
case "activate":
result, err = activateEBMFeature(ctx, scClient, id, flags)
case "deactivate":
result, err = deactivateEBMFeature(ctx, scClient, id, flags.Feature)
default:
return fmt.Errorf("invalid state %q: must be activate or deactivate", flags.State)
}

if err != nil {
return err
}

if result != nil {
formatter := cmdContext.GetOrCreateFormatter(cmd)
return formatter.Format(result)
}

return nil
},
}

cmd.Flags().StringVar(&flags.Feature, "feature", "", "feature name (required)")
cmd.Flags().StringVar(&flags.State, "state", "", "desired state: activate or deactivate (required)")
cmd.Flags().StringVar(&flags.IPXEConfig, "ipxe-config", "", "iPXE config script (for private_ipxe_boot)")
cmd.Flags().StringArrayVar(&flags.AuthMethods, "auth-methods", nil, "auth methods: password, ssh_key (for host_rescue_mode)")
cmd.Flags().StringArrayVar(&flags.SSHKeyFingerprints, "ssh-key-fingerprints", nil, "SSH key fingerprints (for host_rescue_mode with ssh_key auth)")

_ = cmd.MarkFlagRequired("feature")
_ = cmd.MarkFlagRequired("state")

return cmd
}

func activateEBMFeature(ctx context.Context, client *serverscom.Client, id string, flags *featureSetFlags) (*serverscom.DedicatedServerFeature, error) {
switch flags.Feature {
case "disaggregated_public_ports":
return client.Hosts.ActivateDisaggregatedPublicPortsFeature(ctx, id)
case "disaggregated_private_ports":
return client.Hosts.ActivateDisaggregatedPrivatePortsFeature(ctx, id)
case "no_public_ip_address":
return client.Hosts.ActivateNoPublicIpAddressFeature(ctx, id)
case "no_private_ip":
return client.Hosts.ActivateNoPrivateIpFeature(ctx, id)
case "oob_public_access":
return client.Hosts.ActivateOobPublicAccessFeature(ctx, id)
case "no_public_network":
return client.Hosts.ActivateNoPublicNetworkFeature(ctx, id)
case "host_rescue_mode":
return client.Hosts.ActivateHostRescueModeFeature(ctx, id, serverscom.HostRescueModeFeatureInput{
AuthMethods: flags.AuthMethods,
SSHKeyFingerprints: flags.SSHKeyFingerprints,
})
case "private_ipxe_boot":
return client.Hosts.ActivatePrivateIpxeBootFeature(ctx, id, serverscom.PrivateIpxeBootFeatureInput{
IPXEConfig: flags.IPXEConfig,
})
default:
return nil, fmt.Errorf("unsupported feature: %s", flags.Feature)
}
}

func deactivateEBMFeature(ctx context.Context, client *serverscom.Client, id, feature string) (*serverscom.DedicatedServerFeature, error) {
switch feature {
case "disaggregated_public_ports":
return client.Hosts.DeactivateDisaggregatedPublicPortsFeature(ctx, id)
case "disaggregated_private_ports":
return client.Hosts.DeactivateDisaggregatedPrivatePortsFeature(ctx, id)
case "no_public_ip_address":
return client.Hosts.DeactivateNoPublicIpAddressFeature(ctx, id)
case "no_private_ip":
return client.Hosts.DeactivateNoPrivateIpFeature(ctx, id)
case "oob_public_access":
return client.Hosts.DeactivateOobPublicAccessFeature(ctx, id)
case "no_public_network":
return client.Hosts.DeactivateNoPublicNetworkFeature(ctx, id)
case "host_rescue_mode":
return client.Hosts.DeactivateHostRescueModeFeature(ctx, id)
case "private_ipxe_boot":
return client.Hosts.DeactivatePrivateIpxeBootFeature(ctx, id)
default:
return nil, fmt.Errorf("unsupported feature: %s", feature)
}
}
155 changes: 155 additions & 0 deletions cmd/entities/hosts/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ import (

var (
featuresFixtureBasePath = filepath.Join("..", "..", "..", "testdata", "entities", "hosts", "features")

testFeatureResult = serverscom.DedicatedServerFeature{
Name: "disaggregated_public_ports",
Status: "activation",
}
testPrivateIpxeBootResult = serverscom.DedicatedServerFeature{
Name: "private_ipxe_boot",
Status: "activation",
}
testHostRescueModeResult = serverscom.DedicatedServerFeature{
Name: "host_rescue_mode",
Status: "activation",
}
)

func TestListEBMFeaturesCmd(t *testing.T) {
Expand Down Expand Up @@ -138,3 +151,145 @@ func TestListEBMFeaturesCmd(t *testing.T) {
})
}
}

func TestEBMFeatureSetCmd(t *testing.T) {
testCases := []struct {
name string
args []string
expectedOutput []byte
expectError bool
configureMock func(*mocks.MockHostsService)
}{
{
name: "activate feature",
args: []string{testServerID, "--feature", "disaggregated_public_ports", "--state", "activate", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
ActivateDisaggregatedPublicPortsFeature(gomock.Any(), testServerID).
Return(&testFeatureResult, nil)
},
},
{
name: "deactivate feature",
args: []string{testServerID, "--feature", "disaggregated_public_ports", "--state", "deactivate", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
DeactivateDisaggregatedPublicPortsFeature(gomock.Any(), testServerID).
Return(&testFeatureResult, nil)
},
},
{
name: "activate private_ipxe_boot with ipxe-config",
args: []string{testServerID, "--feature", "private_ipxe_boot", "--state", "activate", "--ipxe-config", "#!ipxe\nchain http://boot.example.com", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set_private_ipxe_boot.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
ActivatePrivateIpxeBootFeature(gomock.Any(), testServerID, serverscom.PrivateIpxeBootFeatureInput{
IPXEConfig: "#!ipxe\nchain http://boot.example.com",
}).
Return(&testPrivateIpxeBootResult, nil)
},
},
{
name: "deactivate private_ipxe_boot",
args: []string{testServerID, "--feature", "private_ipxe_boot", "--state", "deactivate", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set_private_ipxe_boot.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
DeactivatePrivateIpxeBootFeature(gomock.Any(), testServerID).
Return(&testPrivateIpxeBootResult, nil)
},
},
{
name: "activate host_rescue_mode with password auth",
args: []string{testServerID, "--feature", "host_rescue_mode", "--state", "activate", "--auth-methods", "password", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set_host_rescue_mode.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
ActivateHostRescueModeFeature(gomock.Any(), testServerID, serverscom.HostRescueModeFeatureInput{
AuthMethods: []string{"password"},
}).
Return(&testHostRescueModeResult, nil)
},
},
{
name: "activate host_rescue_mode with ssh_key auth",
args: []string{testServerID, "--feature", "host_rescue_mode", "--state", "activate", "--auth-methods", "ssh_key", "--ssh-key-fingerprints", "aa:bb:cc", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set_host_rescue_mode.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
ActivateHostRescueModeFeature(gomock.Any(), testServerID, serverscom.HostRescueModeFeatureInput{
AuthMethods: []string{"ssh_key"},
SSHKeyFingerprints: []string{"aa:bb:cc"},
}).
Return(&testHostRescueModeResult, nil)
},
},
{
name: "deactivate host_rescue_mode",
args: []string{testServerID, "--feature", "host_rescue_mode", "--state", "deactivate", "--output", "json"},
expectedOutput: testutils.ReadFixture(filepath.Join(featuresFixtureBasePath, "feature_set_host_rescue_mode.json")),
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
DeactivateHostRescueModeFeature(gomock.Any(), testServerID).
Return(&testHostRescueModeResult, nil)
},
},
{
name: "api error",
args: []string{testServerID, "--feature", "disaggregated_public_ports", "--state", "activate"},
expectError: true,
configureMock: func(mock *mocks.MockHostsService) {
mock.EXPECT().
ActivateDisaggregatedPublicPortsFeature(gomock.Any(), testServerID).
Return(nil, errors.New("some error"))
},
},
{
name: "invalid state",
args: []string{testServerID, "--feature", "disaggregated_public_ports", "--state", "invalid"},
expectError: true,
},
{
name: "unsupported feature",
args: []string{testServerID, "--feature", "unknown_feature", "--state", "activate"},
expectError: true,
},
}

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
hostService := mocks.NewMockHostsService(mockCtrl)

scClient := serverscom.NewClientWithEndpoint("", "")
scClient.Hosts = hostService

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)
if tc.configureMock != nil {
tc.configureMock(hostService)
}

testCmdContext := testutils.NewTestCmdContext(scClient)
hostsCmd := NewCmd(testCmdContext)

args := append([]string{"hosts", "ebm", "feature-set"}, tc.args...)
builder := testutils.NewTestCommandBuilder().
WithCommand(hostsCmd).
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)))
}
})
}
}
1 change: 1 addition & 0 deletions cmd/entities/hosts/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func NewCmd(cmdContext *base.CmdContext) *cobra.Command {
newListEBMCmd,
newListEBMServicesCmd,
newListEBMFeaturesCmd,
newEBMFeatureSetCmd,
newGetEBMOOBCredsCmd,
},
},
Expand Down
4 changes: 4 additions & 0 deletions testdata/entities/hosts/features/feature_set.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "disaggregated_public_ports",
"status": "activation"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "host_rescue_mode",
"status": "activation"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "private_ipxe_boot",
"status": "activation"
}
Loading