Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Metal CLI support create shared interconnection with vlans and vrfs #397

Merged
merged 13 commits into from
Jan 23, 2024
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
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
aayushrangwala marked this conversation as resolved.
Show resolved Hide resolved
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())
ctreatma marked this conversation as resolved.
Show resolved Hide resolved
})

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
Loading