diff --git a/internal/ports/convert.go b/internal/ports/convert.go index 39965133..582a5827 100644 --- a/internal/ports/convert.go +++ b/internal/ports/convert.go @@ -35,8 +35,7 @@ import ( func (c *Client) Convert() *cobra.Command { var portID string var bonded, layer2, bulk, force, ipv4, ipv6 bool - // retrievePortCmd represents the retrievePort command - retrievePortCmd := &cobra.Command{ + convertPortCmd := &cobra.Command{ Use: `convert -i [--bonded] [--bulk] --layer2 [--force] [--public-ipv4] [--public-ipv6]`, Aliases: []string{}, Short: "Converts a list of ports or the details of the specified port.", @@ -87,7 +86,10 @@ func (c *Client) Convert() *cobra.Command { return nil, nil, nil } } - return c.PortService.ConvertLayer2(context.Background(), portID).Execute() + + return c.PortService.ConvertLayer2(context.Background(), portID). + PortAssignInput(*metal.NewPortAssignInput()). + Execute() } convToL3 := func(portID string) (*metal.Port, *http.Response, error) { log.Printf("Converting port %s to layer-3 with addresses %v", portID, addrs) @@ -105,8 +107,6 @@ func (c *Client) Convert() *cobra.Command { port, _, err := c.PortService.FindPortById(context.Background(), portID). Include(c.Servicer.Includes(nil)). - // TODO: uncomment this when metal api supports it - // Exclude(c.Servicer.Excludes(nil)). Execute() if err != nil { return fmt.Errorf("Could not get Port: %w", err) @@ -121,31 +121,27 @@ func (c *Client) Convert() *cobra.Command { }, } - retrievePortCmd.Flags().StringVarP(&portID, "port-id", "i", "", "The UUID of a port.") - retrievePortCmd.Flags().BoolVarP(&bonded, "bonded", "b", false, "Convert to layer-2 bonded.") - retrievePortCmd.Flags().BoolVarP(&bulk, "bulk", "", false, "Affect both ports in a bond.") - retrievePortCmd.Flags().BoolVarP(&layer2, "layer2", "2", false, "Convert to layer-2 unbonded.") - retrievePortCmd.Flags().BoolVarP(&force, "force", "f", false, "Force conversion to layer-2 bonded.") - retrievePortCmd.Flags().BoolVarP(&ipv4, "public-ipv4", "4", false, "Convert to layer-2 bonded with public IPv4.") - retrievePortCmd.Flags().BoolVarP(&ipv6, "public-ipv6", "6", false, "Convert to layer-2 bonded with public IPv6.") + convertPortCmd.Flags().StringVarP(&portID, "port-id", "i", "", "The UUID of a port.") + convertPortCmd.Flags().BoolVarP(&bonded, "bonded", "b", false, "Convert to layer-2 bonded.") + convertPortCmd.Flags().BoolVarP(&bulk, "bulk", "", false, "Affect both ports in a bond.") + convertPortCmd.Flags().BoolVarP(&layer2, "layer2", "2", false, "Convert to layer-2 unbonded.") + convertPortCmd.Flags().BoolVarP(&force, "force", "f", false, "Force conversion to layer-2 bonded.") + convertPortCmd.Flags().BoolVarP(&ipv4, "public-ipv4", "4", false, "Convert to layer-2 bonded with public IPv4.") + convertPortCmd.Flags().BoolVarP(&ipv6, "public-ipv6", "6", false, "Convert to layer-2 bonded with public IPv6.") - return retrievePortCmd + return convertPortCmd } func portBondingHandler(cmd *cobra.Command, c *Client, portId string) error { if cmd.Flag("bonded").Changed { _, _, err := c.PortService.BondPort(context.Background(), portId). Include(c.Servicer.Includes(nil)). - // TODO: uncomment this when metal api supports it - // Exclude(c.Servicer.Excludes(nil)). Execute() return err } _, _, err := c.PortService.DisbondPort(context.Background(), portId). Include(c.Servicer.Includes(nil)). - // TODO: uncomment this when metal api supports it - // Exclude(c.Servicer.Excludes(nil)). Execute() return err } diff --git a/internal/ports/port.go b/internal/ports/port.go index 0ceabe5e..2b246b5b 100644 --- a/internal/ports/port.go +++ b/internal/ports/port.go @@ -62,7 +62,6 @@ type Servicer interface { MetalAPI(*cobra.Command) *metal.APIClient Filters() map[string]string Includes(defaultIncludes []string) (incl []string) - Excludes(defaultExcludes []string) (excl []string) } func NewClient(s Servicer, out outputs.Outputer) *Client { diff --git a/internal/ports/retrieve.go b/internal/ports/retrieve.go index 842d2723..248e0251 100644 --- a/internal/ports/retrieve.go +++ b/internal/ports/retrieve.go @@ -42,9 +42,7 @@ func (c *Client) Retrieve() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true port, _, err := c.PortService.FindPortById(context.Background(), portID). - Include(c.Servicer.Excludes(nil)). - // TODO: uncomment this when metal api supports it - // Exclude(c.Servicer.Excludes(nil)). + Include(c.Servicer.Includes(nil)). Execute() if err != nil { return fmt.Errorf("Could not get Port: %w", err) diff --git a/internal/ports/vlans.go b/internal/ports/vlans.go index cc192b9e..88bdd49a 100644 --- a/internal/ports/vlans.go +++ b/internal/ports/vlans.go @@ -79,8 +79,6 @@ func (c *Client) Vlans() *cobra.Command { batch, _, err := c.PortService.CreatePortVlanAssignmentBatch(context.Background(), portID). PortVlanAssignmentBatchCreateInput(*req). Include(c.Servicer.Includes([]string{"port"})). - // TODO: uncomment this when metal api supports it - // Exclude(c.Servicer.Excludes(nil)). Execute() if err != nil { return fmt.Errorf("Could not update port VLAN assignments: %w", err) diff --git a/test/e2e/ports/convert/convert_test.go b/test/e2e/ports/convert/convert_test.go new file mode 100644 index 00000000..b87ac673 --- /dev/null +++ b/test/e2e/ports/convert/convert_test.go @@ -0,0 +1,120 @@ +package convert + +import ( + "io" + "os" + "strconv" + "strings" + "testing" + + root "github.com/equinix/metal-cli/internal/cli" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/internal/ports" + "github.com/equinix/metal-cli/test/helper" + + "github.com/spf13/cobra" +) + +func TestPorts_Convert(t *testing.T) { + var projectId, deviceId *string + subCommand := "port" + consumerToken := "" + apiURL := "" + Version := "devel" + rootClient := root.NewClient(consumerToken, apiURL, Version) + tests := []struct { + name string + cmd *cobra.Command + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "convert port", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + projId, err := helper.CreateTestProject("metal-cli-test-ports-project") + if err != nil { + t.Error(err) + } + projectId = &projId + + devId, err := helper.CreateTestDevice(*projectId, "metal-cli-test-ports-device") + if err != nil { + t.Error(err) + } + deviceId = &devId + + device, err := helper.GetDeviceById(*deviceId) + if len(device.NetworkPorts) < 3 { + t.Errorf("All 3 ports doesnot exist for the created device: %s", device.GetId()) + } + port := device.GetNetworkPorts()[2] + + active, err := helper.IsDeviceStateActive(*deviceId) + if err == nil && active { + root.SetArgs([]string{subCommand, "convert", "-i", port.GetId(), "--layer2", "--bonded", "--force"}) + + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + + if !strings.Contains(string(out[:]), port.GetId()) { + t.Errorf("cmd output should contain ID of the port: %s", port.GetId()) + } + + if !strings.Contains(string(out[:]), port.GetName()) { + t.Errorf("cmd output should contain name of the port: %s", port.GetName()) + } + + if !strings.Contains(string(out[:]), string(port.GetType())) { + t.Errorf("cmd output should contain type of the port: %s", string(port.GetType())) + } + + if !strings.Contains(string(out[:]), strconv.FormatBool(port.Data.GetBonded())) { + t.Errorf("cmd output should contain if port is bonded: %s", strconv.FormatBool(port.Data.GetBonded())) + } + + if err := helper.DeletePort(port.GetId()); err != nil { + t.Error(err) + } + if err := helper.DeleteVLAN(port.GetId()); err != nil { + t.Error(err) + } + if err := helper.CleanupProjectAndDevice(deviceId, projectId); err != nil { + t.Error(err) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := rootClient.NewCommand() + rootCmd.AddCommand(tt.cmd) + tt.cmdFunc(t, tt.cmd) + }) + } +} + +func cleanup(t *testing.T, deviceId, projectId *string) { + resp, err := helper.IsDeviceStateActive(*deviceId) + if err == nil && resp == true { + err = helper.CleanTestDevice(*deviceId) + if err != nil { + t.Error(err) + } + err = helper.CleanTestProject(*projectId) + if err != nil { + t.Error(err) + } + } +} diff --git a/test/e2e/ports/retrieve/retrieve_test.go b/test/e2e/ports/retrieve/retrieve_test.go new file mode 100644 index 00000000..cd7578b6 --- /dev/null +++ b/test/e2e/ports/retrieve/retrieve_test.go @@ -0,0 +1,102 @@ +package retrieve + +import ( + "io" + "os" + "strconv" + "strings" + "testing" + + root "github.com/equinix/metal-cli/internal/cli" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/internal/ports" + "github.com/equinix/metal-cli/test/helper" + + "github.com/spf13/cobra" +) + +func TestPorts_Retrieve(t *testing.T) { + var projectId, deviceId *string + subCommand := "port" + consumerToken := "" + apiURL := "" + Version := "devel" + rootClient := root.NewClient(consumerToken, apiURL, Version) + tests := []struct { + name string + cmd *cobra.Command + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "retrieve port", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + projId, err := helper.CreateTestProject("metal-cli-test-ports-project") + if err != nil { + t.Error(err) + } + projectId = &projId + + devId, err := helper.CreateTestDevice(*projectId, "metal-cli-test-ports-device") + if err != nil { + t.Error(err) + } + deviceId = &devId + + device, err := helper.GetDeviceById(*deviceId) + + if len(device.NetworkPorts) == 0 { + t.Errorf("ports doesnot exist for the created device: %s", device.GetId()) + } + + port := device.GetNetworkPorts()[0] + root.SetArgs([]string{subCommand, "get", "-i", port.GetId()}) + + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + + if !strings.Contains(string(out[:]), port.GetId()) { + t.Errorf("cmd output should contain ID of the port: %s", port.GetId()) + } + + if !strings.Contains(string(out[:]), port.GetName()) { + t.Errorf("cmd output should contain name of the port: %s", port.GetName()) + } + + if !strings.Contains(string(out[:]), string(port.GetType())) { + t.Errorf("cmd output should contain type of the port: %s", string(port.GetType())) + } + + if !strings.Contains(string(out[:]), port.Data.GetMac()) { + t.Errorf("cmd output should contain MAC address of the port: %s", port.Data.GetMac()) + } + + if !strings.Contains(string(out[:]), strconv.FormatBool(port.Data.GetBonded())) { + t.Errorf("cmd output should contain if port is bonded: %s", strconv.FormatBool(port.Data.GetBonded())) + } + + if err := helper.CleanupProjectAndDevice(deviceId, projectId); err != nil { + t.Error(err) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := rootClient.NewCommand() + rootCmd.AddCommand(tt.cmd) + tt.cmdFunc(t, tt.cmd) + }) + } +} diff --git a/test/e2e/ports/vlans/vlans_test.go b/test/e2e/ports/vlans/vlans_test.go new file mode 100644 index 00000000..cba193f4 --- /dev/null +++ b/test/e2e/ports/vlans/vlans_test.go @@ -0,0 +1,121 @@ +package vlans + +import ( + "io" + "os" + "strconv" + "strings" + "testing" + + root "github.com/equinix/metal-cli/internal/cli" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/internal/ports" + "github.com/equinix/metal-cli/test/helper" + + "github.com/spf13/cobra" +) + +func TestPorts_VLANs(t *testing.T) { + var projectId, deviceId *string + subCommand := "port" + consumerToken := "" + apiURL := "" + Version := "devel" + rootClient := root.NewClient(consumerToken, apiURL, Version) + tests := []struct { + name string + cmd *cobra.Command + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "vlan assignment port", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + projId, err := helper.CreateTestProject("metal-cli-test-ports-project") + if err != nil { + t.Error(err) + } + projectId = &projId + + devId, err := helper.CreateTestDevice(*projectId, "metal-cli-test-ports-device") + if err != nil { + t.Error(err) + } + deviceId = &devId + + device, err := helper.GetDeviceById(*deviceId) + + if len(device.NetworkPorts) < 3 { + t.Errorf("All 3 ports doesnot exist for the created device: %s", device.GetId()) + } + + port := &device.GetNetworkPorts()[2] + + vlan, err := helper.CreateTestVLAN(projId) + if err != nil { + t.Error(err) + } + + active, err := helper.IsDeviceStateActive(*deviceId) + if err == nil && active { + vxLanStr := strconv.Itoa(int(vlan.GetVxlan())) + // should be hybrid-bonded + root.SetArgs([]string{subCommand, "vlan", "-i", port.GetId(), "-a", vxLanStr}) + + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + + // wait for port to have vlans attached + if err := helper.WaitForAttachVlanToPort(port.GetId(), true); err != nil { + t.Error(err) + return + } + + if !strings.Contains(string(out[:]), port.GetId()) { + t.Errorf("cmd output should contain ID of the port: %s", port.GetId()) + } + + if !strings.Contains(string(out[:]), port.GetName()) { + t.Errorf("cmd output should contain name of the port: %s", port.GetName()) + } + + if !strings.Contains(string(out[:]), string(port.GetType())) { + t.Errorf("cmd output should contain type of the port: %s", string(port.GetType())) + } + + if !strings.Contains(string(out[:]), strconv.FormatBool(port.Data.GetBonded())) { + t.Errorf("cmd output should contain if port is bonded: %s", strconv.FormatBool(port.Data.GetBonded())) + } + + // cleanup + if err := helper.UnAssignPortVlan(port.GetId(), vlan.GetId()); err != nil { + t.Error(err) + return + } + } + + if err := helper.CleanupProjectAndDevice(deviceId, projectId); err != nil { + t.Error(err) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := rootClient.NewCommand() + rootCmd.AddCommand(tt.cmd) + tt.cmdFunc(t, tt.cmd) + }) + } +} diff --git a/test/helper/helper.go b/test/helper/helper.go index 0cb143dc..ddcea862 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -16,7 +16,6 @@ func TestClient() *openapiclient.APIClient { return apiClient } -// func Create_test_project(name string) string { func CreateTestProject(name string) (string, error) { TestApiClient := TestClient() @@ -24,7 +23,7 @@ func CreateTestProject(name string) (string, error) { projectResp, r, err := TestApiClient.ProjectsApi.CreateProject(context.Background()).ProjectCreateFromRootInput(projectCreateFromRootInput).Execute() if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `ProjectsApi.CreateProject``: %v\n", err) + fmt.Fprintf(os.Stderr, "Error when calling `ProjectsApi.CreateProject`: %v\n", err) fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) return "", err } @@ -37,7 +36,7 @@ func CreateTestDevice(projectId, name string) (string, error) { hostname := name metroDeviceRequest := openapiclient.CreateDeviceRequest{ DeviceCreateInMetroInput: &openapiclient.DeviceCreateInMetroInput{ - Metro: "da", + Metro: "sv", Plan: "m3.small.x86", OperatingSystem: "ubuntu_20_04", Hostname: &hostname, @@ -45,24 +44,72 @@ func CreateTestDevice(projectId, name string) (string, error) { } deviceResp, _, err := TestApiClient.DevicesApi.CreateDevice(context.Background(), projectId).CreateDeviceRequest(metroDeviceRequest).Execute() if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DevicesApi.CreateDevice``: %v\n", err) + fmt.Fprintf(os.Stderr, "Error when calling `DevicesApi.CreateDevice`: %v\n", err) return "", err } return deviceResp.GetId(), nil } -func IsDeviceStateActive(deviceId string) (bool, error) { +func CreateTestVLAN(projectId string) (*openapiclient.VirtualNetwork, error) { + TestApiClient := TestClient() + + metro := "sv" + vlanCreateInput := openapiclient.VirtualNetworkCreateInput{ + Metro: &metro, + } + vlan, _, err := TestApiClient.VLANsApi. + CreateVirtualNetwork(context.Background(), projectId). + VirtualNetworkCreateInput(vlanCreateInput). + Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `VLANsApi.CreateVirtualNetwork`: %v\n", err) + return nil, err + } + return vlan, nil +} + +func GetDeviceById(deviceId string) (*openapiclient.Device, error) { + TestApiClient := TestClient() + includes := []string{"network_ports"} + + device, _, err := TestApiClient.DevicesApi. + FindDeviceById(context.Background(), deviceId). + Include(includes). + Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DevicesApi.FindDeviceById`: %v\n", err) + return nil, err + } + + return device, nil +} + +func GetPortById(portId string) (*openapiclient.Port, error) { TestApiClient := TestClient() + includes := []string{"virtual_network"} + + port, _, err := TestApiClient.PortsApi. + FindPortById(context.Background(), portId). + Include(includes). + Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PortsApi.FindPortById`: %v\n", err) + return nil, err + } + + return port, nil +} + +func IsDeviceStateActive(deviceId string) (bool, error) { predefinedTime := 500 * time.Second // Adjust this as needed retryInterval := 10 * time.Second // Adjust this as needed startTime := time.Now() for time.Since(startTime) < predefinedTime { - resp, _, err := TestApiClient.DevicesApi.FindDeviceById(context.Background(), deviceId).Execute() + device, err := GetDeviceById(deviceId) if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DevicesApi.FindDeviceById``: %v\n", err) return false, err } - if resp.GetState() == "active" { + if device.GetState() == "active" { return true, nil } @@ -72,6 +119,36 @@ func IsDeviceStateActive(deviceId string) (bool, error) { return false, fmt.Errorf("timed out waiting for device %v to become active", deviceId) } +func WaitForAttachVlanToPort(portId string, attach bool) error { + tick := time.Tick(5 * time.Second) + timeout := 300 * time.Second + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("Timeout exceeded for vlan assignment with port ID: ", portId) + case <-tick: + port, err := GetPortById(portId) + if err != nil { + return err + } + + vlans := port.GetVirtualNetworks() + if attach { + if len(vlans) != 0 { + return nil + } + } else { + if len(vlans) == 0 { + return nil + } + } + } + } +} + func StopTestDevice(deviceId string) error { deviceActionInput := *openapiclient.NewDeviceActionInput("power_off") @@ -89,7 +166,10 @@ func CleanTestDevice(deviceId string) error { forceDelete := true // bool | Force the deletion of the device, by detaching any storage volume still active. (optional) TestApiClient := TestClient() - _, err := TestApiClient.DevicesApi.DeleteDevice(context.Background(), deviceId).ForceDelete(forceDelete).Execute() + _, err := TestApiClient.DevicesApi. + DeleteDevice(context.Background(), deviceId). + ForceDelete(forceDelete). + Execute() if err != nil { fmt.Fprintf(os.Stderr, "Error when calling `DevicesApi.DeleteDevice``: %v\n", err) return err @@ -99,7 +179,9 @@ func CleanTestDevice(deviceId string) error { func CleanTestProject(projectId string) error { TestApiClient := TestClient() - r, err := TestApiClient.ProjectsApi.DeleteProject(context.Background(), projectId).Execute() + r, err := TestApiClient.ProjectsApi. + DeleteProject(context.Background(), projectId). + Execute() if err != nil { fmt.Fprintf(os.Stderr, "Error when calling `ProjectsApi.DeleteProject``: %v\n", err) fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) @@ -166,5 +248,30 @@ func CleanTestVlan(vlanId string) error { fmt.Fprintf(os.Stderr, "Error when calling `VLANsApi.DeleteVirtualNetwork``: %v\n", err) return err } + + return nil +} +func UnAssignPortVlan(portId, vlanId string) error { + testClient := TestClient() + _, _, err := testClient.PortsApi. + UnassignPort(context.Background(), portId). + PortAssignInput(openapiclient.PortAssignInput{Vnid: &vlanId}). + Execute() + return err +} + +func CleanupProjectAndDevice(deviceId, projectId *string) error { + resp, err := IsDeviceStateActive(*deviceId) + if err == nil && resp == true { + err = CleanTestDevice(*deviceId) + if err != nil { + return err + } + err = CleanTestProject(*projectId) + if err != nil { + return err + } + } + return nil }