From 0f1a7386ec55fed3224ab07e22fc32ba957bf3aa Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Wed, 27 Sep 2023 20:51:22 +0530 Subject: [PATCH] Migrate metal ports subcommand cli to metal-go from packngo Signed-off-by: Ayush Rangwala --- internal/ports/convert.go | 95 ++++++++++------- internal/ports/port.go | 15 +-- internal/ports/retrieve.go | 11 +- internal/ports/vlans.go | 40 +++++--- test/e2e/ports/convert_test.go | 175 ++++++++++++++++++++++++++++++++ test/e2e/ports/retrieve_test.go | 77 ++++++++++++++ test/e2e/ports/vlans_test.go | 91 +++++++++++++++++ test/helper/helper.go | 134 ++++++++++++++++++++++-- 8 files changed, 564 insertions(+), 74 deletions(-) create mode 100644 test/e2e/ports/convert_test.go create mode 100644 test/e2e/ports/retrieve_test.go create mode 100644 test/e2e/ports/vlans_test.go diff --git a/internal/ports/convert.go b/internal/ports/convert.go index 5e603db9..a6cb7d38 100644 --- a/internal/ports/convert.go +++ b/internal/ports/convert.go @@ -21,20 +21,21 @@ package ports import ( + "context" "fmt" "log" + "net/http" "strconv" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/manifoldco/promptui" - "github.com/packethost/packngo" "github.com/spf13/cobra" ) 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.", @@ -54,25 +55,13 @@ func (c *Client) Convert() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - if f := cmd.Flag("bonded"); f.Changed { - _, _, err := map[bool]func(string, bool) (*packngo.Port, *packngo.Response, error){ - true: c.PortService.Bond, - false: c.PortService.Disbond, - }[bonded](portID, bulk) - if err != nil { + if cmd.Flag("bonded").Changed { + if err := portBondingHandler(bonded, c, portID); err != nil { return fmt.Errorf("failed to change port bonding: %w", err) } } - addrs := []packngo.AddressRequest{{AddressFamily: 4, Public: false}} - if f := cmd.Flag("public-ipv4"); f.Changed { - addrs = append(addrs, packngo.AddressRequest{AddressFamily: 4, Public: true}) - } - if f := cmd.Flag("public-ipv6"); f.Changed { - addrs = append(addrs, packngo.AddressRequest{AddressFamily: 6, Public: true}) - } - - convToL2 := func(portID string) (*packngo.Port, *packngo.Response, error) { + convToL2 := func(portID string) (*metal.Port, *http.Response, error) { if !force { prompt := promptui.Prompt{ Label: fmt.Sprintf("Are you sure you want to convert Port %s to Layer2 and remove assigned IP addresses: ", portID), @@ -84,14 +73,38 @@ func (c *Client) Convert() *cobra.Command { return nil, nil, nil } } - return c.PortService.ConvertToLayerTwo(portID) + + return c.PortService.ConvertLayer2(context.Background(), portID). + PortAssignInput(*metal.NewPortAssignInput()). + Execute() + } + + addressFamily := int32(metal.IPADDRESSADDRESSFAMILY__4) + public := false + addrs := []metal.PortConvertLayer3InputRequestIpsInner{{AddressFamily: &addressFamily, Public: &public}} + + if f := cmd.Flag("public-ipv4"); f.Changed { + addressFamily = int32(metal.IPADDRESSADDRESSFAMILY__4) + public = true + addrs = append(addrs, metal.PortConvertLayer3InputRequestIpsInner{AddressFamily: &addressFamily, Public: &public}) } - convToL3 := func(portID string) (*packngo.Port, *packngo.Response, error) { + if f := cmd.Flag("public-ipv6"); f.Changed { + addressFamily = int32(metal.IPADDRESSADDRESSFAMILY__6) + public = true + addrs = append(addrs, metal.PortConvertLayer3InputRequestIpsInner{AddressFamily: &addressFamily, Public: &public}) + } + + convToL3 := func(portID string) (*metal.Port, *http.Response, error) { log.Printf("Converting port %s to layer-3 with addresses %v", portID, addrs) - return c.PortService.ConvertToLayerThree(portID, addrs) + return c.PortService. + ConvertLayer3(context.Background(), portID). + PortConvertLayer3Input(metal.PortConvertLayer3Input{ + RequestIps: addrs, + }). + Execute() } if f := cmd.Flag("layer2"); f.Changed { - _, _, err := map[bool]func(string) (*packngo.Port, *packngo.Response, error){ + _, _, err := map[bool]func(string) (*metal.Port, *http.Response, error){ true: convToL2, false: convToL3, }[layer2](portID) @@ -99,30 +112,44 @@ func (c *Client) Convert() *cobra.Command { return fmt.Errorf("failed to change port network mode: %w", err) } } - listOpts := c.Servicer.ListOptions(nil, nil) - getOpts := &packngo.GetOptions{Includes: listOpts.Includes, Excludes: listOpts.Excludes} - port, _, err := c.PortService.Get(portID, getOpts) + port, _, err := c.PortService.FindPortById(context.Background(), portID). + Include(c.Servicer.Includes(nil)). + Execute() if err != nil { return fmt.Errorf("Could not get Port: %w", err) } data := make([][]string, 1) - data[0] = []string{port.ID, port.Name, port.Type, port.NetworkType, port.Data.MAC, strconv.FormatBool(port.Data.Bonded)} + data[0] = []string{port.GetId(), port.GetName(), string(port.GetType()), string(port.GetNetworkType()), port.Data.GetMac(), strconv.FormatBool(port.Data.GetBonded())} header := []string{"ID", "Name", "Type", "Network Type", "MAC", "Bonded"} return c.Out.Output(port, header, &data) }, } - 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 convertPortCmd +} + +func portBondingHandler(bonded bool, c *Client, portId string) error { + if bonded { + _, _, err := c.PortService.BondPort(context.Background(), portId). + Include(c.Servicer.Includes(nil)). + Execute() + return err + } - return retrievePortCmd + _, _, err := c.PortService.DisbondPort(context.Background(), portId). + Include(c.Servicer.Includes(nil)). + Execute() + return err } diff --git a/internal/ports/port.go b/internal/ports/port.go index cab7b46f..2b246b5b 100644 --- a/internal/ports/port.go +++ b/internal/ports/port.go @@ -21,15 +21,15 @@ package ports import ( + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/equinix/metal-cli/internal/outputs" - "github.com/packethost/packngo" "github.com/spf13/cobra" ) type Client struct { Servicer Servicer - PortService packngo.PortService - VLANService packngo.VLANAssignmentService + PortService *metal.PortsApiService + VLANService *metal.VLANsApiService Out outputs.Outputer } @@ -45,8 +45,8 @@ func (c *Client) NewCommand() *cobra.Command { root.PersistentPreRun(cmd, args) } } - c.PortService = c.Servicer.API(cmd).Ports - c.VLANService = c.Servicer.API(cmd).VLANAssignments + c.PortService = c.Servicer.MetalAPI(cmd).PortsApi + c.VLANService = c.Servicer.MetalAPI(cmd).VLANsApi }, } @@ -59,8 +59,9 @@ func (c *Client) NewCommand() *cobra.Command { } type Servicer interface { - API(*cobra.Command) *packngo.Client - ListOptions(defaultIncludes, defaultExcludes []string) *packngo.ListOptions + MetalAPI(*cobra.Command) *metal.APIClient + Filters() map[string]string + Includes(defaultIncludes []string) (incl []string) } func NewClient(s Servicer, out outputs.Outputer) *Client { diff --git a/internal/ports/retrieve.go b/internal/ports/retrieve.go index 89f8c36e..248e0251 100644 --- a/internal/ports/retrieve.go +++ b/internal/ports/retrieve.go @@ -21,10 +21,10 @@ package ports import ( + "context" "fmt" "strconv" - "github.com/packethost/packngo" "github.com/spf13/cobra" ) @@ -41,17 +41,16 @@ func (c *Client) Retrieve() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - listOpts := c.Servicer.ListOptions(nil, nil) - - getOpts := &packngo.GetOptions{Includes: listOpts.Includes, Excludes: listOpts.Excludes} - port, _, err := c.PortService.Get(portID, getOpts) + port, _, err := c.PortService.FindPortById(context.Background(), portID). + Include(c.Servicer.Includes(nil)). + Execute() if err != nil { return fmt.Errorf("Could not get Port: %w", err) } data := make([][]string, 1) - data[0] = []string{port.ID, port.Name, port.Type, port.NetworkType, port.Data.MAC, strconv.FormatBool(port.Data.Bonded)} + data[0] = []string{port.GetId(), port.GetName(), string(port.GetType()), string(port.GetNetworkType()), port.Data.GetMac(), strconv.FormatBool(port.Data.GetBonded())} header := []string{"ID", "Name", "Type", "Network Type", "MAC", "Bonded"} return c.Out.Output(port, header, &data) diff --git a/internal/ports/vlans.go b/internal/ports/vlans.go index 9ad6b588..88bdd49a 100644 --- a/internal/ports/vlans.go +++ b/internal/ports/vlans.go @@ -21,11 +21,12 @@ package ports import ( + "context" "errors" "fmt" "strconv" - "github.com/packethost/packngo" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -50,38 +51,45 @@ func (c *Client) Vlans() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - listOpts := c.Servicer.ListOptions([]string{"port"}, nil) + req := metal.NewPortVlanAssignmentBatchCreateInput() - getOpts := &packngo.GetOptions{Includes: listOpts.Includes, Excludes: listOpts.Excludes} - req := &packngo.VLANAssignmentBatchCreateRequest{} - f := false - t := true for _, vlan := range assignments { - assignment := packngo.VLANAssignmentCreateRequest{VLAN: vlan, State: packngo.VLANAssignmentAssigned, Native: &f} - req.VLANAssignments = append(req.VLANAssignments, assignment) + CreateInputVlanAssignmentsInner := metal.PortVlanAssignmentBatchCreateInputVlanAssignmentsInner{} + CreateInputVlanAssignmentsInner.SetVlan(vlan) + CreateInputVlanAssignmentsInner.SetState(metal.PORTVLANASSIGNMENTBATCHVLANASSIGNMENTSINNERSTATE_ASSIGNED) + req.SetVlanAssignments(append(req.GetVlanAssignments(), CreateInputVlanAssignmentsInner)) } for _, vlan := range unassignments { - assignment := packngo.VLANAssignmentCreateRequest{VLAN: vlan, State: packngo.VLANAssignmentUnassigned} - req.VLANAssignments = append(req.VLANAssignments, assignment) + CreateInputVlanAssignmentsInner := metal.PortVlanAssignmentBatchCreateInputVlanAssignmentsInner{} + CreateInputVlanAssignmentsInner.SetVlan(vlan) + CreateInputVlanAssignmentsInner.SetState(metal.PORTVLANASSIGNMENTBATCHVLANASSIGNMENTSINNERSTATE_UNASSIGNED) + req.SetVlanAssignments(append(req.GetVlanAssignments(), CreateInputVlanAssignmentsInner)) } if native != "" { - assignment := packngo.VLANAssignmentCreateRequest{VLAN: native, State: packngo.VLANAssignmentAssigned, Native: &t} - req.VLANAssignments = append(req.VLANAssignments, assignment) + CreateInputVlanAssignmentsInner := metal.PortVlanAssignmentBatchCreateInputVlanAssignmentsInner{} + CreateInputVlanAssignmentsInner.SetVlan(native) + CreateInputVlanAssignmentsInner.SetState(metal.PORTVLANASSIGNMENTBATCHVLANASSIGNMENTSINNERSTATE_ASSIGNED) + CreateInputVlanAssignmentsInner.SetNative(true) + req.SetVlanAssignments(append(req.GetVlanAssignments(), CreateInputVlanAssignmentsInner)) } - if len(req.VLANAssignments) == 0 { + + if len(req.GetVlanAssignments()) == 0 { return errors.New("no VLAN assignments specified") } - batch, _, err := c.VLANService.CreateBatch(portID, req, getOpts) + batch, _, err := c.PortService.CreatePortVlanAssignmentBatch(context.Background(), portID). + PortVlanAssignmentBatchCreateInput(*req). + Include(c.Servicer.Includes([]string{"port"})). + Execute() if err != nil { return fmt.Errorf("Could not update port VLAN assignments: %w", err) } // TODO: should we return the batch? - port := batch.Port + port := batch.GetPort() data := make([][]string, 1) - data[0] = []string{port.ID, port.Name, port.Type, port.NetworkType, port.Data.MAC, strconv.FormatBool(port.Data.Bonded)} + data[0] = []string{port.GetId(), port.GetName(), string(port.GetType()), string(port.GetNetworkType()), port.Data.GetMac(), strconv.FormatBool(port.Data.GetBonded())} header := []string{"ID", "Name", "Type", "Network Type", "MAC", "Bonded"} return c.Out.Output(port, header, &data) diff --git a/test/e2e/ports/convert_test.go b/test/e2e/ports/convert_test.go new file mode 100644 index 00000000..d5293889 --- /dev/null +++ b/test/e2e/ports/convert_test.go @@ -0,0 +1,175 @@ +package ports + +import ( + "io" + "os" + "strconv" + "strings" + "testing" + + "github.com/equinix/metal-cli/internal/ports" + + metal "github.com/equinix-labs/metal-go/metal/v1" + + root "github.com/equinix/metal-cli/internal/cli" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "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) + + portList := setupProjectAndDevice(t, &projectId, &deviceId) + port := &portList[2] + defer func() { + if err := helper.CleanupProjectAndDevice(deviceId, projectId); err != nil { + t.Error(err) + } + }() + if port == nil { + t.Error("bond0 Port not found on device") + return + } + + tests := []struct { + name string + cmd *cobra.Command + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + expectedNetworkType string + expectedBonded bool + }{ + { + name: "convert port layer-2 bonded false", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + + root.SetArgs([]string{subCommand, "convert", "-i", port.GetId(), "--layer2", "--bonded=false", "--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 + + assertPortCmdOutput(t, port, string(out[:]), "layer2-individual", false) + }, + }, + { + name: "convert port layer-2 bonded true", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + + 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 + + assertPortCmdOutput(t, port, string(out[:]), "layer2-bonded", true) + }, + }, + { + name: "convert port layer-3 bonded true", + cmd: ports.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + + root.SetArgs([]string{subCommand, "convert", "-i", port.GetId(), "-2=false", "--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 + + assertPortCmdOutput(t, port, string(out[:]), "layer3", true) + }, + }, + } + + 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 setupProjectAndDevice(t *testing.T, projectId, deviceId *string) []metal.Port { + 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 + + active, err := helper.IsDeviceStateActive(*deviceId) + if err != nil { + t.Error(err) + } + if !active { + t.Errorf("Timeout while waiting for device: %s to be active", *deviceId) + } + + device, err := helper.GetDeviceById(*deviceId) + if err != nil { + t.Error(err) + return nil + } + if len(device.NetworkPorts) < 3 { + t.Errorf("All 3 ports doesnot exist for the created device: %s", device.GetId()) + } + + return device.GetNetworkPorts() +} + +func assertPortCmdOutput(t *testing.T, port *metal.Port, out, networkType string, bonded bool) { + if !strings.Contains(out, port.GetId()) { + t.Errorf("cmd output should contain ID of the port: %s", port.GetId()) + } + + if !strings.Contains(out, port.GetName()) { + t.Errorf("cmd output should contain name of the port: %s", port.GetName()) + } + + if !strings.Contains(out, networkType) { + t.Errorf("cmd output should contain type of the port: %s", string(port.GetNetworkType())) + } + + if !strings.Contains(out, strconv.FormatBool(bonded)) { + t.Errorf("cmd output should contain if port is bonded: %s", strconv.FormatBool(port.Data.GetBonded())) + } +} diff --git a/test/e2e/ports/retrieve_test.go b/test/e2e/ports/retrieve_test.go new file mode 100644 index 00000000..771e9cd5 --- /dev/null +++ b/test/e2e/ports/retrieve_test.go @@ -0,0 +1,77 @@ +package ports + +import ( + "io" + "os" + "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) + + portList := setupProjectAndDevice(t, &projectId, &deviceId) + port := &portList[2] + defer func() { + if err := helper.CleanupProjectAndDevice(deviceId, projectId); err != nil { + t.Error(err) + } + }() + if port == nil { + t.Error("bond0 Port not found on device") + return + } + + 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() + 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.Data.GetMac()) { + t.Errorf("cmd output should contain MAC address of the port: %s", port.Data.GetMac()) + } + + assertPortCmdOutput(t, port, string(out[:]), string(port.GetNetworkType()), port.Data.GetBonded()) + }, + }, + } + + 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_test.go b/test/e2e/ports/vlans_test.go new file mode 100644 index 00000000..b63fe614 --- /dev/null +++ b/test/e2e/ports/vlans_test.go @@ -0,0 +1,91 @@ +package ports + +import ( + "io" + "os" + "strconv" + "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) + + portList := setupProjectAndDevice(t, &projectId, &deviceId) + port := &portList[2] + vlan, err := helper.CreateTestVLAN(projectId) + if err != nil { + t.Error(err) + } + + defer func() { + 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) + } + }() + if port == nil { + t.Error("bond0 Port not found on device") + return + } + + 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() + + 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 + } + + assertPortCmdOutput(t, port, string(out[:]), "hybrid-bonded", true) + }, + }, + } + + 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..88081bac 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,32 +36,83 @@ 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, }, } - deviceResp, _, err := TestApiClient.DevicesApi.CreateDevice(context.Background(), projectId).CreateDeviceRequest(metroDeviceRequest).Execute() + 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 +122,38 @@ 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 { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + 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: %s", portId) + case <-ticker.C: + 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 +171,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 +184,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 +253,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 { + err = CleanTestDevice(deviceId) + if err != nil { + return err + } + err = CleanTestProject(projectId) + if err != nil { + return err + } + } + return nil }