diff --git a/docs/metal_gateway_get.md b/docs/metal_gateway_get.md index 11bb1f19..edbf6305 100644 --- a/docs/metal_gateway_get.md +++ b/docs/metal_gateway_get.md @@ -15,7 +15,7 @@ metal gateway get -p [flags] ``` # Lists Metal Gateways for project 3b0795ba-ec9a-4a9e-83a7-043e7e11407c: - metal virtual-network get -p 3b0795ba-ec9a-4a9e-83a7-043e7e11407c + metal gateways get -p 3b0795ba-ec9a-4a9e-83a7-043e7e11407c ``` ### Options diff --git a/docs/metal_interconnections_create.md b/docs/metal_interconnections_create.md index 9b72a288..f879d860 100644 --- a/docs/metal_interconnections_create.md +++ b/docs/metal_interconnections_create.md @@ -24,14 +24,17 @@ metal interconnections create -n [-m ] [-r ] [-t [flags] @@ -15,11 +15,15 @@ metal interconnections delete -i [flags] ``` # Deletes the specified interconnection: metal interconnections delete -i 7ec86e23-8dcf-48ed-bd9b-c25c20958277 + > + ✔ Are you sure you want to delete device 7ec86e23-8dcf-48ed-bd9b-c25c20958277 [Y/n]: Y + ``` ### Options ``` + -f, --force Skips confirmation for the interconnection deletion. -h, --help help for delete -i, --id string The UUID of the interconnection. ``` diff --git a/internal/gateway/retrieve.go b/internal/gateway/retrieve.go index b3537c04..6a9a0bdf 100644 --- a/internal/gateway/retrieve.go +++ b/internal/gateway/retrieve.go @@ -41,7 +41,7 @@ func (c *Client) Retrieve() *cobra.Command { Long: "Retrieves a list of all VLANs for the specified project.", Example: ` # Lists Metal Gateways for project 3b0795ba-ec9a-4a9e-83a7-043e7e11407c: - metal virtual-network get -p 3b0795ba-ec9a-4a9e-83a7-043e7e11407c`, + metal gateways get -p 3b0795ba-ec9a-4a9e-83a7-043e7e11407c`, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true @@ -80,6 +80,7 @@ func (c *Client) Retrieve() *cobra.Command { return c.Out.Output(metalGways, header, &data) }, } + retrieveMetalGatewaysCmd.Flags().StringVarP(&projectID, "project-id", "p", "", "The project's UUID. This flag is required, unless specified in the config created by metal init or set as METAL_PROJECT_ID environment variable.") _ = retrieveMetalGatewaysCmd.MarkFlagRequired("project-id") diff --git a/internal/interconnections/create.go b/internal/interconnections/create.go index f745fd3e..cfd54254 100644 --- a/internal/interconnections/create.go +++ b/internal/interconnections/create.go @@ -2,6 +2,7 @@ package interconnections import ( "context" + "errors" "fmt" metal "github.com/equinix/equinix-sdk-go/services/metalv1" @@ -9,8 +10,10 @@ import ( ) func (c *Client) Create() *cobra.Command { - var name, metro, redundancy, connType, projectID, organizationID string + var name, metro, redundancy, connType, projectID, organizationID, svcTokenType string var vrfs []string + var vlans []int32 + var speed int32 createInterconnectionsCmd := &cobra.Command{ Use: `create -n [-m ] [-r ] [-t ] [-p ] | [-O ]`, @@ -25,24 +28,47 @@ func (c *Client) Create() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true + var interconn *metal.Interconnection var err error - createOrganizationInterconnectionRequest := metal.CreateOrganizationInterconnectionRequest{DedicatedPortCreateInput: metal.NewDedicatedPortCreateInput(metro, name, redundancy, metal.DedicatedPortCreateInputType(connType))} - if projectID != "" { - - interconn, _, err = c.Service.CreateProjectInterconnection(context.Background(), projectID).CreateOrganizationInterconnectionRequest(createOrganizationInterconnectionRequest).Execute() - if err != nil { - return fmt.Errorf("could not create interconnections: %w", err) - - } - } else if organizationID != "" { - interconn, _, err = c.Service.CreateOrganizationInterconnection(context.Background(), organizationID).CreateOrganizationInterconnectionRequest(createOrganizationInterconnectionRequest).Execute() - if err != nil { - return fmt.Errorf("could not create interconnections: %w", err) - } - } else { - return fmt.Errorf("Could you provide at least either of projectID OR organizationID") + if err := validInputArgs(projectID, organizationID, connType, vlans, vrfs, svcTokenType); err != nil { + return err + } + + createOrganizationInterconnectionRequest := metal.CreateOrganizationInterconnectionRequest{} + + switch { + case vlanFabricVcCreate(connType, vlans): + in := metal.NewVlanFabricVcCreateInput( + metro, name, redundancy, metal.VlanFabricVcCreateInputServiceTokenType(svcTokenType), + metal.VlanFabricVcCreateInputType(connType), + ) + in.Vlans = vlans + // default speed + in.SetSpeed(speed) + + createOrganizationInterconnectionRequest. + VlanFabricVcCreateInput = in + case vrfsFabricVcCreate(connType, vrfs): + createOrganizationInterconnectionRequest. + VrfFabricVcCreateInput = metal.NewVrfFabricVcCreateInput( + metro, name, redundancy, metal.VlanFabricVcCreateInputServiceTokenType(svcTokenType), + metal.VlanFabricVcCreateInputType(connType), vrfs, + ) + default: + createOrganizationInterconnectionRequest. + DedicatedPortCreateInput = metal.NewDedicatedPortCreateInput( + metro, name, redundancy, metal.DedicatedPortCreateInputType(connType), + ) + } + + interconn, err = c.handleCreate( + projectID, + organizationID, + createOrganizationInterconnectionRequest) + if err != nil { + return fmt.Errorf("could not create interconnections: %w", err) } data := make([][]string, 1) @@ -54,14 +80,16 @@ func (c *Client) Create() *cobra.Command { }, } - createInterconnectionsCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the interconnection") - createInterconnectionsCmd.Flags().StringVarP(&metro, "metro", "m", "", "metro in the interconnection") - createInterconnectionsCmd.Flags().StringVarP(&redundancy, "redundancy", "r", "", "Website URL of the organization.") - createInterconnectionsCmd.Flags().StringVarP(&connType, "type", "t", "", "type of of interconnection.") - // createInterconnectionsCmd.Flags().StringVarP(&connType, "serviceTokentype", "T", "", "service token type for interconnection either fabric OR Metal builds") - createInterconnectionsCmd.Flags().StringSliceVarP(&vrfs, "vrfs", "v", []string{}, "Return only the specified vrfs.") - createInterconnectionsCmd.Flags().StringVarP(&projectID, "projectID", "p", "", "project ID") - createInterconnectionsCmd.Flags().StringVarP(&organizationID, "organizationID", "O", "", "Org ID") + createInterconnectionsCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the interconnection.") + createInterconnectionsCmd.Flags().StringVarP(&metro, "metro", "m", "", "Metro Id or Metro Code from where the interconnection will be originated.") + createInterconnectionsCmd.Flags().StringVarP(&redundancy, "redundancy", "r", "", "Types of redundancy for the interconnection. Either 'primary' or 'redundant'.") + createInterconnectionsCmd.Flags().StringVarP(&connType, "type", "t", "", "Type of of interconnection. Either 'dedicated' or 'shared' when requesting for a Fabric VC.") + createInterconnectionsCmd.Flags().StringSliceVar(&vrfs, "vrf", []string{}, "A list of VRFs to attach to the Interconnection. Ex: --vrfs uuid1, uuid2 .") + createInterconnectionsCmd.Flags().StringVarP(&projectID, "project-id", "p", "", "The project's UUID. Either one of this flag or --organization-id is required.") + createInterconnectionsCmd.Flags().StringVar(&organizationID, "organization-id", "", "The Organization's UUID to be used for creating org level interconnection request. Either one of this flag or --project-id is required.") + createInterconnectionsCmd.Flags().Int32SliceVar(&vlans, "vlan", []int32{}, "A list of VLANs to attach to the Interconnection. Ex: --vlans 1000, 1001 .") + createInterconnectionsCmd.Flags().StringVarP(&svcTokenType, "service-token-type", "T", "", "Type of service token for shared connection. Enum: 'a_side', 'z_side'.") + createInterconnectionsCmd.Flags().Int32VarP(&speed, "speed", "s", int32(1000000000), "The maximum speed of the interconnections.") _ = createInterconnectionsCmd.MarkFlagRequired("name") _ = createInterconnectionsCmd.MarkFlagRequired("metro") @@ -69,3 +97,45 @@ func (c *Client) Create() *cobra.Command { _ = createInterconnectionsCmd.MarkFlagRequired("type") return createInterconnectionsCmd } + +func vlanFabricVcCreate(connType string, vlans []int32) bool { + return connType == "shared" && len(vlans) > 0 +} + +func vrfsFabricVcCreate(connType string, vrfs []string) bool { + return connType == "shared" && len(vrfs) > 0 +} + +func (c *Client) handleCreate(projectID, organizationID string, + req metal.CreateOrganizationInterconnectionRequest) (*metal.Interconnection, error) { + + if projectID != "" { + interconn, _, err := c.Service. + CreateProjectInterconnection(context.Background(), projectID). + CreateOrganizationInterconnectionRequest(req). + Execute() + return interconn, err + } + + interconn, _, err := c.Service. + CreateOrganizationInterconnection(context.Background(), organizationID). + CreateOrganizationInterconnectionRequest(req). + Execute() + return interconn, err +} + +func validInputArgs(projectID, organizationID, connType string, vlans []int32, vrfs []string, svcTokenType string) error { + if projectID == "" && organizationID == "" { + return errors.New("could you provide at least either of projectID OR organizationID") + } + + if (vlanFabricVcCreate(connType, vlans) || vrfsFabricVcCreate(connType, vrfs)) && svcTokenType == "" { + return errors.New("flag 'service-token-type' is required for vlan or vrfs fabric VC create") + } + + if vlanFabricVcCreate(connType, vlans) && vrfsFabricVcCreate(connType, vrfs) { + return errors.New("vlans and vrfs both are provided. Please provide any one type of interconnection") + } + + return nil +} diff --git a/internal/interconnections/delete.go b/internal/interconnections/delete.go index 2a59a9c1..4b9cda1e 100644 --- a/internal/interconnections/delete.go +++ b/internal/interconnections/delete.go @@ -3,23 +3,44 @@ package interconnections import ( "context" "fmt" + "strings" "github.com/spf13/cobra" ) func (c *Client) Delete() *cobra.Command { - var connectionID string + var connectionID, confirmation string + var force bool deleteConnectionCmd := &cobra.Command{ Use: `delete -i `, Short: "Deletes a interconnection.", - Long: "Deletes the specified interconnection.", + Long: "Deletes the specified interconnection. Use --force to skip confirmation", Example: ` # Deletes the specified interconnection: - metal interconnections delete -i 7ec86e23-8dcf-48ed-bd9b-c25c20958277`, + metal interconnections delete -i 7ec86e23-8dcf-48ed-bd9b-c25c20958277 + > + ✔ Are you sure you want to delete device 7ec86e23-8dcf-48ed-bd9b-c25c20958277 [Y/n]: Y +`, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true + if !force { + fmt.Printf("Are you sure you want to delete interconnection %s [Y/n]: ", connectionID) + + _, err := fmt.Scanln(&confirmation) + if err != nil { + fmt.Println("Error reading confirmation:", err) + return nil + } + + confirmation = strings.TrimSpace(strings.ToLower(confirmation)) + if confirmation != "yes" && confirmation != "y" { + fmt.Println("Interconnection deletion cancelled.") + return nil + } + } + _, _, err := c.Service.DeleteInterconnection(context.Background(), connectionID).Execute() if err != nil { return err @@ -31,6 +52,7 @@ func (c *Client) Delete() *cobra.Command { } deleteConnectionCmd.Flags().StringVarP(&connectionID, "id", "i", "", "The UUID of the interconnection.") + deleteConnectionCmd.Flags().BoolVarP(&force, "force", "f", false, "Skips confirmation for the interconnection deletion.") _ = deleteConnectionCmd.MarkFlagRequired("id") return deleteConnectionCmd diff --git a/test/e2e/interconnections/create_test.go b/test/e2e/interconnections/create_test.go new file mode 100644 index 00000000..d7beb34a --- /dev/null +++ b/test/e2e/interconnections/create_test.go @@ -0,0 +1,98 @@ +package interconnections + +import ( + "context" + "fmt" + "strings" + "testing" + + root "github.com/equinix/metal-cli/internal/cli" + "github.com/equinix/metal-cli/internal/interconnections" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/test/helper" + + metal "github.com/equinix/equinix-sdk-go/services/metalv1" + "github.com/spf13/cobra" +) + +func TestInterconnections_Create(t *testing.T) { + subCommand := "interconnections" + rootClient := root.NewClient(helper.ConsumerToken, helper.URL, helper.Version) + randomString := helper.GenerateRandomString(5) + + project := helper.CreateTestProject(t, "metal-cli-interconnections-create-"+randomString) + vlan := helper.CreateTestVLAN(t, project.GetId()) + + apiClient := helper.TestClient() + + tests := []struct { + name string + cmd *cobra.Command + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "create shared vlan interconnection", + cmd: interconnections.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + connName := "conn-1-" + randomString + + root.SetArgs([]string{subCommand, "create", "-p", project.GetId(), "--vlan", fmt.Sprintf("%d", vlan.GetVxlan()), "-n", connName, "-m", vlan.GetMetroCode(), "-r", "primary", "-t", "shared", "-T", "a_side", "-s", "50000000"}) + + out := helper.ExecuteAndCaptureOutput(t, root) + + conns, err := apiClient.InterconnectionsApi. + ProjectListInterconnections(context.Background(), project.GetId()). + ExecuteWithPagination() + if err != nil { + t.Fatal(err) + } + if len(conns.GetInterconnections()) < 1 { + t.Fatal("Interconnections Not Found. Failed to create Interconnections") + } + + var conn *metal.Interconnection + for index, c := range conns.GetInterconnections() { + if c.GetName() == connName { + conn = &conns.GetInterconnections()[index] + break + } + } + + t.Cleanup(func() { + helper.CleanupInterconnection(t, conn.GetId()) + }) + + assertInterconnectionsCmdOutput(t, string(out[:]), conn) + }, + }, + } + + 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 assertInterconnectionsCmdOutput(t *testing.T, out string, conn *metal.Interconnection) { + if !strings.Contains(out, conn.GetId()) { + t.Errorf("cmd output should contain ID of the Interconnection: [%s] \n output:\n%s", conn.GetId(), out) + } + + if !strings.Contains(out, string(conn.GetType())) { + t.Errorf("cmd output should contain type of interconection: [%s] \n output:\n%s", conn.GetType(), out) + } + + if !strings.Contains(out, conn.GetName()) { + t.Errorf("cmd output should contain name of Interconnection: [%s] \n output:\n%s", conn.GetName(), out) + } + + if !strings.Contains(out, conn.GetCreatedAt().String()) { + t.Errorf("cmd output should contain creation time of the Interconnection, expected time:%s, output:\n%s", conn.GetCreatedAt().String(), out) + } +} diff --git a/test/helper/helper.go b/test/helper/helper.go index 82866930..b72cd49b 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -10,11 +10,10 @@ import ( "testing" "time" - "golang.org/x/exp/rand" - "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/exp/rand" ) const ( @@ -552,3 +551,73 @@ func ExecuteAndCaptureOutput(t *testing.T, root *cobra.Command) []byte { return out } + +func CleanupInterconnectionVC(t *testing.T, connectionId string) { + t.Helper() + apiClient := TestClient() + + vcList, resp, err := apiClient.InterconnectionsApi. + ListInterconnectionVirtualCircuits(context.Background(), connectionId). + Execute() + if err != nil && resp.StatusCode != http.StatusNotFound { + t.Fatalf("Error when calling `InterconnectionsApi.ListInterconnectionVirtualCircuits`` for %v: %v\n", connectionId, err) + } + + if vcList != nil && vcList.HasVirtualCircuits() { + for _, vc := range vcList.GetVirtualCircuits() { + _, resp, err := apiClient.InterconnectionsApi. + DeleteVirtualCircuit(context.Background(), vc.VlanVirtualCircuit.GetId()). + Execute() + if err != nil && resp.StatusCode != http.StatusNotFound { + t.Fatalf("Error when calling `InterconnectionsApi.DeleteVirtualCircuit`` for %v: %v\n", connectionId, err) + } + } + } +} + +func CleanupInterconnection(t *testing.T, connectionId string) { + t.Helper() + apiClient := TestClient() + + _, resp, err := apiClient.InterconnectionsApi. + DeleteInterconnection(context.Background(), connectionId). + Execute() + if err != nil && resp.StatusCode != http.StatusNotFound { + t.Fatalf("Error when calling `InterconnectionsApi.DeleteInterconnection`` for %v: %v\n", connectionId, err) + } + + if err := waitForInterconnectionDeleted(apiClient, connectionId, 5*time.Minute); err != nil { + t.Fatal(err) + } + + CleanupInterconnectionVC(t, connectionId) +} + +func waitForInterconnectionDeleted(apiClient *metalv1.APIClient, connId string, timeout time.Duration) error { + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return errors.New("Timeout while waiting for connection to be deleted") + case <-ticker.C: + conn, _, err := apiClient.InterconnectionsApi.GetInterconnection(context.Background(), connId).Execute() + if err != nil { + if strings.Contains(err.Error(), "Not Found") { + return nil + } + return err + } + + if conn == nil { + return nil + } + + fmt.Printf("Connection not deleted. Current status: [%s]", conn.GetStatus()) + } + } +}