diff --git a/cli/command/create/create-acmeaccount.go b/cli/command/create/create-acmeaccount.go index b749496f..76258d83 100644 --- a/cli/command/create/create-acmeaccount.go +++ b/cli/command/create/create-acmeaccount.go @@ -13,7 +13,7 @@ var create_acmeaccountCmd = &cobra.Command{ The config can be set with the --file flag or piped from stdin. For config examples see "example acmeaccount"`, RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0, "AcmeAccountID") + id := cli.RequiredIDset(args, 0, "AcmeAccountID") config, err := proxmox.NewConfigAcmeAccountFromJson(cli.NewConfig()) if err != nil { return diff --git a/cli/command/create/create-pool.go b/cli/command/create/create-pool.go index bef36d27..8d53b400 100644 --- a/cli/command/create/create-pool.go +++ b/cli/command/create/create-pool.go @@ -9,7 +9,7 @@ var create_poolCmd = &cobra.Command{ Use: "pool POOLID [COMMENT]", Short: "Creates a new pool", RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0, "PoolID") + id := cli.RequiredIDset(args, 0, "PoolID") var comment string if len(args) > 1 { comment = args[1] diff --git a/cli/command/create/create-snapshot.go b/cli/command/create/create-snapshot.go new file mode 100644 index 00000000..26cbbe80 --- /dev/null +++ b/cli/command/create/create-snapshot.go @@ -0,0 +1,39 @@ +package create + +import ( + "github.com/Telmate/proxmox-api-go/cli" + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/spf13/cobra" +) + +var ( + // flag needs to be reset, as this value will persist during tests + memory bool + create_snapshotCmd = &cobra.Command{ + Use: "snapshot GUESTID SNAPSHOTNAME [DESCRIPTION]", + Short: "Creates a new snapshot of the specefied guest", + TraverseChildren: true, + Args: cobra.RangeArgs(2, 3), + RunE: func(cmd *cobra.Command, args []string) (err error) { + id := cli.ValidateIntIDset(args, "GuestID") + snapName := cli.RequiredIDset(args, 1, "SnapshotName") + config := proxmox.ConfigSnapshot{ + Name: snapName, + Description: cli.OptionalIDset(args, 2), + VmState: memory, + } + memory = false + err = config.CreateSnapshot(uint(id), cli.NewClient()) + if err != nil { + return + } + cli.PrintItemCreated(CreateCmd.OutOrStdout(), snapName, "Snapshot") + return + }, + } +) + +func init() { + CreateCmd.AddCommand(create_snapshotCmd) + create_snapshotCmd.Flags().BoolVar(&memory, "memory", false, "Snapshot memory") +} diff --git a/cli/command/create/create-storage.go b/cli/command/create/create-storage.go index 4273f773..6083b380 100644 --- a/cli/command/create/create-storage.go +++ b/cli/command/create/create-storage.go @@ -13,7 +13,7 @@ var create_storageCmd = &cobra.Command{ The config can be set with the --file flag or piped from stdin. For config examples see "example storage"`, RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0, "StorageID") + id := cli.RequiredIDset(args, 0, "StorageID") config, err := proxmox.NewConfigStorageFromJson(cli.NewConfig()) if err != nil { return diff --git a/cli/command/create/guest/create-guest.go b/cli/command/create/guest/create-guest.go index eb5fc07e..2b7932af 100644 --- a/cli/command/create/guest/create-guest.go +++ b/cli/command/create/guest/create-guest.go @@ -20,7 +20,7 @@ func init() { func createGuest(args []string, IDtype string) (err error) { id := cli.ValidateIntIDset(args, IDtype+"ID") - node := cli.ValidateIDset(args, 1, "NodeID") + node := cli.RequiredIDset(args, 1, "NodeID") vmr := proxmox.NewVmRef(id) vmr.SetNode(node) c := cli.NewClient() diff --git a/cli/command/delete/delete-snapshot.go b/cli/command/delete/delete-snapshot.go new file mode 100644 index 00000000..5f9a3240 --- /dev/null +++ b/cli/command/delete/delete-snapshot.go @@ -0,0 +1,30 @@ +package delete + +import ( + "github.com/Telmate/proxmox-api-go/cli" + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/spf13/cobra" +) + +var ( + delete_snapshotCmd = &cobra.Command{ + Use: "snapshot GUESTID SNAPSHOTNAME", + Short: "Deletes the Speciefied snapshot", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + id := cli.ValidateIntIDset(args, "GuestID") + snapName := cli.RequiredIDset(args, 1, "SnapshotName") + c := cli.NewClient() + _, err = c.DeleteSnapshot(proxmox.NewVmRef(id), snapName) + if err != nil { + return + } + cli.PrintItemDeleted(deleteCmd.OutOrStdout(), snapName, "Snapshot") + return + }, + } +) + +func init() { + deleteCmd.AddCommand(delete_snapshotCmd) +} diff --git a/cli/command/delete/delete.go b/cli/command/delete/delete.go index 9becf311..426a3246 100644 --- a/cli/command/delete/delete.go +++ b/cli/command/delete/delete.go @@ -18,7 +18,7 @@ func init() { func deleteID(args []string, IDtype string) (err error) { var exitStatus string - id := cli.ValidateIDset(args, 0, IDtype+"ID") + id := cli.RequiredIDset(args, 0, IDtype+"ID") c := cli.NewClient() switch IDtype { case "AcmeAccount": diff --git a/cli/command/get/get.go b/cli/command/get/get.go index 2cfe5f7f..7af994c5 100644 --- a/cli/command/get/get.go +++ b/cli/command/get/get.go @@ -16,7 +16,7 @@ func init() { } func getConfig(args []string, IDtype string) (err error) { - id := cli.ValidateIDset(args, 0, IDtype+"ID") + id := cli.RequiredIDset(args, 0, IDtype+"ID") c := cli.NewClient() var config interface{} switch IDtype { diff --git a/cli/command/guest/guest-rollback.go b/cli/command/guest/guest-rollback.go new file mode 100644 index 00000000..c01e5e28 --- /dev/null +++ b/cli/command/guest/guest-rollback.go @@ -0,0 +1,29 @@ +package guest + +import ( + "fmt" + + "github.com/Telmate/proxmox-api-go/cli" + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/spf13/cobra" +) + +var guest_rollbackCmd = &cobra.Command{ + Use: "rollback GUESTID SNAPSHOT", + Short: "Shuts the speciefid guest down", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + vmr := proxmox.NewVmRef(cli.ValidateIntIDset(args, "GuestID")) + snapName := cli.RequiredIDset(args, 1, "SnapshotName") + c := cli.NewClient() + _, err = c.RollbackSnapshot(vmr, snapName) + if err == nil { + fmt.Fprintf(GuestCmd.OutOrStdout(), "Guest with id (%d) has been rolled back to snapshot (%s)\n", vmr.VmId(), snapName) + } + return + }, +} + +func init() { + GuestCmd.AddCommand(guest_rollbackCmd) +} diff --git a/cli/command/list/list-snapshots.go b/cli/command/list/list-snapshots.go index 2c2698ae..386485f7 100644 --- a/cli/command/list/list-snapshots.go +++ b/cli/command/list/list-snapshots.go @@ -1,37 +1,44 @@ package list import ( - "fmt" "github.com/Telmate/proxmox-api-go/cli" "github.com/Telmate/proxmox-api-go/proxmox" "github.com/spf13/cobra" ) -var list_snapshotsCmd = &cobra.Command{ - Use: "snapshots GuestID", - Short: "Prints a list of QemuSnapshots in raw json format", - Run: func(cmd *cobra.Command, args []string) { - id := cli.ValidateExistinGuestID(args, 0) - c := cli.NewClient() - vmr := proxmox.NewVmRef(id) - _, err := c.GetVmInfo(vmr) - cli.LogFatalError(err) - jbody, _, err := c.ListQemuSnapshot(vmr) - cli.LogFatalError(err) - temp := jbody["data"].([]interface{}) - if len(temp) == 1 { - fmt.Printf("Guest with ID (%d) has no snapshots",id) - } else { - for _, e := range temp { - snapshotName := e.(map[string]interface{})["name"].(string) - if snapshotName != "current" { - fmt.Println(snapshotName) - } +var ( + // flag needs to be reset, as this value will persist during tests + noTree bool + list_snapshotsCmd = &cobra.Command{ + Use: "snapshots GuestID", + Short: "Prints a list of QemuSnapshots in json format", + TraverseChildren: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + id := cli.ValidateExistinGuestID(args, 0) + jbody, err := cli.NewClient().ListSnapshots(proxmox.NewVmRef(id)) + if err != nil { + noTree = false + return } - } - }, -} + var list []*proxmox.Snapshot + if noTree { + noTree = false + list = proxmox.FormatSnapshotsList(jbody) + } else { + list = proxmox.FormatSnapshotsTree(jbody) + } + if len(list) == 0 { + listCmd.Printf("Guest with ID (%d) has no snapshots", id) + } else { + cli.PrintFormattedJson(listCmd.OutOrStdout(), list) + } + return + }, + } +) func init() { listCmd.AddCommand(list_snapshotsCmd) + list_snapshotsCmd.Flags().BoolVar(&noTree, "no-tree", false, "Format output as list instead of a tree.") } diff --git a/cli/command/node/node-reboot.go b/cli/command/node/node-reboot.go index 31b6db0f..c755cc6d 100644 --- a/cli/command/node/node-reboot.go +++ b/cli/command/node/node-reboot.go @@ -9,7 +9,7 @@ var reboot_nodeCmd = &cobra.Command{ Use: "reboot NODE", Short: "Reboots the specified node", RunE: func(cmd *cobra.Command, args []string) (err error) { - node := cli.ValidateIDset(args, 0, "node") + node := cli.RequiredIDset(args, 0, "node") c := cli.NewClient() _, err = c.RebootNode(node) if err != nil { diff --git a/cli/command/node/node-shutdown.go b/cli/command/node/node-shutdown.go index ef37359a..c4994620 100644 --- a/cli/command/node/node-shutdown.go +++ b/cli/command/node/node-shutdown.go @@ -9,7 +9,7 @@ var shutdown_nodeCmd = &cobra.Command{ Use: "shutdown NODE", Short: "Shuts the specified node down", RunE: func(cmd *cobra.Command, args []string) (err error) { - node := cli.ValidateIDset(args, 0, "node") + node := cli.RequiredIDset(args, 0, "node") c := cli.NewClient() _, err = c.ShutdownNode(node) if err != nil { diff --git a/cli/command/set/set-metricserver.go b/cli/command/set/set-metricserver.go index 0a5e82e5..b03ea719 100644 --- a/cli/command/set/set-metricserver.go +++ b/cli/command/set/set-metricserver.go @@ -14,7 +14,7 @@ Depending on the current state of the MetricServer, the MetricServer will be cre The config can be set with the --file flag or piped from stdin. For config examples see "example metricserver"`, RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0,"MetricServerID") + id := cli.RequiredIDset(args, 0, "MetricServerID") config, err := proxmox.NewConfigMetricsFromJson(cli.NewConfig()) if err != nil { return @@ -24,7 +24,7 @@ For config examples see "example metricserver"`, if err != nil { return } - cli.PrintItemSet(setCmd.OutOrStdout() ,id ,"MericServer") + cli.PrintItemSet(setCmd.OutOrStdout(), id, "MericServer") return }, } diff --git a/cli/command/set/set-user.go b/cli/command/set/set-user.go index 155003db..65ef474f 100644 --- a/cli/command/set/set-user.go +++ b/cli/command/set/set-user.go @@ -14,7 +14,7 @@ Depending on the current state of the user, the user will be created or updated. The config can be set with the --file flag or piped from stdin. For config examples see "example user"`, RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0, "UserID") + id := cli.RequiredIDset(args, 0, "UserID") config, err := proxmox.NewConfigUserFromJson(cli.NewConfig()) if err != nil { return @@ -28,7 +28,7 @@ For config examples see "example user"`, if err != nil { return } - cli.PrintItemSet(setCmd.OutOrStdout() ,id ,"User") + cli.PrintItemSet(setCmd.OutOrStdout(), id, "User") return }, } diff --git a/cli/command/update/update-poolcomment.go b/cli/command/update/update-poolcomment.go index 541d0747..db57dfba 100644 --- a/cli/command/update/update-poolcomment.go +++ b/cli/command/update/update-poolcomment.go @@ -8,9 +8,9 @@ import ( var update_poolCmd = &cobra.Command{ Use: "poolcomment POOLID [COMMENT]", Short: "Updates the comment on the speciefied pool", - RunE: func(cmd *cobra.Command, args []string) (err error){ + RunE: func(cmd *cobra.Command, args []string) (err error) { var comment string - id := cli.ValidateIDset(args, 0, "PoolID") + id := cli.RequiredIDset(args, 0, "PoolID") if len(args) > 1 { comment = args[1] } @@ -19,7 +19,7 @@ var update_poolCmd = &cobra.Command{ if err != nil { return } - cli.PrintItemUpdated(updateCmd.OutOrStdout() ,id, "PoolComment") + cli.PrintItemUpdated(updateCmd.OutOrStdout(), id, "PoolComment") return }, } diff --git a/cli/command/update/update-snapshotdescription.go b/cli/command/update/update-snapshotdescription.go new file mode 100644 index 00000000..3c18d123 --- /dev/null +++ b/cli/command/update/update-snapshotdescription.go @@ -0,0 +1,28 @@ +package update + +import ( + "github.com/Telmate/proxmox-api-go/cli" + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/spf13/cobra" +) + +var update_snapshotCmd = &cobra.Command{ + Use: "snapshot GUESTID SNAPSHOTNAME [DESCRIPTION]", + Short: "Updates the description on the speciefied snapshot", + Args: cobra.RangeArgs(2, 3), + RunE: func(cmd *cobra.Command, args []string) (err error) { + id := cli.ValidateIntIDset(args, "GuestID") + snapName := cli.RequiredIDset(args, 1, "SnapshotName") + des := cli.OptionalIDset(args, 2) + err = cli.NewClient().UpdateSnapshotDescription(proxmox.NewVmRef(id), snapName, des) + if err != nil { + return + } + cli.PrintItemUpdated(updateCmd.OutOrStdout(), snapName, "Snapshot") + return + }, +} + +func init() { + updateCmd.AddCommand(update_snapshotCmd) +} diff --git a/cli/command/update/update-storage.go b/cli/command/update/update-storage.go index ec7179c4..655e889d 100644 --- a/cli/command/update/update-storage.go +++ b/cli/command/update/update-storage.go @@ -13,7 +13,7 @@ var update_storageCmd = &cobra.Command{ The config can be set with the --file flag or piped from stdin. For config examples see "example storage"`, RunE: func(cmd *cobra.Command, args []string) (err error) { - id := cli.ValidateIDset(args, 0, "StorageID") + id := cli.RequiredIDset(args, 0, "StorageID") config, err := proxmox.NewConfigStorageFromJson(cli.NewConfig()) if err != nil { return diff --git a/cli/validate.go b/cli/validate.go index ed12ea98..714058ca 100644 --- a/cli/validate.go +++ b/cli/validate.go @@ -6,23 +6,34 @@ import ( "strconv" ) -func ValidateIDset(args []string, indexPos int, text string) string { - if indexPos+1 > len(args) { +// Should be used for Required IDs. +// returns if the indexd arg if it is set. It throws and error when the indexed arg is not set. +func RequiredIDset(args []string, indexPos uint, text string) string { + if int(indexPos+1) > len(args) { log.Fatal(fmt.Errorf("error: no %s has been provided", text)) } return args[indexPos] } +// Should be used for Optional IDs. +// returns if the indexd arg if it is set. It returns an empty string when the indexed arg is not set. +func OptionalIDset(args []string, indexPos uint) (out string) { + if int(indexPos+1) <= len(args) { + out = args[indexPos] + } + return +} + func ValidateIntIDset(args []string, text string) int { - id, err := strconv.Atoi(ValidateIDset(args, 0, text)) + id, err := strconv.Atoi(RequiredIDset(args, 0, text)) if err != nil && id <= 0 { log.Fatal(fmt.Errorf("error: %s must be a positive integer", text)) } return id } -func ValidateExistinGuestID(args []string, indexPos int) int { - id, err := strconv.Atoi(ValidateIDset(args, indexPos, "GuestID")) +func ValidateExistinGuestID(args []string, indexPos uint) int { + id, err := strconv.Atoi(RequiredIDset(args, indexPos, "GuestID")) if err != nil || id < 100 { log.Fatal(fmt.Errorf("error: GuestID must be a positive integer of 100 or greater")) } diff --git a/main.go b/main.go index 14465de6..30323f74 100644 --- a/main.go +++ b/main.go @@ -235,13 +235,13 @@ func main() { failError(config.CloneVm(sourceVmr, vmr, c)) failError(config.UpdateConfig(vmr, c)) log.Println("Complete") - // TODO make createQemuSnapshot in new cli + case "createQemuSnapshot": sourceVmr, err := c.GetVmRefByName(flag.Args()[1]) failError(err) jbody, err = c.CreateQemuSnapshot(sourceVmr, flag.Args()[2]) failError(err) - // TODO make deleteQemuSnapshot in new cli + case "deleteQemuSnapshot": sourceVmr, err := c.GetVmRefByName(flag.Args()[1]) failError(err) @@ -285,7 +285,7 @@ func main() { } } failError(err) - // TODO make rollbackQemu in new cli + case "rollbackQemu": sourceVmr, err := c.GetVmRefByName(flag.Args()[1]) failError(err) diff --git a/proxmox/client.go b/proxmox/client.go index 90afd51d..17bced2a 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -628,6 +628,15 @@ func (c *Client) CloneQemuVm(vmr *VmRef, vmParams map[string]interface{}) (exitS return } +func (c *Client) CreateSnapshot(vmr *VmRef, params map[string]interface{}) (exitStatus string, err error) { + err = c.CheckVmRef(vmr) + if err != nil { + return + } + return c.CreateItemWithTask(params, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/") +} + +// DEPRECATED superceded by (c *Client) CreateSnapshot() func (c *Client) CreateQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus string, err error) { err = c.CheckVmRef(vmr) snapshotParams := map[string]interface{}{ @@ -652,26 +661,28 @@ func (c *Client) CreateQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus return } -func (c *Client) DeleteQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus string, err error) { +func (c *Client) DeleteSnapshot(vmr *VmRef, snapshot string) (exitStatus string, err error) { err = c.CheckVmRef(vmr) if err != nil { - return "", err + return } - url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/%s", vmr.node, vmr.vmType, vmr.vmId, snapshotName) - resp, err := c.session.Delete(url, nil, nil) - if err == nil { - taskResponse, err := ResponseJSON(resp) - if err != nil { - return "", err - } - exitStatus, err = c.WaitForCompletion(taskResponse) - if err != nil { - return "", err - } + return c.DeleteUrlWithTask("/nodes/" + vmr.node + "/" + vmr.vmType + "/" + strconv.Itoa(vmr.vmId) + "/snapshot/" + snapshot) +} + +// DEPRECATED superceded by (c *Client) DeleteSnapshot() +func (c *Client) DeleteQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus string, err error) { + return c.DeleteSnapshot(vmr, snapshotName) +} + +func (c *Client) ListSnapshots(vmr *VmRef) (taskResponse []interface{}, err error) { + err = c.CheckVmRef(vmr) + if err != nil { + return } - return + return c.GetItemConfigInterfaceArray("/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/", "Guest", "SNAPSHOTS") } +// DEPRECATED superceded by (c *Client) ListSnapshots() func (c *Client) ListQemuSnapshot(vmr *VmRef) (taskResponse map[string]interface{}, exitStatus string, err error) { err = c.CheckVmRef(vmr) if err != nil { @@ -689,19 +700,26 @@ func (c *Client) ListQemuSnapshot(vmr *VmRef) (taskResponse map[string]interface return } -func (c *Client) RollbackQemuVm(vmr *VmRef, snapshot string) (exitStatus string, err error) { +func (c *Client) RollbackSnapshot(vmr *VmRef, snapshot string) (exitStatus string, err error) { err = c.CheckVmRef(vmr) if err != nil { - return "", err + return } - url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/%s/rollback", vmr.node, vmr.vmType, vmr.vmId, snapshot) - var taskResponse map[string]interface{} - _, err = c.session.PostJSON(url, nil, nil, nil, &taskResponse) + return c.CreateItemWithTask(nil, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+snapshot+"/rollback") +} + +// DEPRECATED superceded by (c *Client) RollbackSnapshot() +func (c *Client) RollbackQemuVm(vmr *VmRef, snapshot string) (exitStatus string, err error) { + return c.RollbackSnapshot(vmr, snapshot) +} + +// Can only be used to update the description of an already existing snapshot +func (c *Client) UpdateSnapshotDescription(vmr *VmRef, snapshot, description string) (err error) { + err = c.CheckVmRef(vmr) if err != nil { - return "", err + return } - exitStatus, err = c.WaitForCompletion(taskResponse) - return + return c.UpdateItem(map[string]interface{}{"description": description}, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+snapshot+"/config") } // SetVmConfig - send config options diff --git a/proxmox/snapshot.go b/proxmox/snapshot.go new file mode 100644 index 00000000..f457177b --- /dev/null +++ b/proxmox/snapshot.go @@ -0,0 +1,84 @@ +package proxmox + +import ( + "encoding/json" + "fmt" +) + +type ConfigSnapshot struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + VmState bool `json:"ram,omitempty"` +} + +func (config *ConfigSnapshot) mapValues() map[string]interface{} { + return map[string]interface{}{ + "snapname": config.Name, + "description": config.Description, + "vmstate": config.VmState, + } +} + +func (config *ConfigSnapshot) CreateSnapshot(guestId uint, c *Client) (err error) { + params := config.mapValues() + _, err = c.CreateSnapshot(NewVmRef(int(guestId)), params) + if err != nil { + params, _ := json.Marshal(¶ms) + return fmt.Errorf("error creating Snapshot: %v, (params: %v)", err, string(params)) + } + return +} + +// Used for formatting the output when retrieving snapshots +type Snapshot struct { + Name string `json:"name"` + SnapTime uint `json:"time,omitempty"` + Description string `json:"description,omitempty"` + VmState bool `json:"ram,omitempty"` + Children []*Snapshot `json:"children,omitempty"` + Parent string `json:"parent,omitempty"` +} + +// Formats the taskResponse as a list of snapshots +func FormatSnapshotsList(taskResponse []interface{}) (list []*Snapshot) { + list = make([]*Snapshot, len(taskResponse)) + for i, e := range taskResponse { + list[i] = &Snapshot{} + if _, isSet := e.(map[string]interface{})["description"]; isSet { + list[i].Description = e.(map[string]interface{})["description"].(string) + } + if _, isSet := e.(map[string]interface{})["name"]; isSet { + list[i].Name = e.(map[string]interface{})["name"].(string) + } + if _, isSet := e.(map[string]interface{})["parent"]; isSet { + list[i].Parent = e.(map[string]interface{})["parent"].(string) + } + if _, isSet := e.(map[string]interface{})["snaptime"]; isSet { + list[i].SnapTime = uint(e.(map[string]interface{})["snaptime"].(float64)) + } + if _, isSet := e.(map[string]interface{})["vmstate"]; isSet { + list[i].VmState = Itob(int(e.(map[string]interface{})["vmstate"].(float64))) + } + } + return +} + +// Formats a list of snapshots as a tree of snapshots +func FormatSnapshotsTree(taskResponse []interface{}) (tree []*Snapshot) { + list := FormatSnapshotsList(taskResponse) + for _, e := range list { + for _, ee := range list { + if e.Parent == ee.Name { + ee.Children = append(ee.Children, e) + break + } + } + } + for _, e := range list { + if e.Parent == "" { + tree = append(tree, e) + } + e.Parent = "" + } + return +} diff --git a/proxmox/snapshot_test.go b/proxmox/snapshot_test.go new file mode 100644 index 00000000..2dce5a3e --- /dev/null +++ b/proxmox/snapshot_test.go @@ -0,0 +1,206 @@ +package proxmox + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// Test the formatting logic to build the tree of snapshots +func Test_FormatSnapshotsTree(t *testing.T) { + input := test_FormatSnapshots_Input() + output := test_FormatSnapshotsTree_Output() + for i, e := range input { + result, _ := json.Marshal(FormatSnapshotsTree(e)) + require.JSONEq(t, output[i], string(result)) + } +} + +// Test the formatting logic to build the list of snapshots +func Test_FormatSnapshotsList(t *testing.T) { + input := test_FormatSnapshots_Input() + output := test_FormatSnapshotsList_Output() + for i, e := range input { + result, _ := json.Marshal(FormatSnapshotsList(e)) + require.JSONEq(t, output[i], string(result)) + } +} + +func test_FormatSnapshots_Input() [][]interface{} { + return [][]interface{}{{map[string]interface{}{ + "name": "aa", + "snaptime": float64(1666361849), + "description": "", + "parent": "", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaa", + "snaptime": float64(1666361866), + "description": "", + "parent": "aa", + "vmstate": float64(1), + }, map[string]interface{}{ + "name": "aaaa", + "snaptime": float64(1666362071), + "description": "123456", + "parent": "aaa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaab", + "snaptime": float64(1666362062), + "description": "", + "parent": "aaa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaac", + "snaptime": float64(1666361873), + "description": "", + "parent": "aaa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaad", + "snaptime": float64(1666361937), + "description": "abcdefg", + "parent": "aaa", + "vmstate": float64(1), + }, map[string]interface{}{ + "name": "aaae", + "snaptime": float64(1666362084), + "description": "", + "parent": "aaa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "current", + "description": "You are here!", + "parent": "aaae", + }, map[string]interface{}{ + "name": "aab", + "snaptime": float64(1666361920), + "description": "", + "parent": "aa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaba", + "snaptime": float64(1666361952), + "description": "", + "parent": "aab", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aabaa", + "snaptime": float64(1666361960), + "description": "", + "parent": "aaba", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aac", + "snaptime": float64(1666361896), + "description": "", + "parent": "aa", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aaca", + "snaptime": float64(1666361988), + "description": "!@#()&", + "parent": "aac", + "vmstate": float64(1), + }, map[string]interface{}{ + "name": "aacaa", + "snaptime": float64(1666362006), + "description": "", + "parent": "aaca", + "vmstate": float64(1), + }, map[string]interface{}{ + "name": "aacb", + "snaptime": float64(1666361977), + "description": "", + "parent": "aac", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aacba", + "snaptime": float64(1666362021), + "description": "QWERTY", + "parent": "aacb", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aacc", + "snaptime": float64(1666361904), + "description": "", + "parent": "aac", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "aacca", + "snaptime": float64(1666361910), + "description": "", + "parent": "aacc", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "bb", + "snaptime": float64(1666361866), + "description": "aA1!", + "parent": "", + "vmstate": float64(1), + }, map[string]interface{}{ + "name": "bba", + "snaptime": float64(1666362071), + "description": "", + "parent": "bb", + "vmstate": float64(0), + }, map[string]interface{}{ + "name": "bbb", + "snaptime": float64(1666362062), + "description": "", + "parent": "bb", + "vmstate": float64(0), + }}} +} + +func test_FormatSnapshotsTree_Output() []string { + return []string{`[{ + "name":"aa","time":1666361849,"children":[{ + "name":"aaa","time":1666361866,"ram":true,"children":[{ + "name":"aaaa","time":1666362071,"description":"123456"},{ + "name":"aaab","time":1666362062},{ + "name":"aaac","time":1666361873},{ + "name":"aaad","time":1666361937,"description":"abcdefg","ram":true},{ + "name":"aaae","time":1666362084,"children":[{ + "name":"current","description":"You are here!"}]}]},{ + "name":"aab","time":1666361920,"children":[{ + "name":"aaba","time":1666361952,"children":[{ + "name":"aabaa","time":1666361960}]}]},{ + "name":"aac","time":1666361896,"children":[{ + "name":"aaca","time":1666361988,"description":"!@#()\u0026","ram":true,"children":[{ + "name":"aacaa","time":1666362006,"ram":true}]},{ + "name":"aacb","time":1666361977,"children":[{ + "name":"aacba","time":1666362021,"description":"QWERTY"}]},{ + "name":"aacc","time":1666361904,"children":[{ + "name":"aacca","time":1666361910}]}]}]},{ + "name":"bb","time":1666361866,"description":"aA1!","ram":true,"children":[{ + "name":"bba","time":1666362071},{ + "name":"bbb","time":1666362062}]}]`} +} + +func test_FormatSnapshotsList_Output() []string { + return []string{`[{ + "name":"aa","time":1666361849},{ + "name":"aaa","time":1666361866,"ram":true,"parent":"aa"},{ + "name":"aaaa","time":1666362071,"description":"123456","parent":"aaa"},{ + "name":"aaab","time":1666362062,"parent":"aaa"},{ + "name":"aaac","time":1666361873,"parent":"aaa"},{ + "name":"aaad","time":1666361937,"description":"abcdefg","ram":true,"parent":"aaa"},{ + "name":"aaae","time":1666362084,"parent":"aaa"},{ + "name":"current","description":"You are here!","parent":"aaae"},{ + "name":"aab","time":1666361920,"parent":"aa"},{ + "name":"aaba","time":1666361952,"parent":"aab"},{ + "name":"aabaa","time":1666361960,"parent":"aaba"},{ + "name":"aac","time":1666361896,"parent":"aa"},{ + "name":"aaca","time":1666361988,"description":"!@#()\u0026","ram":true,"parent":"aac"},{ + "name":"aacaa","time":1666362006,"ram":true,"parent":"aaca"},{ + "name":"aacb","time":1666361977,"parent":"aac"},{ + "name":"aacba","time":1666362021,"description":"QWERTY","parent":"aacb"},{ + "name":"aacc","time":1666361904,"parent":"aac"},{ + "name":"aacca","time":1666361910,"parent":"aacc"},{ + "name":"bb","time":1666361866,"description":"aA1!","ram":true},{ + "name":"bba","time":1666362071,"parent":"bb"},{ + "name":"bbb","time":1666362062,"parent":"bb"}]`} +} diff --git a/test/cli/Snapshot/Snapshot_0_test.go b/test/cli/Snapshot/Snapshot_0_test.go new file mode 100644 index 00000000..586dd8f3 --- /dev/null +++ b/test/cli/Snapshot/Snapshot_0_test.go @@ -0,0 +1,203 @@ +package cli_snapshot_test + +import ( + "encoding/json" + "testing" + + cliTest "github.com/Telmate/proxmox-api-go/test/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Snapshot_0_GuestQemu_300_Cleanup(t *testing.T) { + Test := cliTest.Test{ + ReqErr: true, + ErrContains: "300", + Args: []string{"-i", "delete", "guest", "300"}, + } + Test.StandardTest(t) +} + +func Test_Snapshot_0_GuestQemu_300_Create(t *testing.T) { + Test := cliTest.Test{ + InputJson: ` +{ + "name": "test-qemu300", + "memory": 128, + "os": "l26", + "cores": 1, + "sockets": 1 +}`, + Expected: "(300)", + Contains: true, + Args: []string{"-i", "create", "guest", "qemu", "300", "pve"}, + } + Test.StandardTest(t) +} + +func Test_Snapshot_0_GuestQemu_300_Start(t *testing.T) { + Test := cliTest.Test{ + Expected: "(300)", + Contains: true, + Args: []string{"-i", "guest", "start", "300"}, + } + Test.StandardTest(t) +} + +// Create a snapshot with all settings populated +func Test_Snapshot_0_Create_Full(t *testing.T) { + Test := cliTest.Test{ + Expected: "(snap00)", + Contains: true, + Args: []string{"-i", "create", "snapshot", "300", "snap00", "description00", "--memory"}, + } + Test.StandardTest(t) +} + +// Check if the snapshot was made properly and the right json structure is returned (tree) +func Test_Snapshot_0_Get_Full(t *testing.T) { + Test := cliTest.Test{ + Return: true, + Args: []string{"-i", "list", "snapshots", "300"}, + } + var data []*snapshot + require.NoError(t, json.Unmarshal(Test.StandardTest(t), &data)) + assert.Equal(t, "snap00", data[0].Name) + assert.Equal(t, "description00", data[0].Description) + assert.Equal(t, true, data[0].VmState) + assert.GreaterOrEqual(t, data[0].SnapTime, uint(0)) +} + +// Remove the description of the snapshot +func Test_Snapshot_0_Update_Description_Empty(t *testing.T) { + Test := cliTest.Test{ + Args: []string{"-i", "update", "snapshot", "300", "snap00", ""}, + } + Test.StandardTest(t) +} + +// Check if description is removed and the right json structure is returned (no tree) +func Test_Snapshot_0_Get_Description_Empty(t *testing.T) { + Test := cliTest.Test{ + NotExpected: "description00", + NotContains: true, + Return: true, + Args: []string{"-i", "list", "snapshots", "300", "--no-tree"}, + } + var data []snapshot + require.NoError(t, json.Unmarshal(Test.StandardTest(t), &data)) +} + +// Create a snapshot with the least settings populated +func Test_Snapshot_0_Create_Empty(t *testing.T) { + // t.(time.Second*120, true) + // time.Sleep(time.Second * 20) + Test := cliTest.Test{ + Expected: "(snap01)", + Contains: true, + Args: []string{"-i", "create", "snapshot", "300", "snap01"}, + } + Test.StandardTest(t) +} + +// Check if the snapshot was made properly +func Test_Snapshot_0_Get_Empty(t *testing.T) { + // time.Sleep(time.Second * 5) + Test := cliTest.Test{ + Return: true, + Args: []string{"-i", "list", "snapshots", "300"}, + } + var data []*snapshot + require.NoError(t, json.Unmarshal(Test.StandardTest(t), &data)) + assert.Equal(t, "snap01", data[0].Children[0].Name) + assert.Equal(t, "", data[0].Children[0].Description) + assert.Equal(t, false, data[0].Children[0].VmState) + assert.GreaterOrEqual(t, data[0].Children[0].SnapTime, uint(0)) +} + +// Add the description to the snapshot +func Test_Snapshot_0_Update_Description_Full(t *testing.T) { + Test := cliTest.Test{ + Args: []string{"-i", "update", "snapshot", "300", "snap01", "description01"}, + } + Test.StandardTest(t) +} + +// Check if description is added +func Test_Snapshot_0_Get_Description_Full(t *testing.T) { + Test := cliTest.Test{ + Expected: "description01", + Contains: true, + Return: true, + Args: []string{"-i", "list", "snapshots", "300"}, + } + var data []*snapshot + require.NoError(t, json.Unmarshal(Test.StandardTest(t), &data)) +} + +// rollback snapshot +func Test_Snapshot_0_Set_Rollback(t *testing.T) { + Test := cliTest.Test{ + Expected: "(snap00)", + Contains: true, + Args: []string{"-i", "guest", "rollback", "300", "snap00"}, + } + Test.StandardTest(t) +} + +// Check if the snapshot was rolled back +func Test_Snapshot_0_Get_Rollback(t *testing.T) { + Test := cliTest.Test{ + Return: true, + Args: []string{"-i", "list", "snapshots", "300", "--no-tree"}, + } + var data []*snapshot + var nofail bool + require.NoError(t, json.Unmarshal(Test.StandardTest(t), &data)) + for _, e := range data { + if e.Name == "current" { + assert.Equal(t, "You are here!", e.Description) + assert.Equal(t, "snap00", e.Parent) + nofail = true + break + } + } + assert.Equal(t, true, nofail) +} + +// delete snapshot +func Test_Snapshot_0_Delete(t *testing.T) { + Test := cliTest.Test{ + Expected: "(snap00)", + Contains: true, + Args: []string{"-i", "delete", "snapshot", "300", "snap00"}, + } + Test.StandardTest(t) +} + +// Check if the snapshot was deleted +func Test_Snapshot_0_Get_Delete(t *testing.T) { + Test := cliTest.Test{ + NotExpected: "snap00", + NotContains: true, + Args: []string{"-i", "list", "snapshots", "300"}, + } + Test.StandardTest(t) +} + +func Test_Snapshot_0_GuestQemu_300_Delete(t *testing.T) { + Test := cliTest.Test{ + ReqErr: false, + Args: []string{"-i", "delete", "guest", "300"}, + } + Test.StandardTest(t) +} + +type snapshot struct { + Name string `json:"name"` + SnapTime uint `json:"time,omitempty"` + Description string `json:"description,omitempty"` + VmState bool `json:"ram,omitempty"` + Children []*snapshot `json:"children,omitempty"` + Parent string `json:"parent,omitempty"` +} diff --git a/test/cli/shared_tests.go b/test/cli/shared_tests.go index 2f45e33a..0bb4e3c1 100644 --- a/test/cli/shared_tests.go +++ b/test/cli/shared_tests.go @@ -25,6 +25,8 @@ type Test struct { ReqErr bool //if an error is expected as output ErrContains string //the string the error should contain + Return bool //if the output should be read and returned for more advanced prcessing + Args []string //cli arguments } @@ -41,7 +43,7 @@ func ListTest(t *testing.T, args []string, expected string) { assert.Contains(t, string(out), expected) } -func (test *Test) StandardTest(t *testing.T) { +func (test *Test) StandardTest(t *testing.T) (out []byte) { SetEnvironmentVariables() cli.RootCmd.SetArgs(test.Args) buffer := new(bytes.Buffer) @@ -58,7 +60,7 @@ func (test *Test) StandardTest(t *testing.T) { require.NoError(t, err) } if test.Expected != "" { - out, _ := io.ReadAll(buffer) + out, _ = io.ReadAll(buffer) if test.Contains { assert.Contains(t, string(out), test.Expected) } else { @@ -66,7 +68,7 @@ func (test *Test) StandardTest(t *testing.T) { } } if test.NotExpected != "" { - out, _ := io.ReadAll(buffer) + out, _ = io.ReadAll(buffer) if test.NotContains { assert.NotContains(t, string(out), test.NotExpected) } else { @@ -74,9 +76,13 @@ func (test *Test) StandardTest(t *testing.T) { } } if test.OutputJson != "" { - out, _ := io.ReadAll(buffer) + out, _ = io.ReadAll(buffer) require.JSONEq(t, test.OutputJson, string(out)) } + if test.Return && len(out) == 0 { + out, _ = io.ReadAll(buffer) + } + return } type LoginTest struct {