Skip to content

Commit

Permalink
feat: Metal CLI support create shared interconnection with vlans and …
Browse files Browse the repository at this point in the history
…vrfs (#397)

Part of #307

---------

Signed-off-by: Ayush Rangwala <[email protected]>
  • Loading branch information
aayushrangwala authored Jan 23, 2024
1 parent 35b7312 commit 2bb181a
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docs/metal_gateway_get.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ metal gateway get -p <project_UUID> [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
Expand Down
19 changes: 11 additions & 8 deletions docs/metal_interconnections_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ metal interconnections create -n <name> [-m <metro>] [-r <redundancy> ] [-t <typ
### Options

```
-h, --help help for create
-m, --metro string metro in the interconnection
-n, --name string Name of the interconnection
-O, --organizationID string Org ID
-p, --projectID string project ID
-r, --redundancy string Website URL of the organization.
-t, --type string type of of interconnection.
-v, --vrfs strings Return only the specified vrfs.
-h, --help help for create
-m, --metro string Metro Id or Metro Code from where the interconnection will be originated.
-n, --name string Name of the interconnection.
--organization-id string The Organization's UUID to be used for creating org level interconnection request. Either one of this flag or --project-id is required.
-p, --project-id string The project's UUID. Either one of this flag or --organization-id is required.
-r, --redundancy string Types of redundancy for the interconnection. Either 'primary' or 'redundant'.
-T, --service-token-type string Type of service token for shared connection. Enum: 'a_side', 'z_side'.
-s, --speed int32 The maximum speed of the interconnections. (default 1000000000)
-t, --type string Type of of interconnection. Either 'dedicated' or 'shared' when requesting for a Fabric VC.
--vlan int32Slice A list of VLANs to attach to the Interconnection. Ex: --vlans 1000, 1001 . (default [])
--vrf strings A list of VRFs to attach to the Interconnection. Ex: --vrfs uuid1, uuid2 .
```

### Options inherited from parent commands
Expand Down
6 changes: 5 additions & 1 deletion docs/metal_interconnections_delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Deletes a interconnection.

### Synopsis

Deletes the specified interconnection.
Deletes the specified interconnection. Use --force to skip confirmation

```
metal interconnections delete -i <connection_id> [flags]
Expand All @@ -15,11 +15,15 @@ metal interconnections delete -i <connection_id> [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.
```
Expand Down
3 changes: 2 additions & 1 deletion internal/gateway/retrieve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down
118 changes: 94 additions & 24 deletions internal/interconnections/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package interconnections

import (
"context"
"errors"
"fmt"

metal "github.com/equinix/equinix-sdk-go/services/metalv1"
"github.com/spf13/cobra"
)

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 <name> [-m <metro>] [-r <redundancy> ] [-t <type> ] [-p <project_id> ] | [-O <organization_id> ]`,
Expand All @@ -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)
Expand All @@ -54,18 +80,62 @@ 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")
_ = createInterconnectionsCmd.MarkFlagRequired("redundancy")
_ = 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
}
28 changes: 25 additions & 3 deletions internal/interconnections/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <connection_id> `,
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
Expand All @@ -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
Expand Down
98 changes: 98 additions & 0 deletions test/e2e/interconnections/create_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 2bb181a

Please sign in to comment.