diff --git a/internal/devices/create.go b/internal/devices/create.go index 426a1f66..22850b88 100644 --- a/internal/devices/create.go +++ b/internal/devices/create.go @@ -21,11 +21,11 @@ package devices import ( + "context" "fmt" "os" - "time" - "github.com/packethost/packngo" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -64,62 +64,63 @@ func (c *Client) Create() *cobra.Command { metal device create -p $METAL_PROJECT_ID -P c3.medium.x86 -m sv -H test-rocky -O rocky_8 -r 47161704-1715-4b45-8549-fb3f4b2c32c7`, RunE: func(cmd *cobra.Command, args []string) error { - var endDt *packngo.Timestamp - if userdata != "" && userdataFile != "" { return fmt.Errorf("either userdata or userdata-file should be set") } cmd.SilenceUsage = true - if userdataFile != "" { userdataRaw, readErr := os.ReadFile(userdataFile) if readErr != nil { - return fmt.Errorf("Could not read userdata-file: %w", readErr) + return fmt.Errorf("could not read userdata-file: %w", readErr) } userdata = string(userdataRaw) } - - if terminationTime != "" { - parsedTime, err := time.Parse(time.RFC3339, terminationTime) - if err != nil { - return fmt.Errorf("Could not parse time %q: %w", terminationTime, err) - } - endDt = &packngo.Timestamp{Time: parsedTime} - } - + // var endDt time.Time + + // if terminationTime != "" { + // parsedTime, err := time.Parse(time.RFC3339, terminationTime) + // if err != nil { + // return fmt.Errorf("could not parse time %q: %w", terminationTime, err) + // } + // endDt = parsedTime + // } var facilityArgs []string + + // var deviceCreateInFacilityInput *metal.DeviceCreateInFacilityInput + var request metal.ApiCreateDeviceRequest if facility != "" { facilityArgs = append(facilityArgs, facility) - } - request := &packngo.DeviceCreateRequest{ - Hostname: hostname, - Plan: plan, - Facility: facilityArgs, - Metro: metro, - OS: operatingSystem, - BillingCycle: billingCycle, - ProjectID: projectID, - UserData: userdata, - CustomData: customdata, - IPXEScriptURL: ipxescripturl, - Tags: tags, - PublicIPv4SubnetSize: publicIPv4SubnetSize, - AlwaysPXE: alwaysPXE, - HardwareReservationID: hardwareReservationID, - SpotInstance: spotInstance, - SpotPriceMax: spotPriceMax, - TerminationTime: endDt, + facilityDeviceRequest := metal.CreateDeviceRequest{ + DeviceCreateInFacilityInput: &metal.DeviceCreateInFacilityInput{ + Facility: facilityArgs, + Plan: plan, + OperatingSystem: operatingSystem, + Hostname: &hostname, + }, + } + request = c.Service.CreateDevice(context.Background(), projectID).CreateDeviceRequest(facilityDeviceRequest).Include(nil).Exclude(nil) } + if metro != "" { + + metroDeviceRequest := metal.CreateDeviceRequest{ + DeviceCreateInMetroInput: &metal.DeviceCreateInMetroInput{ + Metro: metro, + Plan: plan, + OperatingSystem: operatingSystem, + Hostname: &hostname, + }, + } - device, _, err := c.Service.Create(request) + request = c.Service.CreateDevice(context.Background(), projectID).CreateDeviceRequest(metroDeviceRequest).Include(nil).Exclude(nil) + } + device, _, err := request.Execute() if err != nil { - return fmt.Errorf("Could not create Device: %w", err) + return fmt.Errorf("could not create Device: %w", err) } - header := []string{"ID", "Hostname", "OS", "State", "Created"} data := make([][]string, 1) - data[0] = []string{device.ID, device.Hostname, device.OS.Name, device.State, device.Created} + data[0] = []string{device.GetId(), device.GetHostname(), *device.GetOperatingSystem().Name, device.GetState(), device.GetCreatedAt().String()} return c.Out.Output(device, header, &data) }, diff --git a/internal/devices/delete.go b/internal/devices/delete.go index 4b95576b..1ca13632 100644 --- a/internal/devices/delete.go +++ b/internal/devices/delete.go @@ -21,6 +21,7 @@ package devices import ( + "context" "fmt" "github.com/manifoldco/promptui" @@ -31,7 +32,7 @@ func (c *Client) Delete() *cobra.Command { var deviceID string var force bool deleteDevice := func(id string) error { - _, err := c.Service.Delete(id, force) + _, err := c.Service.DeleteDevice(context.Background(), id).ForceDelete(force).Execute() if err != nil { return err } diff --git a/internal/devices/device.go b/internal/devices/device.go index ff31cccc..58f01309 100644 --- a/internal/devices/device.go +++ b/internal/devices/device.go @@ -21,15 +21,15 @@ package devices 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" "github.com/spf13/viper" ) type Client struct { Servicer Servicer - Service packngo.DeviceService + Service metal.DevicesApiService Out outputs.Outputer } @@ -47,7 +47,7 @@ func (c *Client) NewCommand() *cobra.Command { } } - c.Service = c.Servicer.API(cmd).Devices + c.Service = *c.Servicer.MetalAPI(cmd).DevicesApi }, } @@ -65,9 +65,11 @@ 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 Config(cmd *cobra.Command) *viper.Viper + 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/devices/reboot.go b/internal/devices/reboot.go index 4b50c7b7..dd60bbde 100644 --- a/internal/devices/reboot.go +++ b/internal/devices/reboot.go @@ -21,8 +21,10 @@ package devices import ( + "context" "fmt" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -38,7 +40,8 @@ func (c *Client) Reboot() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - _, err := c.Service.Reboot(deviceID) + DeviceAction := metal.NewDeviceActionInput("reboot") + _, err := c.Service.PerformAction(context.Background(), deviceID).DeviceActionInput(*DeviceAction).Execute() if err != nil { return fmt.Errorf("Could not reboot Device: %w", err) } diff --git a/internal/devices/reinstall.go b/internal/devices/reinstall.go index 29b43a0b..435c03b0 100644 --- a/internal/devices/reinstall.go +++ b/internal/devices/reinstall.go @@ -21,9 +21,10 @@ package devices import ( + "context" "fmt" - "github.com/packethost/packngo" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -47,14 +48,30 @@ func (c *Client) Reinstall() *cobra.Command { metal device reinstall -d 50382f72-02b7-4b40-ac8d-253713e1e174 -O ubuntu_22_04 --preserve-data`, RunE: func(cmd *cobra.Command, args []string) error { - request := packngo.DeviceReinstallFields{ - OperatingSystem: operatingSystem, - PreserveData: preserveData, - DeprovisionFast: deprovisionFast, + DeviceAction := metal.NewDeviceActionInput("reinstall") + + if preserveData { + DeviceAction.PreserveData = &preserveData + } + if deprovisionFast { + DeviceAction.DeprovisionFast = &deprovisionFast + } + device, _, Err := c.Service.FindDeviceById(context.Background(), id).Execute() + if Err != nil { + fmt.Printf("Error when calling `DevicesApiService.FindDeviceByID``: %v\n", Err) + Err = fmt.Errorf("Could not reinstall Device: %w", Err) + return Err + } + + if operatingSystem == "" || operatingSystem == "null" { + DeviceAction.SetOperatingSystem(device.OperatingSystem.GetSlug()) + } else { + DeviceAction.OperatingSystem = &operatingSystem } - _, err := c.Service.Reinstall(id, &request) + _, err := c.Service.PerformAction(context.Background(), id).DeviceActionInput(*DeviceAction).Execute() if err != nil { + fmt.Printf("Error when calling `DevicesApiService.PerformAction``: %v\n", err) err = fmt.Errorf("Could not reinstall Device: %w", err) } diff --git a/internal/devices/retrieve.go b/internal/devices/retrieve.go index 29ee0c60..7794323e 100644 --- a/internal/devices/retrieve.go +++ b/internal/devices/retrieve.go @@ -21,8 +21,11 @@ package devices import ( + "context" "fmt" + "strconv" + pager "github.com/equinix/metal-cli/internal/pagination" "github.com/spf13/cobra" ) @@ -48,26 +51,52 @@ func (c *Client) Retrieve() *cobra.Command { cmd.SilenceUsage = true if deviceID != "" { - device, _, err := c.Service.Get(deviceID, nil) + device, _, err := c.Service.FindDeviceById(context.Background(), deviceID).Include(c.Servicer.Includes(nil)).Exclude(c.Servicer.Excludes(nil)).Execute() if err != nil { return fmt.Errorf("Could not get Devices: %w", err) } header := []string{"ID", "Hostname", "OS", "State", "Created"} data := make([][]string, 1) - data[0] = []string{device.ID, device.Hostname, device.OS.Name, device.State, device.Created} + data[0] = []string{device.GetId(), device.GetHostname(), device.OperatingSystem.GetName(), device.GetState(), device.GetCreatedAt().String()} return c.Out.Output(device, header, &data) } - devices, _, err := c.Service.List(projectID, c.Servicer.ListOptions(nil, nil)) + request := c.Service.FindProjectDevices(context.Background(), projectID).Include(c.Servicer.Includes(nil)).Exclude(c.Servicer.Excludes(nil)) + filters := c.Servicer.Filters() + if filters["type"] != "" { + request = request.Type_(filters["type"]) + } + + if filters["facility"] != "" { + request = request.Facility(filters["facility"]) + } + + if filters["hostname"] != "" { + request = request.Hostname(filters["hostname"]) + } + + if filters["reserved"] != "" { + value := filters["reserved"] + reserve, rerr := strconv.ParseBool(value) + if rerr != nil { + request = request.Reserved(reserve) + } + } + + if filters["tag"] != "" { + request = request.Tag(filters["tag"]) + } + + devices, err := pager.GetProjectDevices(request) if err != nil { return fmt.Errorf("Could not list Devices: %w", err) } data := make([][]string, len(devices)) for i, dc := range devices { - data[i] = []string{dc.ID, dc.Hostname, dc.OS.Name, dc.State, dc.Created} + data[i] = []string{dc.GetId(), dc.GetHostname(), dc.OperatingSystem.GetName(), dc.GetState(), dc.GetCreatedAt().String()} } header := []string{"ID", "Hostname", "OS", "State", "Created"} diff --git a/internal/devices/start.go b/internal/devices/start.go index 52cd4e2f..7411f4b9 100644 --- a/internal/devices/start.go +++ b/internal/devices/start.go @@ -21,8 +21,10 @@ package devices import ( + "context" "fmt" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -38,7 +40,8 @@ func (c *Client) Start() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - _, err := c.Service.PowerOn(deviceID) + DeviceAction := metal.NewDeviceActionInput("power_on") + _, err := c.Service.PerformAction(context.Background(), deviceID).DeviceActionInput(*DeviceAction).Execute() if err != nil { return fmt.Errorf("Could not start Device: %w", err) } diff --git a/internal/devices/stop.go b/internal/devices/stop.go index 7b65101a..472849b9 100644 --- a/internal/devices/stop.go +++ b/internal/devices/stop.go @@ -21,8 +21,10 @@ package devices import ( + "context" "fmt" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -37,7 +39,8 @@ func (c *Client) Stop() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - _, err := c.Service.PowerOff(deviceID) + DeviceAction := metal.NewDeviceActionInput("power_off") + _, err := c.Service.PerformAction(context.Background(), deviceID).DeviceActionInput(*DeviceAction).Execute() if err != nil { return fmt.Errorf("Could not stop Device: %w", err) } diff --git a/internal/devices/update.go b/internal/devices/update.go index cb3c5a1c..4aad23dd 100644 --- a/internal/devices/update.go +++ b/internal/devices/update.go @@ -21,9 +21,11 @@ package devices import ( + "context" + "encoding/json" "fmt" - "github.com/packethost/packngo" + metal "github.com/equinix-labs/metal-go/metal/v1" "github.com/spf13/cobra" ) @@ -51,48 +53,53 @@ func (c *Client) Update() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - req := &packngo.DeviceUpdateRequest{} + deviceUpdate := metal.NewDeviceUpdateInput() if hostname != "" { - req.Hostname = &hostname + deviceUpdate.Hostname = &hostname } if description != "" { - req.Description = &description + deviceUpdate.Description = &description } if userdata != "" { - req.UserData = &userdata + deviceUpdate.Userdata = &userdata } if locked { - req.Locked = &locked + deviceUpdate.Locked = &locked } if len(tags) > 0 { - req.Tags = &tags + deviceUpdate.Tags = tags } if alwaysPXE { - req.AlwaysPXE = &alwaysPXE + deviceUpdate.AlwaysPxe = &alwaysPXE } if ipxescripturl != "" { - req.IPXEScriptURL = &ipxescripturl + deviceUpdate.IpxeScriptUrl = &ipxescripturl } if customdata != "" { - req.CustomData = &customdata - } + var customdataIntr map[string]interface{} + err := json.Unmarshal([]byte(customdata), &customdataIntr) + if err != nil { + panic(err) + } - device, _, err := c.Service.Update(deviceID, req) + deviceUpdate.Customdata = customdataIntr + } + device, _, err := c.Service.UpdateDevice(context.Background(), deviceID).DeviceUpdateInput(*deviceUpdate).Execute() if err != nil { return fmt.Errorf("Could not update Device: %w", err) } header := []string{"ID", "Hostname", "OS", "State"} data := make([][]string, 1) - data[0] = []string{device.ID, device.Hostname, device.OS.Name, device.State} + data[0] = []string{device.GetId(), device.GetHostname(), device.OperatingSystem.GetName(), device.GetState()} return c.Out.Output(device, header, &data) }, diff --git a/internal/pagination/pager.go b/internal/pagination/pager.go index fd8c728a..e75ceaad 100644 --- a/internal/pagination/pager.go +++ b/internal/pagination/pager.go @@ -127,3 +127,22 @@ func GetAllOrganizations(s metal.OrganizationsApiService, include, exclude []str return orgs, nil } } + +func GetProjectDevices(s metal.ApiFindProjectDevicesRequest) ([]metal.Device, error) { + var devices []metal.Device + + page := int32(1) // int32 | Page to return (optional) (default to 1) + perPage := int32(20) // int32 | Items returned per page (optional) (default to 10) + for { + devicePage, _, err := s.Page(page).PerPage(perPage).Execute() + if err != nil { + return nil, err + } + devices = append(devices, devicePage.Devices...) + if devicePage.Meta.GetLastPage() > devicePage.Meta.GetCurrentPage() { + page = page + 1 + continue + } + return devices, nil + } +} diff --git a/test/e2e/device_test_create.go b/test/e2e/device_test_create.go new file mode 100644 index 00000000..11773a53 --- /dev/null +++ b/test/e2e/device_test_create.go @@ -0,0 +1,203 @@ +package e2e + +import ( + "fmt" + "io" + "os" + "regexp" + "strings" + "testing" + "time" + + root "github.com/equinix/metal-cli/internal/cli" + "github.com/equinix/metal-cli/internal/devices" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/internal/projects" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCli_Devices_create(t *testing.T) { + var projectId, deviceId string + subCommand := "device" + consumerToken := "" + apiURL := "" + Version := "metal" + rootClient := root.NewClient(consumerToken, apiURL, Version) + type fields struct { + MainCmd *cobra.Command + Outputer outputPkg.Outputer + } + tests := []struct { + name string + fields fields + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "create_project", + fields: fields{ + MainCmd: projects.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + project_name := "metal-cli-test-project" + root := c.Root() + root.SetArgs([]string{"project", "create", "-n", project_name}) + 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 + fmt.Print(string(out[:])) + if !strings.Contains(string(out[:]), "metal-cli-test-project") { + t.Error("expected output should include metal-cli-test-device string in the out string ") + } + + // Regular expression pattern to match the ID hash when NAME is "cli-acc-org-test3" + idNamePattern := `(?m)^\| ([a-zA-Z0-9-]+) +\| *` + project_name + ` *\|` + + // Find the match of the ID and NAME pattern in the table string + match := regexp.MustCompile(idNamePattern).FindStringSubmatch(string(out[:])) + + // Extract the ID from the match + if len(match) > 1 { + projectId = strings.TrimSpace(match[1]) + } else { + fmt.Println("No projecct ID found for the given NAME.") + } + }, + }, + { + name: "create_device", + fields: fields{ + MainCmd: devices.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + root.SetArgs([]string{subCommand, "create", "-p", string(projectId), "-P", "c3.small.x86", "-m", "da", "-O", "ubuntu_20_04", "-H", "metal-cli-test-project-dev"}) + 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[:]), "metal-cli-test-project-dev") && + !strings.Contains(string(out[:]), "Ubuntu 20.04 LTS") && + !strings.Contains(string(out[:]), "queued") { + t.Error("expected output should include metal-test-device-project, Ubuntu 20.04 LTS, and queued strings in the out string ") + } + fmt.Print(string(out[:])) + name := "metal-cli-test-project-dev" + idNamePattern := `(?m)^\| ([a-zA-Z0-9-]+) +\| *` + name + ` *\|` + + // Find the match of the ID and NAME pattern in the table string + match := regexp.MustCompile(idNamePattern).FindStringSubmatch(string(out[:])) + + // Extract the ID from the match + if len(match) > 1 { + deviceId = strings.TrimSpace(match[1]) + } else { + fmt.Println("No ID found for the given NAME.") + } + }, + }, + { + name: "delete_device", + fields: fields{ + MainCmd: devices.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + + // Regular expression pattern to match the STATE column content + statePattern := `(?m)^\|[^|]+\|[^|]+\|[^|]+\| *([^|]+) *\|` + // Predefined time and interval for retries + predefinedTime := 200 * time.Second // Adjust this as needed + retryInterval := 5 * time.Second // Adjust this as needed + + // Loop to check STATE with retries + startTime := time.Now() + for time.Since(startTime) < predefinedTime { + root.SetArgs([]string{subCommand, "get", "-i", deviceId}) + 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 + + // Find the match of the STATE pattern in the table string + match := regexp.MustCompile(statePattern).FindStringSubmatch(string(out[:])) + + // Extract the STATE from the match + if len(match) > 1 { + state := strings.TrimSpace(match[1]) + if state == "active" { + break + } + } else { + break + } + + // Sleep for the specified interval + time.Sleep(retryInterval) + } + + root.SetArgs([]string{subCommand, "delete", "-f", "-i", deviceId}) + err := root.Execute() + assert.NoError(t, err) + + }, + }, + { + name: "delete_project", + fields: fields{ + MainCmd: projects.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + root.SetArgs([]string{"project", "delete", "-i", projectId, "-f"}) + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + // Sleep for 1 Minute + time.Sleep(60 * time.Second) + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + if !strings.Contains(string(out[:]), "Project "+projectId+" successfully deleted.") { + t.Error("expected output should include" + "Project " + projectId + " successfully deleted." + "in the out string ") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := rootClient.NewCommand() + rootCmd.AddCommand(tt.fields.MainCmd) + tt.cmdFunc(t, tt.fields.MainCmd) + }) + } +}