diff --git a/README.md b/README.md index 8882209..9309703 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ $ export VULTR_API_KEY=87dFbC91rJjkL/18zJEQxS * Run it ```sh $ vultr version -Client version: 1.12.0 +Client version: 1.13.0 Vultr API endpoint: https://api.vultr.com/ Vultr API version: v1 OS/Arch (client): linux/amd64 @@ -46,7 +46,7 @@ $ export VULTR_API_KEY=89dFbb91rGjkL/12zJEQxS * Run it ```sh $ vultr version -Client version: 1.12.0 +Client version: 1.13.0 Vultr API endpoint: https://api.vultr.com/ Vultr API version: v1 OS/Arch (client): linux/amd64 diff --git a/cmd/commands.go b/cmd/commands.go index be924da..c35221f 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -34,6 +34,9 @@ func (c *CLI) RegisterCommands() { // os c.Command("os", "list all available operating systems", osList) + // applications + c.Command("apps", "list all available applications", appList) + // plans c.Command("plans", "list all active plans", planList) @@ -64,6 +67,10 @@ func (c *CLI) RegisterCommands() { cmd.Command("change", "change operating system of virtual machine (all data will be lost)", serversChangeOS) cmd.Command("list", "show a list of operating systems to which can be changed to", serversListOS) }) + cmd.Command("app", "show and change application on a virtual machine", func(cmd *cli.Cmd) { + cmd.Command("change", "change application of virtual machine (all data will be lost)", serversChangeApplication) + cmd.Command("list", "show a list of available applications to which can be changed to", serversListApplications) + }) cmd.Command("iso", "attach/detach ISO of a virtual machine", func(cmd *cli.Cmd) { cmd.Command("attach", "attach ISO to a virtual machine (server will hard reboot)", serversAttachISO) cmd.Command("detach", "detach ISO from a virtual machine (server will hard reboot)", serversDetachISO) diff --git a/cmd/commands_applications.go b/cmd/commands_applications.go new file mode 100644 index 0000000..0ee752a --- /dev/null +++ b/cmd/commands_applications.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/jawher/mow.cli" +) + +func appList(cmd *cli.Cmd) { + cmd.Action = func() { + apps, err := GetClient().GetApplications() + if err != nil { + log.Fatal(err) + } + + if len(apps) == 0 { + fmt.Println() + return + } + + lengths := []int{8, 32, 24, 32, 12} + tabsPrint(columns{"APPID", "NAME", "SHORT_NAME", "DEPLOY_NAME", "SURCHARGE"}, lengths) + for _, app := range apps { + tabsPrint(columns{app.ID, app.Name, app.ShortName, app.DeployName, app.Surcharge}, lengths) + } + tabsFlush() + } +} diff --git a/cmd/commands_servers.go b/cmd/commands_servers.go index 8f7676b..58a514e 100644 --- a/cmd/commands_servers.go +++ b/cmd/commands_servers.go @@ -27,6 +27,7 @@ func serversCreate(cmd *cli.Cmd) { sshkey := cmd.StringOpt("k sshkey", "", "SSHKEYID (see ) of SSH key to apply to this server on install") hostname := cmd.StringOpt("hostname", "", "Hostname to assign to this server") tag := cmd.StringOpt("tag", "", "Tag to assign to this server") + appID := cmd.StringOpt("a app", "", "If launching an application (OSID 186), this is the APPID to launch") ipv6 := cmd.BoolOpt("ipv6", false, "Assign an IPv6 subnet to this virtual machine (where available)") privateNetworking := cmd.BoolOpt("private-networking", false, "Add private networking support for this virtual machine") autoBackups := cmd.BoolOpt("autobackups", false, "Enable automatic backups for this virtual machine") @@ -44,6 +45,7 @@ func serversCreate(cmd *cli.Cmd) { AutoBackups: *autoBackups, Hostname: *hostname, Tag: *tag, + AppID: *appID, DontNotifyOnActivate: !*notifyActivate, } if *userDataFile != "" { @@ -474,3 +476,37 @@ func reverseIpv4Set(cmd *cli.Cmd) { fmt.Printf("IPv4 reverse DNS set to: %v\n", *entry) } } + +func serversChangeApplication(cmd *cli.Cmd) { + cmd.Spec = "SUBID APPID" + id := cmd.StringArg("SUBID", "", "SUBID of virtual machine (see )") + appID := cmd.StringArg("APPID", "", "Application to use (see )") + cmd.Action = func() { + if err := GetClient().ChangeApplicationofServer(*id, *appID); err != nil { + log.Fatal(err) + } + fmt.Printf("Virtual machine application changed to: %v\n", *appID) + } +} + +func serversListApplications(cmd *cli.Cmd) { + id := cmd.StringArg("SUBID", "", "SUBID of virtual machine (see )") + cmd.Action = func() { + apps, err := GetClient().ListApplicationsforServer(*id) + if err != nil { + log.Fatal(err) + } + + if len(apps) == 0 { + fmt.Println() + return + } + + lengths := []int{8, 32, 24, 32, 12} + tabsPrint(columns{"APPID", "NAME", "SHORT_NAME", "DEPLOY_NAME", "SURCHARGE"}, lengths) + for _, app := range apps { + tabsPrint(columns{app.ID, app.Name, app.ShortName, app.DeployName, app.Surcharge}, lengths) + } + tabsFlush() + } +} diff --git a/lib/applications.go b/lib/applications.go new file mode 100644 index 0000000..f44d8c8 --- /dev/null +++ b/lib/applications.go @@ -0,0 +1,38 @@ +package lib + +import ( + "sort" + "strings" +) + +// Application on Vultr +type Application struct { + ID string `json:"APPID"` + Name string `json:"name"` + ShortName string `json:"short_name"` + DeployName string `json:"deploy_name"` + Surcharge float64 `json:"surcharge"` +} + +type applications []Application + +func (s applications) Len() int { return len(s) } +func (s applications) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s applications) Less(i, j int) bool { + return strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name) +} + +// GetApplications returns a list of all available applications on Vultr +func (c *Client) GetApplications() ([]Application, error) { + var appMap map[string]Application + if err := c.get(`app/list`, &appMap); err != nil { + return nil, err + } + + var appList []Application + for _, app := range appMap { + appList = append(appList, app) + } + sort.Sort(applications(appList)) + return appList, nil +} diff --git a/lib/applications_test.go b/lib/applications_test.go new file mode 100644 index 0000000..376dac4 --- /dev/null +++ b/lib/applications_test.go @@ -0,0 +1,57 @@ +package lib + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Applications_GetApplications_Error(t *testing.T) { + server, client := getTestServerAndClient(http.StatusNotAcceptable, `{error}`) + defer server.Close() + + apps, err := client.GetApplications() + assert.Nil(t, apps) + if assert.NotNil(t, err) { + assert.Equal(t, `{error}`, err.Error()) + } +} + +func Test_Applications_GetApplications_NoApplication(t *testing.T) { + server, client := getTestServerAndClient(http.StatusOK, `[]`) + defer server.Close() + + apps, err := client.GetApplications() + if err != nil { + t.Error(err) + } + assert.Nil(t, apps) +} + +func Test_Applications_GetApplications_OK(t *testing.T) { + server, client := getTestServerAndClient(http.StatusOK, `{ +"2": {"APPID": "2","name": "WordPress","short_name": "wordpress","deploy_name": "WordPress on CentOS 6 x64","surcharge": 0}, +"1": {"APPID": "1","name": "LEMP","short_name": "lemp","deploy_name": "LEMP on CentOS 6 x64","surcharge": 5} +}`) + defer server.Close() + + apps, err := client.GetApplications() + if err != nil { + t.Error(err) + } + if assert.NotNil(t, apps) { + assert.Equal(t, 2, len(apps)) + + assert.Equal(t, "1", apps[0].ID) + assert.Equal(t, "LEMP", apps[0].Name) + assert.Equal(t, "lemp", apps[0].ShortName) + assert.Equal(t, "LEMP on CentOS 6 x64", apps[0].DeployName) + assert.Equal(t, float64(5), apps[0].Surcharge) + + assert.Equal(t, "2", apps[1].ID) + assert.Equal(t, "WordPress", apps[1].Name) + assert.Equal(t, "wordpress", apps[1].ShortName) + assert.Equal(t, "WordPress on CentOS 6 x64", apps[1].DeployName) + } +} diff --git a/lib/client.go b/lib/client.go index 7e2364b..61df58e 100644 --- a/lib/client.go +++ b/lib/client.go @@ -18,7 +18,7 @@ import ( const ( // Version of this libary - Version = "1.12.0" + Version = "1.13.0" // APIVersion of Vultr APIVersion = "v1" diff --git a/lib/servers.go b/lib/servers.go index f5d5448..862b441 100644 --- a/lib/servers.go +++ b/lib/servers.go @@ -38,6 +38,8 @@ type Server struct { KVMUrl string `json:"kvm_url"` AutoBackups string `json:"auto_backups"` Tag string `json:"tag"` + OSID string `json:"OSID"` + AppID string `json:"APPID"` } // ServerOptions are optional parameters to be used during server creation @@ -54,6 +56,7 @@ type ServerOptions struct { DontNotifyOnActivate bool Hostname string Tag string + AppID string } type servers []Server @@ -156,6 +159,18 @@ func (s *Server) UnmarshalJSON(data []byte) (err error) { } s.AllowedBandwidth = ab + value = fmt.Sprintf("%v", fields["OSID"]) + if value == "" { + value = "" + } + s.OSID = value + + value = fmt.Sprintf("%v", fields["APPID"]) + if value == "" { + value = "" + } + s.AppID = value + s.ID = fmt.Sprintf("%v", fields["SUBID"]) s.Name = fmt.Sprintf("%v", fields["label"]) s.OS = fmt.Sprintf("%v", fields["os"]) @@ -292,6 +307,10 @@ func (c *Client) CreateServer(name string, regionID, planID, osID int, options * if options.Tag != "" { values.Add("tag", options.Tag) } + + if options.AppID != "" { + values.Add("APPID", options.AppID) + } } var server Server @@ -465,3 +484,30 @@ func (c *Client) BandwidthOfServer(id string) (bandwidth []map[string]string, er return bandwidth, nil } + +// ChangeApplicationofServer changes the virtual machine to a different application +func (c *Client) ChangeApplicationofServer(id string, appID string) error { + values := url.Values{ + "SUBID": {id}, + "APPID": {appID}, + } + + if err := c.post(`server/app_change`, values, nil); err != nil { + return err + } + return nil +} + +// ListApplicationsforServer lists all available operating systems to which an existing virtual machine can be changed +func (c *Client) ListApplicationsforServer(id string) (apps []Application, err error) { + var appMap map[string]Application + if err := c.get(`server/app_change_list?SUBID=`+id, &appMap); err != nil { + return nil, err + } + + for _, app := range appMap { + apps = append(apps, app) + } + sort.Sort(applications(apps)) + return apps, nil +} diff --git a/lib/servers_test.go b/lib/servers_test.go index 71db4ab..629116b 100644 --- a/lib/servers_test.go +++ b/lib/servers_test.go @@ -37,7 +37,7 @@ func Test_Servers_GetServers_OK(t *testing.T) { "netmask_v4":"255.255.254.0","gateway_v4":"192.168.1.1","power_status":"down","VPSPLANID":"31", "v6_networks": [{"v6_network": "2002:DB9:1000::", "v6_main_ip": "2000:DB8:1000::0000", "v6_network_size": "32" }], "label":"test beta","internal_ip":"10.10.10.10", - "kvm_url":"https:\/\/my.vultr.com\/subs\/vps\/novnc\/api.php?data=456","auto_backups":"yes"}, + "kvm_url":"https:\/\/my.vultr.com\/subs\/vps\/novnc\/api.php?data=456","auto_backups":"yes", "OSID": "127", "APPID": "0"}, "9753721":{"SUBID":"9753721","os":"Ubuntu 14.04 x64","ram":"768 MB","disk":"Virtual 15 GB","main_ip":"123.456.789.0", "vcpu_count":"2","location":"Frankfurt","DCID":"9","default_password":"oops!","date_created":"2017-07-07 07:07:07", "pending_charges":0.04,"status":"active","cost_per_month":"5.00","current_bandwidth_gb":7,"allowed_bandwidth_gb":"1000", @@ -69,6 +69,8 @@ func Test_Servers_GetServers_OK(t *testing.T) { assert.Equal(t, 0, len(servers[0].V6Networks)) assert.Equal(t, 7.0, servers[0].CurrentBandwidth) assert.Equal(t, 1000.0, servers[0].AllowedBandwidth) + assert.Equal(t, "", servers[0].OSID) + assert.Equal(t, "", servers[0].AppID) assert.Equal(t, "789032", servers[1].ID) assert.Equal(t, "test beta", servers[1].Name) @@ -84,6 +86,8 @@ func Test_Servers_GetServers_OK(t *testing.T) { assert.Equal(t, "10.10.10.10", servers[1].InternalIP) assert.Equal(t, `https://my.vultr.com/subs/vps/novnc/api.php?data=456`, servers[1].KVMUrl) assert.Equal(t, "yes", servers[1].AutoBackups) + assert.Equal(t, "127", servers[1].OSID) + assert.Equal(t, "0", servers[1].AppID) } } @@ -495,3 +499,69 @@ func Test_Servers_BandwidthOfServer_OK(t *testing.T) { assert.Equal(t, "2455005", bandwidth[2]["outgoing"]) } } + +func Test_Servers_ChangeApplicationofServer_Error(t *testing.T) { + server, client := getTestServerAndClient(http.StatusNotAcceptable, `{error}`) + defer server.Close() + + err := client.ChangeApplicationofServer("123456789", "3") + if assert.NotNil(t, err) { + assert.Equal(t, `{error}`, err.Error()) + } +} + +func Test_Servers_ChangeApplicationofServer_OK(t *testing.T) { + server, client := getTestServerAndClient(http.StatusOK, `{no-response?!}`) + defer server.Close() + + assert.Nil(t, client.ChangeApplicationofServer("123456789", "3")) +} + +func Test_Servers_ListApplicationsforServer_Error(t *testing.T) { + server, client := getTestServerAndClient(http.StatusNotAcceptable, `{error}`) + defer server.Close() + + apps, err := client.ListApplicationsforServer("123456789") + assert.Nil(t, apps) + if assert.NotNil(t, err) { + assert.Equal(t, `{error}`, err.Error()) + } +} + +func Test_Servers_ListApplicationsforServer_NoOS(t *testing.T) { + server, client := getTestServerAndClient(http.StatusOK, `[]`) + defer server.Close() + + apps, err := client.ListApplicationsforServer("123456789") + if err != nil { + t.Error(err) + } + assert.Nil(t, apps) +} + +func Test_Servers_ListApplicationsforServer_OK(t *testing.T) { + server, client := getTestServerAndClient(http.StatusOK, `{ +"2": {"APPID": "2","name": "WordPress","short_name": "wordpress","deploy_name": "WordPress on CentOS 6 x64","surcharge": 0}, +"1": {"APPID": "1","name": "LEMP","short_name": "lemp","deploy_name": "LEMP on CentOS 6 x64","surcharge": 5} +}`) + defer server.Close() + + apps, err := client.ListApplicationsforServer("123456789") + if err != nil { + t.Error(err) + } + if assert.NotNil(t, apps) { + assert.Equal(t, 2, len(apps)) + + assert.Equal(t, "1", apps[0].ID) + assert.Equal(t, "LEMP", apps[0].Name) + assert.Equal(t, "lemp", apps[0].ShortName) + assert.Equal(t, "LEMP on CentOS 6 x64", apps[0].DeployName) + assert.Equal(t, float64(5), apps[0].Surcharge) + + assert.Equal(t, "2", apps[1].ID) + assert.Equal(t, "WordPress", apps[1].Name) + assert.Equal(t, "wordpress", apps[1].ShortName) + assert.Equal(t, "WordPress on CentOS 6 x64", apps[1].DeployName) + } +}