diff --git a/README.md b/README.md
index ccd088381..79404556a 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ Currently supported DNS providers:
- Name.com
- Route 53
- SoftLayer
+ - Vultr
At Stack Overflow, we use this system to manage hundreds of domains
and subdomains across multiple registrars and DNS providers.
diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html
index 2c1740e19..a96235369 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -18,6 +18,7 @@
NS1 |
ROUTE53 |
SOFTLAYER |
+ VULTR |
@@ -59,6 +60,9 @@
|
+
+
+ |
@@ -98,6 +102,9 @@
|
+
+
+ |
@@ -137,6 +144,9 @@
|
+
+
+ |
@@ -162,6 +172,9 @@
|
+
+
+ |
@@ -197,6 +210,9 @@
|
+
+
+ |
@@ -228,6 +244,9 @@
|
+
+
+ |
@@ -253,6 +272,9 @@
|
+
+
+ |
@@ -272,6 +294,9 @@
|
|
|
+
+
+ |
@@ -305,6 +330,7 @@
|
+ |
@@ -344,6 +370,9 @@
|
+
+
+ |
@@ -383,6 +412,9 @@
|
+
+
+ |
diff --git a/docs/_providers/vultr.md b/docs/_providers/vultr.md
new file mode 100644
index 000000000..04b3b2e63
--- /dev/null
+++ b/docs/_providers/vultr.md
@@ -0,0 +1,39 @@
+---
+name: Vultr
+title: Vultr Provider
+layout: default
+jsId: VULTR
+---
+# Vultr Provider
+
+## Configuration
+
+In your providers config json file you must include a Vultr personal access token:
+
+{% highlight json %}
+{
+ "vultr":{
+ "token": "your-vultr-personal-access-token"
+ }
+}
+{% endhighlight %}
+
+## Metadata
+
+This provider does not recognize any special metadata fields unique to Vultr.
+
+## Usage
+
+Example javascript:
+
+{% highlight js %}
+var VULTR = NewDnsProvider("vultr", "VULTR");
+
+D("example.tld", REG_DNSIMPLE, DnsProvider(VULTR),
+ A("test","1.2.3.4")
+);
+{% endhighlight %}
+
+## Activation
+
+Vultr depends on a Vultr personal access token.
diff --git a/docs/provider-list.md b/docs/provider-list.md
index 277c34833..7da093947 100644
--- a/docs/provider-list.md
+++ b/docs/provider-list.md
@@ -66,6 +66,7 @@ Maintainers of contributed providers:
* namecheap @captncraig
* ns1 @captncraig
* OVH @Oprax
+* Vultr @geek1011
### Requested providers
diff --git a/integrationTest/providers.json b/integrationTest/providers.json
index 3ef449fe8..9acac3f9c 100644
--- a/integrationTest/providers.json
+++ b/integrationTest/providers.json
@@ -62,5 +62,9 @@
"domain": "$SL_DOMAIN",
"username": "$SL_USERNAME",
"api_key": "$SL_API_KEY"
+ },
+ "VULTR": {
+ "token": "$VULTR_TOKEN",
+ "domain": "$VULTR_DOMAIN"
}
}
diff --git a/providers/_all/all.go b/providers/_all/all.go
index 7244eab1b..f1c9a8d7c 100644
--- a/providers/_all/all.go
+++ b/providers/_all/all.go
@@ -15,4 +15,5 @@ import (
_ "github.com/StackExchange/dnscontrol/providers/ns1"
_ "github.com/StackExchange/dnscontrol/providers/route53"
_ "github.com/StackExchange/dnscontrol/providers/softlayer"
+ _ "github.com/StackExchange/dnscontrol/providers/vultr"
)
diff --git a/providers/vultr/convert_test.go b/providers/vultr/convert_test.go
new file mode 100644
index 000000000..ec4cff291
--- /dev/null
+++ b/providers/vultr/convert_test.go
@@ -0,0 +1,73 @@
+package vultr
+
+import (
+ "testing"
+
+ vultr "github.com/JamesClonk/vultr/lib"
+ "github.com/StackExchange/dnscontrol/models"
+)
+
+func TestConversion(t *testing.T) {
+ dc := &models.DomainConfig{
+ Name: "example.com",
+ }
+
+ records := []*vultr.DNSRecord{
+ {
+ Type: "A",
+ Name: "",
+ Data: "127.0.0.1",
+ TTL: 300,
+ },
+ {
+ Type: "CNAME",
+ Name: "*",
+ Data: "example.com",
+ TTL: 300,
+ },
+ {
+ Type: "SRV",
+ Name: "_ssh_.tcp",
+ Data: "5 22 ssh.example.com",
+ Priority: 5,
+ TTL: 300,
+ },
+ {
+ Type: "MX",
+ Name: "",
+ Data: "mail.example.com",
+ TTL: 300,
+ },
+ {
+ Type: "NS",
+ Name: "",
+ Data: "ns1.example.net",
+ TTL: 300,
+ },
+ {
+ Type: "TXT",
+ Name: "test",
+ Data: "\"testasd asda sdas dasd\"",
+ TTL: 300,
+ },
+ {
+ Type: "CAA",
+ Name: "testasd",
+ Data: "0 issue \"test.example.net\"",
+ TTL: 300,
+ },
+ }
+
+ for _, record := range records {
+ rc, err := toRecordConfig(dc, record)
+ if err != nil {
+ t.Error("Error converting Vultr record", record)
+ }
+
+ converted := toVultrRecord(dc, rc)
+
+ if converted.Type != record.Type || converted.Name != record.Name || converted.Data != record.Data || converted.Priority != record.Priority || converted.TTL != record.TTL {
+ t.Error("Vultr record conversion mismatch", record, rc, converted)
+ }
+ }
+}
diff --git a/providers/vultr/vultr.go b/providers/vultr/vultr.go
new file mode 100644
index 000000000..b6d74bf95
--- /dev/null
+++ b/providers/vultr/vultr.go
@@ -0,0 +1,333 @@
+package vultr
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/models"
+ "github.com/StackExchange/dnscontrol/providers"
+ "github.com/StackExchange/dnscontrol/providers/diff"
+ "github.com/miekg/dns/dnsutil"
+
+ vultr "github.com/JamesClonk/vultr/lib"
+)
+
+/*
+
+Vultr API DNS provider:
+
+Info required in `creds.json`:
+ - token
+
+*/
+
+var docNotes = providers.DocumentationNotes{
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanUseAlias: providers.Cannot(),
+ providers.CanUseTLSA: providers.Cannot(),
+ providers.CanUsePTR: providers.Cannot(),
+}
+
+func init() {
+ providers.RegisterDomainServiceProviderType("VULTR", NewVultr, providers.CanUseSRV, providers.CanUseCAA, docNotes)
+}
+
+// VultrApi represents the Vultr DNSServiceProvider
+type VultrApi struct {
+ client *vultr.Client
+ token string
+}
+
+// defaultNS are the default nameservers for Vultr
+var defaultNS = []string{
+ "ns1.vultr.com",
+ "ns2.vultr.com",
+}
+
+// NewVultr initializes a Vultr DNSServiceProvider
+func NewVultr(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ api := &VultrApi{
+ token: m["token"],
+ }
+
+ if api.token == "" {
+ return nil, fmt.Errorf("Vultr API token is required")
+ }
+
+ api.client = vultr.NewClient(api.token, nil)
+
+ // Validate token
+ _, err := api.client.GetAccountInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ return api, nil
+}
+
+// GetDomainCorrections gets the corrections for a DomainConfig
+func (api *VultrApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ dc.Punycode()
+
+ ok, err := api.isDomainInAccount(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ if !ok {
+ return nil, fmt.Errorf("%s is not a domain in the Vultr account", dc.Name)
+ }
+
+ records, err := api.client.GetDNSRecords(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ curRecords := make([]*models.RecordConfig, len(records))
+ for i := range records {
+ r, err := toRecordConfig(dc, &records[i])
+ if err != nil {
+ return nil, err
+ }
+
+ curRecords[i] = r
+ }
+
+ differ := diff.New(dc)
+ _, create, delete, modify := differ.IncrementalDiff(curRecords)
+
+ corrections := []*models.Correction{}
+
+ for _, mod := range delete {
+ id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
+ corrections = append(corrections, &models.Correction{
+ Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
+ F: func() error {
+ return api.client.DeleteDNSRecord(dc.Name, id)
+ },
+ })
+ }
+
+ for _, mod := range create {
+ r := toVultrRecord(dc, mod.Desired)
+ corrections = append(corrections, &models.Correction{
+ Msg: mod.String(),
+ F: func() error {
+ return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL)
+ },
+ })
+ }
+
+ for _, mod := range modify {
+ id := mod.Existing.Original.(*vultr.DNSRecord).RecordID
+ r := toVultrRecord(dc, mod.Desired)
+ r.RecordID = id
+ corrections = append(corrections, &models.Correction{
+ Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),
+ F: func() error {
+ return api.client.UpdateDNSRecord(dc.Name, *r)
+ },
+ })
+ }
+
+ return corrections, nil
+}
+
+// GetNameservers gets the Vultr nameservers for a domain
+func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ return models.StringsToNameservers(defaultNS), nil
+}
+
+// EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist
+func (api *VultrApi) EnsureDomainExists(domain string) error {
+ ok, err := api.isDomainInAccount(domain)
+ if err != nil {
+ return err
+ }
+
+ if !ok {
+ // Vultr requires an initial IP, use a dummy one
+ err := api.client.CreateDNSDomain(domain, "127.0.0.1")
+ if err != nil {
+ return err
+ }
+
+ ok, err := api.isDomainInAccount(domain)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("Unexpected error adding domain %s to Vultr account", domain)
+ }
+ }
+
+ return nil
+}
+
+func (api *VultrApi) isDomainInAccount(domain string) (bool, error) {
+ domains, err := api.client.GetDNSDomains()
+ if err != nil {
+ return false, err
+ }
+
+ var vd *vultr.DNSDomain
+ for _, d := range domains {
+ if d.Domain == domain {
+ vd = &d
+ }
+ }
+
+ if vd == nil {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations
+func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) {
+ // Turns r.Name into a FQDN
+ // Vultr uses "" as the apex domain, instead of "@", and this handles it fine.
+ name := dnsutil.AddOrigin(r.Name, dc.Name)
+
+ data := r.Data
+ // Make target into a FQDN if it is a CNAME, NS, MX, or SRV
+ if r.Type == "CNAME" || r.Type == "NS" || r.Type == "MX" {
+ if !strings.HasSuffix(data, ".") {
+ data = data + "."
+ }
+ data = dnsutil.AddOrigin(data, dc.Name)
+ }
+ // Remove quotes if it is a TXT
+ if r.Type == "TXT" {
+ if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
+ return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
+ }
+ data = data[1 : len(data)-1]
+ }
+
+ rc := &models.RecordConfig{
+ NameFQDN: name,
+ Type: r.Type,
+ Target: data,
+ TTL: uint32(r.TTL),
+ Original: r,
+ }
+
+ if r.Type == "MX" {
+ rc.MxPreference = uint16(r.Priority)
+ }
+
+ if r.Type == "SRV" {
+ rc.SrvPriority = uint16(r.Priority)
+
+ // Vultr returns in the format "[weight] [port] [target]"
+ splitData := strings.SplitN(rc.Target, " ", 3)
+ if len(splitData) != 3 {
+ return nil, fmt.Errorf("Unexpected data for SRV record returned by Vultr")
+ }
+
+ weight, err := strconv.ParseUint(splitData[0], 10, 16)
+ if err != nil {
+ return nil, err
+ }
+ rc.SrvWeight = uint16(weight)
+
+ port, err := strconv.ParseUint(splitData[1], 10, 16)
+ if err != nil {
+ return nil, err
+ }
+ rc.SrvPort = uint16(port)
+
+ target := splitData[2]
+ if !strings.HasSuffix(target, ".") {
+ target = target + "."
+ }
+ rc.Target = dnsutil.AddOrigin(target, dc.Name)
+ }
+
+ if r.Type == "CAA" {
+ // Vultr returns in the format "[flag] [tag] [value]"
+ splitData := strings.SplitN(rc.Target, " ", 3)
+ if len(splitData) != 3 {
+ return nil, fmt.Errorf("Unexpected data for CAA record returned by Vultr")
+ }
+
+ flag, err := strconv.ParseUint(splitData[0], 10, 8)
+ if err != nil {
+ return nil, err
+ }
+ rc.CaaFlag = uint8(flag)
+
+ rc.CaaTag = splitData[1]
+
+ value := splitData[2]
+ if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
+ value = value[1 : len(value)-1]
+ }
+ if strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`) {
+ value = value[1 : len(value)-1]
+ }
+ rc.Target = value
+ }
+
+ return rc, nil
+}
+
+// toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations
+func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord {
+ name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
+
+ // Vultr uses a blank string to represent the apex domain
+ if name == "@" {
+ name = ""
+ }
+
+ data := rc.Target
+
+ // Vultr does not use a period suffix for the server for CNAME, NS, or MX
+ if strings.HasSuffix(data, ".") {
+ data = data[:len(data)-1]
+ }
+ // Vultr needs TXT record in quotes
+ if rc.Type == "TXT" {
+ data = fmt.Sprintf(`"%s"`, data)
+ }
+
+ priority := 0
+
+ if rc.Type == "MX" {
+ priority = int(rc.MxPreference)
+ }
+
+ if rc.Type == "SRV" {
+ priority = int(rc.SrvPriority)
+ }
+
+ r := &vultr.DNSRecord{
+ Type: rc.Type,
+ Name: name,
+ Data: data,
+ TTL: int(rc.TTL),
+ Priority: priority,
+ }
+
+ if rc.Type == "SRV" {
+ target := rc.Target
+ if strings.HasSuffix(target, ".") {
+ target = target[:len(target)-1]
+ }
+
+ r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target)
+ }
+
+ if rc.Type == "CAA" {
+ r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.Target)
+ }
+
+ return r
+}
diff --git a/vendor/github.com/JamesClonk/vultr/LICENSE b/vendor/github.com/JamesClonk/vultr/LICENSE
new file mode 100644
index 000000000..be20a1280
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Fabio Berchtold
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/JamesClonk/vultr/lib/account_info.go b/vendor/github.com/JamesClonk/vultr/lib/account_info.go
new file mode 100644
index 000000000..f994f9242
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/account_info.go
@@ -0,0 +1,71 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+)
+
+// AccountInfo of Vultr account
+type AccountInfo struct {
+ Balance float64 `json:"balance"`
+ PendingCharges float64 `json:"pending_charges"`
+ LastPaymentDate string `json:"last_payment_date"`
+ LastPaymentAmount float64 `json:"last_payment_amount"`
+}
+
+// GetAccountInfo retrieves the Vultr account information about current balance, pending charges, etc..
+func (c *Client) GetAccountInfo() (info AccountInfo, err error) {
+ if err := c.get(`account/info`, &info); err != nil {
+ return AccountInfo{}, err
+ }
+ return
+}
+
+// UnmarshalJSON implements json.Unmarshaller on AccountInfo.
+// This is needed because the Vultr API is inconsistent in it's JSON responses for account info.
+// Some fields can change type, from JSON number to JSON string and vice-versa.
+func (a *AccountInfo) UnmarshalJSON(data []byte) (err error) {
+ if a == nil {
+ *a = AccountInfo{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ value := fmt.Sprintf("%v", fields["balance"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ b, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ a.Balance = b
+
+ value = fmt.Sprintf("%v", fields["pending_charges"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ pc, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ a.PendingCharges = pc
+
+ value = fmt.Sprintf("%v", fields["last_payment_amount"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ lpa, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ a.LastPaymentAmount = lpa
+
+ a.LastPaymentDate = fmt.Sprintf("%v", fields["last_payment_date"])
+
+ return
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/applications.go b/vendor/github.com/JamesClonk/vultr/lib/applications.go
new file mode 100644
index 000000000..f44d8c81e
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/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/vendor/github.com/JamesClonk/vultr/lib/block_storage.go b/vendor/github.com/JamesClonk/vultr/lib/block_storage.go
new file mode 100644
index 000000000..e9e8e115d
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/block_storage.go
@@ -0,0 +1,210 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// BlockStorage on Vultr account
+type BlockStorage struct {
+ ID string `json:"SUBID,string"`
+ Name string `json:"label"`
+ RegionID int `json:"DCID,string"`
+ SizeGB int `json:"size_gb,string"`
+ Created string `json:"date_created"`
+ Cost string `json:"cost_per_month"`
+ Status string `json:"status"`
+ AttachedTo string `json:"attached_to_SUBID"`
+}
+
+type blockstorages []BlockStorage
+
+func (b blockstorages) Len() int { return len(b) }
+func (b blockstorages) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b blockstorages) Less(i, j int) bool {
+ // sort order: name, size, status
+ if strings.ToLower(b[i].Name) < strings.ToLower(b[j].Name) {
+ return true
+ } else if strings.ToLower(b[i].Name) > strings.ToLower(b[j].Name) {
+ return false
+ }
+ if b[i].SizeGB < b[j].SizeGB {
+ return true
+ } else if b[i].SizeGB > b[j].SizeGB {
+ return false
+ }
+ return b[i].Status < b[j].Status
+}
+
+// UnmarshalJSON implements json.Unmarshaller on BlockStorage.
+// This is needed because the Vultr API is inconsistent in it's JSON responses.
+// Some fields can change type, from JSON number to JSON string and vice-versa.
+func (b *BlockStorage) UnmarshalJSON(data []byte) (err error) {
+ if b == nil {
+ *b = BlockStorage{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ value := fmt.Sprintf("%v", fields["SUBID"])
+ if len(value) == 0 || value == "" || value == "0" {
+ b.ID = ""
+ } else {
+ id, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ b.ID = strconv.FormatFloat(id, 'f', -1, 64)
+ }
+
+ value = fmt.Sprintf("%v", fields["DCID"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ region, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ b.RegionID = int(region)
+
+ value = fmt.Sprintf("%v", fields["size_gb"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ size, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ b.SizeGB = int(size)
+
+ value = fmt.Sprintf("%v", fields["attached_to_SUBID"])
+ if len(value) == 0 || value == "" || value == "0" {
+ b.AttachedTo = ""
+ } else {
+ attached, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ b.AttachedTo = strconv.FormatFloat(attached, 'f', -1, 64)
+ }
+
+ b.Name = fmt.Sprintf("%v", fields["label"])
+ b.Created = fmt.Sprintf("%v", fields["date_created"])
+ b.Status = fmt.Sprintf("%v", fields["status"])
+ b.Cost = fmt.Sprintf("%v", fields["cost_per_month"])
+
+ return
+}
+
+// GetBlockStorages returns a list of all active block storages on Vultr account
+func (c *Client) GetBlockStorages() (storages []BlockStorage, err error) {
+ if err := c.get(`block/list`, &storages); err != nil {
+ return nil, err
+ }
+ sort.Sort(blockstorages(storages))
+ return storages, nil
+}
+
+// GetBlockStorage returns block storage with given ID
+func (c *Client) GetBlockStorage(id string) (BlockStorage, error) {
+ storages, err := c.GetBlockStorages()
+ if err != nil {
+ return BlockStorage{}, err
+ }
+
+ for _, s := range storages {
+ if s.ID == id {
+ return s, nil
+ }
+ }
+ return BlockStorage{}, fmt.Errorf("BlockStorage with ID %v not found", id)
+}
+
+// CreateBlockStorage creates a new block storage on Vultr account
+func (c *Client) CreateBlockStorage(name string, regionID, size int) (BlockStorage, error) {
+ values := url.Values{
+ "label": {name},
+ "DCID": {fmt.Sprintf("%v", regionID)},
+ "size_gb": {fmt.Sprintf("%v", size)},
+ }
+
+ var storage BlockStorage
+ if err := c.post(`block/create`, values, &storage); err != nil {
+ return BlockStorage{}, err
+ }
+ storage.RegionID = regionID
+ storage.Name = name
+ storage.SizeGB = size
+
+ return storage, nil
+}
+
+// ResizeBlockStorage resizes an existing block storage
+func (c *Client) ResizeBlockStorage(id string, size int) error {
+ values := url.Values{
+ "SUBID": {id},
+ "size_gb": {fmt.Sprintf("%v", size)},
+ }
+
+ if err := c.post(`block/resize`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// LabelBlockStorage changes the label on an existing block storage
+func (c *Client) LabelBlockStorage(id, name string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "label": {name},
+ }
+
+ if err := c.post(`block/label_set`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// AttachBlockStorage attaches block storage to an existing virtual machine
+func (c *Client) AttachBlockStorage(id, serverID string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "attach_to_SUBID": {serverID},
+ }
+
+ if err := c.post(`block/attach`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DetachBlockStorage detaches block storage from virtual machine
+func (c *Client) DetachBlockStorage(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`block/detach`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteBlockStorage deletes an existing block storage
+func (c *Client) DeleteBlockStorage(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`block/delete`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/client.go b/vendor/github.com/JamesClonk/vultr/lib/client.go
new file mode 100644
index 000000000..0f805a10c
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/client.go
@@ -0,0 +1,249 @@
+package lib
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/juju/ratelimit"
+)
+
+const (
+ // Version of this libary
+ Version = "1.13.0"
+
+ // APIVersion of Vultr
+ APIVersion = "v1"
+
+ // DefaultEndpoint to be used
+ DefaultEndpoint = "https://api.vultr.com/"
+
+ mediaType = "application/json"
+)
+
+// retryableStatusCodes are API response status codes that indicate that
+// the failed request can be retried without further actions.
+var retryableStatusCodes = map[int]struct{}{
+ 503: {}, // Rate limit hit
+ 500: {}, // Internal server error. Try again at a later time.
+}
+
+// Client represents the Vultr API client
+type Client struct {
+ // HTTP client for communication with the Vultr API
+ client *http.Client
+
+ // User agent for HTTP client
+ UserAgent string
+
+ // Endpoint URL for API requests
+ Endpoint *url.URL
+
+ // API key for accessing the Vultr API
+ APIKey string
+
+ // Max. number of request attempts
+ MaxAttempts int
+
+ // Throttling struct
+ bucket *ratelimit.Bucket
+}
+
+// Options represents optional settings and flags that can be passed to NewClient
+type Options struct {
+ // HTTP client for communication with the Vultr API
+ HTTPClient *http.Client
+
+ // User agent for HTTP client
+ UserAgent string
+
+ // Endpoint URL for API requests
+ Endpoint string
+
+ // API rate limitation, calls per duration
+ RateLimitation time.Duration
+
+ // Max. number of times to retry API calls
+ MaxRetries int
+}
+
+// NewClient creates new Vultr API client. Options are optional and can be nil.
+func NewClient(apiKey string, options *Options) *Client {
+ userAgent := "vultr-go/" + Version
+ transport := &http.Transport{
+ TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
+ }
+ client := http.DefaultClient
+ client.Transport = transport
+ endpoint, _ := url.Parse(DefaultEndpoint)
+ rate := 505 * time.Millisecond
+ attempts := 1
+
+ if options != nil {
+ if options.HTTPClient != nil {
+ client = options.HTTPClient
+ }
+ if options.UserAgent != "" {
+ userAgent = options.UserAgent
+ }
+ if options.Endpoint != "" {
+ endpoint, _ = url.Parse(options.Endpoint)
+ }
+ if options.RateLimitation != 0 {
+ rate = options.RateLimitation
+ }
+ if options.MaxRetries != 0 {
+ attempts = options.MaxRetries + 1
+ }
+ }
+
+ return &Client{
+ UserAgent: userAgent,
+ client: client,
+ Endpoint: endpoint,
+ APIKey: apiKey,
+ MaxAttempts: attempts,
+ bucket: ratelimit.NewBucket(rate, 1),
+ }
+}
+
+func apiPath(path string) string {
+ return fmt.Sprintf("/%s/%s", APIVersion, path)
+}
+
+func apiKeyPath(path, apiKey string) string {
+ if strings.Contains(path, "?") {
+ return path + "&api_key=" + apiKey
+ }
+ return path + "?api_key=" + apiKey
+}
+
+func (c *Client) get(path string, data interface{}) error {
+ req, err := c.newRequest("GET", apiPath(path), nil)
+ if err != nil {
+ return err
+ }
+ return c.do(req, data)
+}
+
+func (c *Client) post(path string, values url.Values, data interface{}) error {
+ req, err := c.newRequest("POST", apiPath(path), strings.NewReader(values.Encode()))
+ if err != nil {
+ return err
+ }
+ return c.do(req, data)
+}
+
+func (c *Client) newRequest(method string, path string, body io.Reader) (*http.Request, error) {
+ relPath, err := url.Parse(apiKeyPath(path, c.APIKey))
+ if err != nil {
+ return nil, err
+ }
+
+ url := c.Endpoint.ResolveReference(relPath)
+
+ req, err := http.NewRequest(method, url.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("User-Agent", c.UserAgent)
+ req.Header.Add("Accept", mediaType)
+
+ if req.Method == "POST" {
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ }
+ return req, nil
+}
+
+func (c *Client) do(req *http.Request, data interface{}) error {
+ // Throttle http requests to avoid hitting Vultr's API rate-limit
+ c.bucket.Wait(1)
+
+ // Request body gets drained on each read so we
+ // need to save it's content for retrying requests
+ var err error
+ var requestBody []byte
+ if req.Body != nil {
+ requestBody, err = ioutil.ReadAll(req.Body)
+ if err != nil {
+ return fmt.Errorf("Error reading request body: %v", err)
+ }
+ req.Body.Close()
+ }
+
+ var apiError error
+ for tryCount := 1; tryCount <= c.MaxAttempts; tryCount++ {
+ // Restore request body to the original state
+ if requestBody != nil {
+ req.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
+ }
+
+ resp, err := c.client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode == http.StatusOK {
+ if data != nil {
+ // avoid unmarshalling problem because Vultr API returns
+ // empty array instead of empty map when it shouldn't!
+ if string(body) == `[]` {
+ data = nil
+ } else {
+ if err := json.Unmarshal(body, data); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ }
+
+ apiError = errors.New(string(body))
+ if !isCodeRetryable(resp.StatusCode) {
+ break
+ }
+
+ delay := backoffDuration(tryCount)
+ time.Sleep(delay)
+ }
+
+ return apiError
+}
+
+// backoffDuration returns the duration to wait before retrying the request.
+// Duration is an exponential function of the retry count with a jitter of ~0-30%.
+func backoffDuration(retryCount int) time.Duration {
+ // Upper limit of delay at ~1 minute
+ if retryCount > 7 {
+ retryCount = 7
+ }
+
+ rand.Seed(time.Now().UnixNano())
+ delay := (1 << uint(retryCount)) * (rand.Intn(150) + 500)
+ return time.Duration(delay) * time.Millisecond
+}
+
+// isCodeRetryable returns true if the given status code means that we should retry.
+func isCodeRetryable(statusCode int) bool {
+ if _, ok := retryableStatusCodes[statusCode]; ok {
+ return true
+ }
+
+ return false
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/dns.go b/vendor/github.com/JamesClonk/vultr/lib/dns.go
new file mode 100644
index 000000000..e66275b68
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/dns.go
@@ -0,0 +1,150 @@
+package lib
+
+import (
+ "fmt"
+ "net/url"
+ "sort"
+ "strings"
+)
+
+// DNSDomain represents a DNS domain on Vultr
+type DNSDomain struct {
+ Domain string `json:"domain"`
+ Created string `json:"date_created"`
+}
+
+type dnsdomains []DNSDomain
+
+func (d dnsdomains) Len() int { return len(d) }
+func (d dnsdomains) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
+func (d dnsdomains) Less(i, j int) bool {
+ return strings.ToLower(d[i].Domain) < strings.ToLower(d[j].Domain)
+}
+
+// DNSRecord represents a DNS record on Vultr
+type DNSRecord struct {
+ RecordID int `json:"RECORDID"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Data string `json:"data"`
+ Priority int `json:"priority"`
+ TTL int `json:"ttl"`
+}
+
+type dnsrecords []DNSRecord
+
+func (d dnsrecords) Len() int { return len(d) }
+func (d dnsrecords) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
+func (d dnsrecords) Less(i, j int) bool {
+ // sort order: type, data, name
+ if d[i].Type < d[j].Type {
+ return true
+ } else if d[i].Type > d[j].Type {
+ return false
+ }
+ if d[i].Data < d[j].Data {
+ return true
+ } else if d[i].Data > d[j].Data {
+ return false
+ }
+ return strings.ToLower(d[i].Name) < strings.ToLower(d[j].Name)
+}
+
+// GetDNSDomains returns a list of available domains on Vultr account
+func (c *Client) GetDNSDomains() (domains []DNSDomain, err error) {
+ if err := c.get(`dns/list`, &domains); err != nil {
+ return nil, err
+ }
+ sort.Sort(dnsdomains(domains))
+ return domains, nil
+}
+
+// GetDNSRecords returns a list of all DNS records of a particular domain
+func (c *Client) GetDNSRecords(domain string) (records []DNSRecord, err error) {
+ if err := c.get(`dns/records?domain=`+domain, &records); err != nil {
+ return nil, err
+ }
+ sort.Sort(dnsrecords(records))
+ return records, nil
+}
+
+// CreateDNSDomain creates a new DNS domain name on Vultr
+func (c *Client) CreateDNSDomain(domain, serverIP string) error {
+ values := url.Values{
+ "domain": {domain},
+ "serverip": {serverIP},
+ }
+
+ if err := c.post(`dns/create_domain`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteDNSDomain deletes an existing DNS domain name
+func (c *Client) DeleteDNSDomain(domain string) error {
+ values := url.Values{
+ "domain": {domain},
+ }
+
+ if err := c.post(`dns/delete_domain`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// CreateDNSRecord creates a new DNS record
+func (c *Client) CreateDNSRecord(domain, name, rtype, data string, priority, ttl int) error {
+ values := url.Values{
+ "domain": {domain},
+ "name": {name},
+ "type": {rtype},
+ "data": {data},
+ "priority": {fmt.Sprintf("%v", priority)},
+ "ttl": {fmt.Sprintf("%v", ttl)},
+ }
+
+ if err := c.post(`dns/create_record`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// UpdateDNSRecord updates an existing DNS record
+func (c *Client) UpdateDNSRecord(domain string, dnsrecord DNSRecord) error {
+ values := url.Values{
+ "domain": {domain},
+ "RECORDID": {fmt.Sprintf("%v", dnsrecord.RecordID)},
+ }
+
+ if dnsrecord.Name != "" {
+ values.Add("name", dnsrecord.Name)
+ }
+ if dnsrecord.Data != "" {
+ values.Add("data", dnsrecord.Data)
+ }
+ if dnsrecord.Priority != 0 {
+ values.Add("priority", fmt.Sprintf("%v", dnsrecord.Priority))
+ }
+ if dnsrecord.TTL != 0 {
+ values.Add("ttl", fmt.Sprintf("%v", dnsrecord.TTL))
+ }
+
+ if err := c.post(`dns/update_record`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteDNSRecord deletes an existing DNS record
+func (c *Client) DeleteDNSRecord(domain string, recordID int) error {
+ values := url.Values{
+ "domain": {domain},
+ "RECORDID": {fmt.Sprintf("%v", recordID)},
+ }
+
+ if err := c.post(`dns/delete_record`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/firewall.go b/vendor/github.com/JamesClonk/vultr/lib/firewall.go
new file mode 100644
index 000000000..c9c54e9d5
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/firewall.go
@@ -0,0 +1,248 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// FirewallGroup represents a firewall group on Vultr
+type FirewallGroup struct {
+ ID string `json:"FIREWALLGROUPID"`
+ Description string `json:"description"`
+ Created string `json:"date_created"`
+ Modified string `json:"date_modified"`
+ InstanceCount int `json:"instance_count"`
+ RuleCount int `json:"rule_count"`
+ MaxRuleCount int `json:"max_rule_count"`
+}
+
+// FirewallRule represents a firewall rule on Vultr
+type FirewallRule struct {
+ RuleNumber int `json:"rulenumber"`
+ Action string `json:"action"`
+ Protocol string `json:"protocol"`
+ Port string `json:"port"`
+ Network *net.IPNet
+}
+
+type firewallGroups []FirewallGroup
+
+func (f firewallGroups) Len() int { return len(f) }
+func (f firewallGroups) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+func (f firewallGroups) Less(i, j int) bool {
+ // sort order: description
+ return strings.ToLower(f[i].Description) < strings.ToLower(f[j].Description)
+}
+
+type firewallRules []FirewallRule
+
+func (r firewallRules) Len() int { return len(r) }
+func (r firewallRules) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
+func (r firewallRules) Less(i, j int) bool {
+ // sort order: rule number
+ return r[i].RuleNumber < r[j].RuleNumber
+}
+
+// UnmarshalJSON implements json.Unmarshaller on FirewallRule.
+// This is needed because the Vultr API is inconsistent in it's JSON responses.
+// Some fields can change type, from JSON number to JSON string and vice-versa.
+func (r *FirewallRule) UnmarshalJSON(data []byte) (err error) {
+ if r == nil {
+ *r = FirewallRule{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ value := fmt.Sprintf("%v", fields["rulenumber"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ number, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ r.RuleNumber = int(number)
+
+ value = fmt.Sprintf("%v", fields["subnet_size"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ subnetSize, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+
+ r.Action = fmt.Sprintf("%v", fields["action"])
+ r.Protocol = fmt.Sprintf("%v", fields["protocol"])
+ r.Port = fmt.Sprintf("%v", fields["port"])
+ subnet := fmt.Sprintf("%v", fields["subnet"])
+
+ if subnetSize > 0 && len(subnet) > 0 {
+ _, r.Network, err = net.ParseCIDR(fmt.Sprintf("%s/%d", subnet, subnetSize))
+ if err != nil {
+ return fmt.Errorf("Failed to parse subnet from Vultr API")
+ }
+ } else {
+ _, r.Network, _ = net.ParseCIDR("0.0.0.0/0")
+ }
+
+ return
+}
+
+// GetFirewallGroups returns a list of all available firewall groups on Vultr
+func (c *Client) GetFirewallGroups() ([]FirewallGroup, error) {
+ var groupMap map[string]FirewallGroup
+ if err := c.get(`firewall/group_list`, &groupMap); err != nil {
+ return nil, err
+ }
+
+ var groupList []FirewallGroup
+ for _, g := range groupMap {
+ groupList = append(groupList, g)
+ }
+ sort.Sort(firewallGroups(groupList))
+ return groupList, nil
+}
+
+// GetFirewallGroup returns the firewall group with given ID
+func (c *Client) GetFirewallGroup(id string) (FirewallGroup, error) {
+ groups, err := c.GetFirewallGroups()
+ if err != nil {
+ return FirewallGroup{}, err
+ }
+
+ for _, g := range groups {
+ if g.ID == id {
+ return g, nil
+ }
+ }
+ return FirewallGroup{}, fmt.Errorf("Firewall group with ID %v not found", id)
+}
+
+// CreateFirewallGroup creates a new firewall group in Vultr account
+func (c *Client) CreateFirewallGroup(description string) (string, error) {
+ values := url.Values{}
+
+ // Optional description
+ if len(description) > 0 {
+ values.Add("description", description)
+ }
+
+ var result FirewallGroup
+ err := c.post(`firewall/group_create`, values, &result)
+ if err != nil {
+ return "", err
+ }
+ return result.ID, nil
+}
+
+// DeleteFirewallGroup deletes an existing firewall group
+func (c *Client) DeleteFirewallGroup(groupID string) error {
+ values := url.Values{
+ "FIREWALLGROUPID": {groupID},
+ }
+
+ if err := c.post(`firewall/group_delete`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetFirewallGroupDescription sets the description of an existing firewall group
+func (c *Client) SetFirewallGroupDescription(groupID, description string) error {
+ values := url.Values{
+ "FIREWALLGROUPID": {groupID},
+ "description": {description},
+ }
+
+ if err := c.post(`firewall/group_set_description`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// GetFirewallRules returns a list of rules for the given firewall group
+func (c *Client) GetFirewallRules(groupID string) ([]FirewallRule, error) {
+ var ruleMap map[string]FirewallRule
+ ipTypes := []string{"v4", "v6"}
+ for _, ipType := range ipTypes {
+ args := fmt.Sprintf("direction=in&FIREWALLGROUPID=%s&ip_type=%s",
+ groupID, ipType)
+ if err := c.get(`firewall/rule_list?`+args, &ruleMap); err != nil {
+ return nil, err
+ }
+ }
+
+ var ruleList []FirewallRule
+ for _, r := range ruleMap {
+ ruleList = append(ruleList, r)
+ }
+ sort.Sort(firewallRules(ruleList))
+ return ruleList, nil
+}
+
+// CreateFirewallRule creates a new firewall rule in Vultr account.
+// groupID is the ID of the firewall group to create the rule in
+// protocol must be one of: "icmp", "tcp", "udp", "gre"
+// port can be a port number or colon separated port range (TCP/UDP only)
+func (c *Client) CreateFirewallRule(groupID, protocol, port string,
+ network *net.IPNet) (int, error) {
+ ip := network.IP.String()
+ maskBits, _ := network.Mask.Size()
+ if ip == "" {
+ return 0, fmt.Errorf("Invalid network")
+ }
+
+ var ipType string
+ if network.IP.To4() != nil {
+ ipType = "v4"
+ } else {
+ ipType = "v6"
+ }
+
+ values := url.Values{
+ "FIREWALLGROUPID": {groupID},
+ // possible values: "in"
+ "direction": {"in"},
+ // possible values: "icmp", "tcp", "udp", "gre"
+ "protocol": {protocol},
+ // possible values: "v4", "v6"
+ "ip_type": {ipType},
+ // IP address representing a subnet
+ "subnet": {ip},
+ // IP prefix size in bits
+ "subnet_size": {fmt.Sprintf("%v", maskBits)},
+ }
+
+ if len(port) > 0 {
+ values.Add("port", port)
+ }
+
+ var result FirewallRule
+ err := c.post(`firewall/rule_create`, values, &result)
+ if err != nil {
+ return 0, err
+ }
+ return result.RuleNumber, nil
+}
+
+// DeleteFirewallRule deletes an existing firewall rule
+func (c *Client) DeleteFirewallRule(ruleNumber int, groupID string) error {
+ values := url.Values{
+ "FIREWALLGROUPID": {groupID},
+ "rulenumber": {fmt.Sprintf("%v", ruleNumber)},
+ }
+
+ if err := c.post(`firewall/rule_delete`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/ip.go b/vendor/github.com/JamesClonk/vultr/lib/ip.go
new file mode 100644
index 000000000..4d169cb3f
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/ip.go
@@ -0,0 +1,192 @@
+package lib
+
+import (
+ "fmt"
+ "net/url"
+ "sort"
+)
+
+// IPv4 information of a virtual machine
+type IPv4 struct {
+ IP string `json:"ip"`
+ Netmask string `json:"netmask"`
+ Gateway string `json:"gateway"`
+ Type string `json:"type"`
+ ReverseDNS string `json:"reverse"`
+}
+
+type ipv4s []IPv4
+
+func (s ipv4s) Len() int { return len(s) }
+func (s ipv4s) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s ipv4s) Less(i, j int) bool {
+ // sort order: type, ip
+ if s[i].Type < s[j].Type {
+ return true
+ } else if s[i].Type > s[j].Type {
+ return false
+ }
+ return s[i].IP < s[j].IP
+}
+
+// IPv6 information of a virtual machine
+type IPv6 struct {
+ IP string `json:"ip"`
+ Network string `json:"network"`
+ NetworkSize string `json:"network_size"`
+ Type string `json:"type"`
+}
+
+type ipv6s []IPv6
+
+func (s ipv6s) Len() int { return len(s) }
+func (s ipv6s) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s ipv6s) Less(i, j int) bool {
+ // sort order: type, ip
+ if s[i].Type < s[j].Type {
+ return true
+ } else if s[i].Type > s[j].Type {
+ return false
+ }
+ return s[i].IP < s[j].IP
+}
+
+// ReverseDNSIPv6 information of a virtual machine
+type ReverseDNSIPv6 struct {
+ IP string `json:"ip"`
+ ReverseDNS string `json:"reverse"`
+}
+
+type reverseDNSIPv6s []ReverseDNSIPv6
+
+func (s reverseDNSIPv6s) Len() int { return len(s) }
+func (s reverseDNSIPv6s) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s reverseDNSIPv6s) Less(i, j int) bool { return s[i].IP < s[j].IP }
+
+// ListIPv4 lists the IPv4 information of a virtual machine
+func (c *Client) ListIPv4(id string) (list []IPv4, err error) {
+ var ipMap map[string][]IPv4
+ if err := c.get(`server/list_ipv4?SUBID=`+id, &ipMap); err != nil {
+ return nil, err
+ }
+
+ for _, iplist := range ipMap {
+ for _, ip := range iplist {
+ list = append(list, ip)
+ }
+ }
+ sort.Sort(ipv4s(list))
+ return list, nil
+}
+
+// CreateIPv4 creates an IPv4 address and attaches it to a virtual machine
+func (c *Client) CreateIPv4(id string, reboot bool) error {
+ values := url.Values{
+ "SUBID": {id},
+ "reboot": {fmt.Sprintf("%t", reboot)},
+ }
+
+ if err := c.post(`server/create_ipv4`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteIPv4 deletes an IPv4 address and detaches it from a virtual machine
+func (c *Client) DeleteIPv4(id, ip string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ip": {ip},
+ }
+
+ if err := c.post(`server/destroy_ipv4`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ListIPv6 lists the IPv4 information of a virtual machine
+func (c *Client) ListIPv6(id string) (list []IPv6, err error) {
+ var ipMap map[string][]IPv6
+ if err := c.get(`server/list_ipv6?SUBID=`+id, &ipMap); err != nil {
+ return nil, err
+ }
+
+ for _, iplist := range ipMap {
+ for _, ip := range iplist {
+ list = append(list, ip)
+ }
+ }
+ sort.Sort(ipv6s(list))
+ return list, nil
+}
+
+// ListIPv6ReverseDNS lists the IPv6 reverse DNS entries of a virtual machine
+func (c *Client) ListIPv6ReverseDNS(id string) (list []ReverseDNSIPv6, err error) {
+ var ipMap map[string][]ReverseDNSIPv6
+ if err := c.get(`server/reverse_list_ipv6?SUBID=`+id, &ipMap); err != nil {
+ return nil, err
+ }
+
+ for _, iplist := range ipMap {
+ for _, ip := range iplist {
+ list = append(list, ip)
+ }
+ }
+ sort.Sort(reverseDNSIPv6s(list))
+ return list, nil
+}
+
+// DeleteIPv6ReverseDNS removes a reverse DNS entry for an IPv6 address of a virtual machine
+func (c *Client) DeleteIPv6ReverseDNS(id string, ip string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ip": {ip},
+ }
+
+ if err := c.post(`server/reverse_delete_ipv6`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetIPv6ReverseDNS sets a reverse DNS entry for an IPv6 address of a virtual machine
+func (c *Client) SetIPv6ReverseDNS(id, ip, entry string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ip": {ip},
+ "entry": {entry},
+ }
+
+ if err := c.post(`server/reverse_set_ipv6`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DefaultIPv4ReverseDNS sets a reverse DNS entry for an IPv4 address of a virtual machine to the original setting
+func (c *Client) DefaultIPv4ReverseDNS(id, ip string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ip": {ip},
+ }
+
+ if err := c.post(`server/reverse_default_ipv4`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetIPv4ReverseDNS sets a reverse DNS entry for an IPv4 address of a virtual machine
+func (c *Client) SetIPv4ReverseDNS(id, ip, entry string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ip": {ip},
+ "entry": {entry},
+ }
+
+ if err := c.post(`server/reverse_set_ipv4`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/iso.go b/vendor/github.com/JamesClonk/vultr/lib/iso.go
new file mode 100644
index 000000000..a9e1880e4
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/iso.go
@@ -0,0 +1,44 @@
+package lib
+
+import (
+ "sort"
+ "strings"
+)
+
+// ISO image on Vultr
+type ISO struct {
+ ID int `json:"ISOID"`
+ Created string `json:"date_created"`
+ Filename string `json:"filename"`
+ Size int `json:"size"`
+ MD5sum string `json:"md5sum"`
+}
+
+type isos []ISO
+
+func (s isos) Len() int { return len(s) }
+func (s isos) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s isos) Less(i, j int) bool {
+ // sort order: filename, created
+ if strings.ToLower(s[i].Filename) < strings.ToLower(s[j].Filename) {
+ return true
+ } else if strings.ToLower(s[i].Filename) > strings.ToLower(s[j].Filename) {
+ return false
+ }
+ return s[i].Created < s[j].Created
+}
+
+// GetISO returns a list of all ISO images on Vultr account
+func (c *Client) GetISO() ([]ISO, error) {
+ var isoMap map[string]ISO
+ if err := c.get(`iso/list`, &isoMap); err != nil {
+ return nil, err
+ }
+
+ var isoList []ISO
+ for _, iso := range isoMap {
+ isoList = append(isoList, iso)
+ }
+ sort.Sort(isos(isoList))
+ return isoList, nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/os.go b/vendor/github.com/JamesClonk/vultr/lib/os.go
new file mode 100644
index 000000000..647d253f8
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/os.go
@@ -0,0 +1,37 @@
+package lib
+
+import (
+ "sort"
+ "strings"
+)
+
+// OS image on Vultr
+type OS struct {
+ ID int `json:"OSID"`
+ Name string `json:"name"`
+ Arch string `json:"arch"`
+ Family string `json:"family"`
+ Windows bool `json:"windows"`
+ Surcharge string `json:"surcharge"`
+}
+
+type oses []OS
+
+func (s oses) Len() int { return len(s) }
+func (s oses) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s oses) Less(i, j int) bool { return strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name) }
+
+// GetOS returns a list of all available operating systems on Vultr
+func (c *Client) GetOS() ([]OS, error) {
+ var osMap map[string]OS
+ if err := c.get(`os/list`, &osMap); err != nil {
+ return nil, err
+ }
+
+ var osList []OS
+ for _, os := range osMap {
+ osList = append(osList, os)
+ }
+ sort.Sort(oses(osList))
+ return osList, nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/plans.go b/vendor/github.com/JamesClonk/vultr/lib/plans.go
new file mode 100644
index 000000000..b3bef4eff
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/plans.go
@@ -0,0 +1,78 @@
+package lib
+
+import (
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// Plan on Vultr
+type Plan struct {
+ ID int `json:"VPSPLANID,string"`
+ Name string `json:"name"`
+ VCpus int `json:"vcpu_count,string"`
+ RAM string `json:"ram"`
+ Disk string `json:"disk"`
+ Bandwidth string `json:"bandwidth"`
+ Price string `json:"price_per_month"`
+ Regions []int `json:"available_locations"`
+}
+
+type plans []Plan
+
+func (p plans) Len() int { return len(p) }
+func (p plans) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+func (p plans) Less(i, j int) bool {
+ pa, _ := strconv.ParseFloat(strings.TrimSpace(p[i].Price), 64)
+ pb, _ := strconv.ParseFloat(strings.TrimSpace(p[j].Price), 64)
+ ra, _ := strconv.ParseInt(strings.TrimSpace(p[i].RAM), 10, 64)
+ rb, _ := strconv.ParseInt(strings.TrimSpace(p[j].RAM), 10, 64)
+ da, _ := strconv.ParseInt(strings.TrimSpace(p[i].Disk), 10, 64)
+ db, _ := strconv.ParseInt(strings.TrimSpace(p[j].Disk), 10, 64)
+
+ // sort order: price, vcpu, ram, disk
+ if pa < pb {
+ return true
+ } else if pa > pb {
+ return false
+ }
+
+ if p[i].VCpus < p[j].VCpus {
+ return true
+ } else if p[i].VCpus > p[j].VCpus {
+ return false
+ }
+
+ if ra < rb {
+ return true
+ } else if ra > rb {
+ return false
+ }
+
+ return da < db
+}
+
+// GetPlans returns a list of all available plans on Vultr account
+func (c *Client) GetPlans() ([]Plan, error) {
+ var planMap map[string]Plan
+ if err := c.get(`plans/list`, &planMap); err != nil {
+ return nil, err
+ }
+
+ var p plans
+ for _, plan := range planMap {
+ p = append(p, plan)
+ }
+
+ sort.Sort(plans(p))
+ return p, nil
+}
+
+// GetAvailablePlansForRegion returns available plans for specified region
+func (c *Client) GetAvailablePlansForRegion(id int) (planIDs []int, err error) {
+ if err := c.get(fmt.Sprintf(`regions/availability?DCID=%v`, id), &planIDs); err != nil {
+ return nil, err
+ }
+ return
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/regions.go b/vendor/github.com/JamesClonk/vultr/lib/regions.go
new file mode 100644
index 000000000..70ceb2ede
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/regions.go
@@ -0,0 +1,44 @@
+package lib
+
+import "sort"
+
+// Region on Vultr
+type Region struct {
+ ID int `json:"DCID,string"`
+ Name string `json:"name"`
+ Country string `json:"country"`
+ Continent string `json:"continent"`
+ State string `json:"state"`
+ Ddos bool `json:"ddos_protection"`
+ BlockStorage bool `json:"block_storage"`
+ Code string `json:"regioncode"`
+}
+
+type regions []Region
+
+func (s regions) Len() int { return len(s) }
+func (s regions) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s regions) Less(i, j int) bool {
+ // sort order: continent, name
+ if s[i].Continent < s[j].Continent {
+ return true
+ } else if s[i].Continent > s[j].Continent {
+ return false
+ }
+ return s[i].Name < s[j].Name
+}
+
+// GetRegions returns a list of all available Vultr regions
+func (c *Client) GetRegions() ([]Region, error) {
+ var regionMap map[string]Region
+ if err := c.get(`regions/list`, ®ionMap); err != nil {
+ return nil, err
+ }
+
+ var regionList []Region
+ for _, os := range regionMap {
+ regionList = append(regionList, os)
+ }
+ sort.Sort(regions(regionList))
+ return regionList, nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/reservedip.go b/vendor/github.com/JamesClonk/vultr/lib/reservedip.go
new file mode 100644
index 000000000..22097cf7d
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/reservedip.go
@@ -0,0 +1,192 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// IP on Vultr
+type IP struct {
+ ID string `json:"SUBID,string"`
+ RegionID int `json:"DCID,string"`
+ IPType string `json:"ip_type"`
+ Subnet string `json:"subnet"`
+ SubnetSize int `json:"subnet_size"`
+ Label string `json:"label"`
+ AttachedTo string `json:"attached_SUBID,string"`
+}
+
+type ips []IP
+
+func (s ips) Len() int { return len(s) }
+func (s ips) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s ips) Less(i, j int) bool {
+ // sort order: label, iptype, subnet
+ if strings.ToLower(s[i].Label) < strings.ToLower(s[j].Label) {
+ return true
+ } else if strings.ToLower(s[i].Label) > strings.ToLower(s[j].Label) {
+ return false
+ }
+ if s[i].IPType < s[j].IPType {
+ return true
+ } else if s[i].IPType > s[j].IPType {
+ return false
+ }
+ return s[i].Subnet < s[j].Subnet
+}
+
+// UnmarshalJSON implements json.Unmarshaller on IP.
+// This is needed because the Vultr API is inconsistent in it's JSON responses.
+// Some fields can change type, from JSON number to JSON string and vice-versa.
+func (i *IP) UnmarshalJSON(data []byte) (err error) {
+ if i == nil {
+ *i = IP{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ value := fmt.Sprintf("%v", fields["SUBID"])
+ if len(value) == 0 || value == "" || value == "0" {
+ i.ID = ""
+ } else {
+ id, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ i.ID = strconv.FormatFloat(id, 'f', -1, 64)
+ }
+
+ value = fmt.Sprintf("%v", fields["DCID"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ region, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ i.RegionID = int(region)
+
+ value = fmt.Sprintf("%v", fields["attached_SUBID"])
+ if len(value) == 0 || value == "" || value == "0" || value == "false" {
+ i.AttachedTo = ""
+ } else {
+ attached, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ i.AttachedTo = strconv.FormatFloat(attached, 'f', -1, 64)
+ }
+
+ value = fmt.Sprintf("%v", fields["subnet_size"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ size, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ i.SubnetSize = int(size)
+
+ i.IPType = fmt.Sprintf("%v", fields["ip_type"])
+ i.Subnet = fmt.Sprintf("%v", fields["subnet"])
+ i.Label = fmt.Sprintf("%v", fields["label"])
+
+ return
+}
+
+// ListReservedIP returns a list of all available reserved IPs on Vultr account
+func (c *Client) ListReservedIP() ([]IP, error) {
+ var ipMap map[string]IP
+
+ err := c.get(`reservedip/list`, &ipMap)
+ if err != nil {
+ return nil, err
+ }
+
+ ipList := make([]IP, 0)
+ for _, ip := range ipMap {
+ ipList = append(ipList, ip)
+ }
+ sort.Sort(ips(ipList))
+ return ipList, nil
+}
+
+// GetReservedIP returns reserved IP with given ID
+func (c *Client) GetReservedIP(id string) (IP, error) {
+ var ipMap map[string]IP
+
+ err := c.get(`reservedip/list`, &ipMap)
+ if err != nil {
+ return IP{}, err
+ }
+ if ip, ok := ipMap[id]; ok {
+ return ip, nil
+ }
+ return IP{}, fmt.Errorf("IP with ID %v not found", id)
+}
+
+// CreateReservedIP creates a new reserved IP on Vultr account
+func (c *Client) CreateReservedIP(regionID int, ipType string, label string) (string, error) {
+ values := url.Values{
+ "DCID": {fmt.Sprintf("%v", regionID)},
+ "ip_type": {ipType},
+ }
+ if len(label) > 0 {
+ values.Add("label", label)
+ }
+
+ result := IP{}
+ err := c.post(`reservedip/create`, values, &result)
+ if err != nil {
+ return "", err
+ }
+ return result.ID, nil
+}
+
+// DestroyReservedIP deletes an existing reserved IP
+func (c *Client) DestroyReservedIP(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+ return c.post(`reservedip/destroy`, values, nil)
+}
+
+// AttachReservedIP attaches a reserved IP to a virtual machine
+func (c *Client) AttachReservedIP(ip string, serverID string) error {
+ values := url.Values{
+ "ip_address": {ip},
+ "attach_SUBID": {serverID},
+ }
+ return c.post(`reservedip/attach`, values, nil)
+}
+
+// DetachReservedIP detaches a reserved IP from an existing virtual machine
+func (c *Client) DetachReservedIP(serverID string, ip string) error {
+ values := url.Values{
+ "ip_address": {ip},
+ "detach_SUBID": {serverID},
+ }
+ return c.post(`reservedip/detach`, values, nil)
+}
+
+// ConvertReservedIP converts an existing virtual machines IP to a reserved IP
+func (c *Client) ConvertReservedIP(serverID string, ip string) (string, error) {
+ values := url.Values{
+ "SUBID": {serverID},
+ "ip_address": {ip},
+ }
+
+ result := IP{}
+ err := c.post(`reservedip/convert`, values, &result)
+ if err != nil {
+ return "", err
+ }
+ return result.ID, err
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/scripts.go b/vendor/github.com/JamesClonk/vultr/lib/scripts.go
new file mode 100644
index 000000000..d6639cf1e
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/scripts.go
@@ -0,0 +1,126 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "sort"
+ "strings"
+)
+
+// StartupScript on Vultr account
+type StartupScript struct {
+ ID string `json:"SCRIPTID"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Content string `json:"script"`
+}
+
+type startupscripts []StartupScript
+
+func (s startupscripts) Len() int { return len(s) }
+func (s startupscripts) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s startupscripts) Less(i, j int) bool {
+ return strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name)
+}
+
+// UnmarshalJSON implements json.Unmarshaller on StartupScript.
+// Necessary because the SCRIPTID field has inconsistent types.
+func (s *StartupScript) UnmarshalJSON(data []byte) (err error) {
+ if s == nil {
+ *s = StartupScript{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ s.ID = fmt.Sprintf("%v", fields["SCRIPTID"])
+ s.Name = fmt.Sprintf("%v", fields["name"])
+ s.Type = fmt.Sprintf("%v", fields["type"])
+ s.Content = fmt.Sprintf("%v", fields["script"])
+
+ return
+}
+
+// GetStartupScripts returns a list of all startup scripts on the current Vultr account
+func (c *Client) GetStartupScripts() (scripts []StartupScript, err error) {
+ var scriptMap map[string]StartupScript
+ if err := c.get(`startupscript/list`, &scriptMap); err != nil {
+ return nil, err
+ }
+
+ for _, script := range scriptMap {
+ if script.Type == "" {
+ script.Type = "boot" // set default script type
+ }
+ scripts = append(scripts, script)
+ }
+ sort.Sort(startupscripts(scripts))
+ return scripts, nil
+}
+
+// GetStartupScript returns the startup script with the given ID
+func (c *Client) GetStartupScript(id string) (StartupScript, error) {
+ scripts, err := c.GetStartupScripts()
+ if err != nil {
+ return StartupScript{}, err
+ }
+
+ for _, s := range scripts {
+ if s.ID == id {
+ return s, nil
+ }
+ }
+ return StartupScript{}, nil
+}
+
+// CreateStartupScript creates a new startup script
+func (c *Client) CreateStartupScript(name, content, scriptType string) (StartupScript, error) {
+ values := url.Values{
+ "name": {name},
+ "script": {content},
+ "type": {scriptType},
+ }
+
+ var script StartupScript
+ if err := c.post(`startupscript/create`, values, &script); err != nil {
+ return StartupScript{}, err
+ }
+ script.Name = name
+ script.Content = content
+ script.Type = scriptType
+
+ return script, nil
+}
+
+// UpdateStartupScript updates an existing startup script
+func (c *Client) UpdateStartupScript(script StartupScript) error {
+ values := url.Values{
+ "SCRIPTID": {script.ID},
+ }
+ if script.Name != "" {
+ values.Add("name", script.Name)
+ }
+ if script.Content != "" {
+ values.Add("script", script.Content)
+ }
+
+ if err := c.post(`startupscript/update`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteStartupScript deletes an existing startup script from Vultr account
+func (c *Client) DeleteStartupScript(id string) error {
+ values := url.Values{
+ "SCRIPTID": {id},
+ }
+
+ if err := c.post(`startupscript/destroy`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/servers.go b/vendor/github.com/JamesClonk/vultr/lib/servers.go
new file mode 100644
index 000000000..418198a71
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/servers.go
@@ -0,0 +1,561 @@
+package lib
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// Server (virtual machine) on Vultr account
+type Server struct {
+ ID string `json:"SUBID"`
+ Name string `json:"label"`
+ OS string `json:"os"`
+ RAM string `json:"ram"`
+ Disk string `json:"disk"`
+ MainIP string `json:"main_ip"`
+ VCpus int `json:"vcpu_count,string"`
+ Location string `json:"location"`
+ RegionID int `json:"DCID,string"`
+ DefaultPassword string `json:"default_password"`
+ Created string `json:"date_created"`
+ PendingCharges float64 `json:"pending_charges"`
+ Status string `json:"status"`
+ Cost string `json:"cost_per_month"`
+ CurrentBandwidth float64 `json:"current_bandwidth_gb"`
+ AllowedBandwidth float64 `json:"allowed_bandwidth_gb,string"`
+ NetmaskV4 string `json:"netmask_v4"`
+ GatewayV4 string `json:"gateway_v4"`
+ PowerStatus string `json:"power_status"`
+ ServerState string `json:"server_state"`
+ PlanID int `json:"VPSPLANID,string"`
+ V6Networks []V6Network `json:"v6_networks"`
+ InternalIP string `json:"internal_ip"`
+ KVMUrl string `json:"kvm_url"`
+ AutoBackups string `json:"auto_backups"`
+ Tag string `json:"tag"`
+ OSID string `json:"OSID"`
+ AppID string `json:"APPID"`
+ FirewallGroupID string `json:"FIREWALLGROUPID"`
+}
+
+// ServerOptions are optional parameters to be used during server creation
+type ServerOptions struct {
+ IPXEChainURL string
+ ISO int
+ Script int
+ UserData string
+ Snapshot string
+ SSHKey string
+ ReservedIP string
+ IPV6 bool
+ PrivateNetworking bool
+ AutoBackups bool
+ DontNotifyOnActivate bool
+ Hostname string
+ Tag string
+ AppID string
+ FirewallGroupID string
+}
+
+type servers []Server
+
+func (s servers) Len() int { return len(s) }
+func (s servers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s servers) Less(i, j int) bool {
+ // sort order: name, ip
+ if strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name) {
+ return true
+ } else if strings.ToLower(s[i].Name) > strings.ToLower(s[j].Name) {
+ return false
+ }
+ return s[i].MainIP < s[j].MainIP
+}
+
+// V6Network represents a IPv6 network of a Vultr server
+type V6Network struct {
+ Network string `json:"v6_network"`
+ MainIP string `json:"v6_main_ip"`
+ NetworkSize string `json:"v6_network_size"`
+}
+
+// ISOStatus represents an ISO image attached to a Vultr server
+type ISOStatus struct {
+ State string `json:"state"`
+ ISOID string `json:"ISOID"`
+}
+
+// UnmarshalJSON implements json.Unmarshaller on Server.
+// This is needed because the Vultr API is inconsistent in it's JSON responses for servers.
+// Some fields can change type, from JSON number to JSON string and vice-versa.
+func (s *Server) UnmarshalJSON(data []byte) (err error) {
+ if s == nil {
+ *s = Server{}
+ }
+
+ var fields map[string]interface{}
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+
+ value := fmt.Sprintf("%v", fields["vcpu_count"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ vcpu, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ s.VCpus = int(vcpu)
+
+ value = fmt.Sprintf("%v", fields["DCID"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ region, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ s.RegionID = int(region)
+
+ value = fmt.Sprintf("%v", fields["VPSPLANID"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ plan, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ s.PlanID = int(plan)
+
+ value = fmt.Sprintf("%v", fields["pending_charges"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ pc, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ s.PendingCharges = pc
+
+ value = fmt.Sprintf("%v", fields["current_bandwidth_gb"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ cb, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ s.CurrentBandwidth = cb
+
+ value = fmt.Sprintf("%v", fields["allowed_bandwidth_gb"])
+ if len(value) == 0 || value == "" {
+ value = "0"
+ }
+ ab, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return err
+ }
+ s.AllowedBandwidth = ab
+
+ value = fmt.Sprintf("%v", fields["OSID"])
+ if value == "" {
+ value = ""
+ }
+ s.OSID = value
+
+ value = fmt.Sprintf("%v", fields["APPID"])
+ if value == "" || value == "0" {
+ value = ""
+ }
+ s.AppID = value
+
+ value = fmt.Sprintf("%v", fields["FIREWALLGROUPID"])
+ if value == "" || value == "0" {
+ value = ""
+ }
+ s.FirewallGroupID = value
+
+ s.ID = fmt.Sprintf("%v", fields["SUBID"])
+ s.Name = fmt.Sprintf("%v", fields["label"])
+ s.OS = fmt.Sprintf("%v", fields["os"])
+ s.RAM = fmt.Sprintf("%v", fields["ram"])
+ s.Disk = fmt.Sprintf("%v", fields["disk"])
+ s.MainIP = fmt.Sprintf("%v", fields["main_ip"])
+ s.Location = fmt.Sprintf("%v", fields["location"])
+ s.DefaultPassword = fmt.Sprintf("%v", fields["default_password"])
+ s.Created = fmt.Sprintf("%v", fields["date_created"])
+ s.Status = fmt.Sprintf("%v", fields["status"])
+ s.Cost = fmt.Sprintf("%v", fields["cost_per_month"])
+ s.NetmaskV4 = fmt.Sprintf("%v", fields["netmask_v4"])
+ s.GatewayV4 = fmt.Sprintf("%v", fields["gateway_v4"])
+ s.PowerStatus = fmt.Sprintf("%v", fields["power_status"])
+ s.ServerState = fmt.Sprintf("%v", fields["server_state"])
+
+ v6networks := make([]V6Network, 0)
+ if networks, ok := fields["v6_networks"].([]interface{}); ok {
+ for _, network := range networks {
+ if network, ok := network.(map[string]interface{}); ok {
+ v6network := V6Network{
+ Network: fmt.Sprintf("%v", network["v6_network"]),
+ MainIP: fmt.Sprintf("%v", network["v6_main_ip"]),
+ NetworkSize: fmt.Sprintf("%v", network["v6_network_size"]),
+ }
+ v6networks = append(v6networks, v6network)
+ }
+ }
+ s.V6Networks = v6networks
+ }
+
+ s.InternalIP = fmt.Sprintf("%v", fields["internal_ip"])
+ s.KVMUrl = fmt.Sprintf("%v", fields["kvm_url"])
+ s.AutoBackups = fmt.Sprintf("%v", fields["auto_backups"])
+ s.Tag = fmt.Sprintf("%v", fields["tag"])
+
+ return
+}
+
+// GetServers returns a list of current virtual machines on Vultr account
+func (c *Client) GetServers() (serverList []Server, err error) {
+ var serverMap map[string]Server
+ if err := c.get(`server/list`, &serverMap); err != nil {
+ return nil, err
+ }
+
+ for _, server := range serverMap {
+ serverList = append(serverList, server)
+ }
+ sort.Sort(servers(serverList))
+ return serverList, nil
+}
+
+// GetServersByTag returns a list of all virtual machines matching by tag
+func (c *Client) GetServersByTag(tag string) (serverList []Server, err error) {
+ var serverMap map[string]Server
+ if err := c.get(`server/list?tag=`+tag, &serverMap); err != nil {
+ return nil, err
+ }
+
+ for _, server := range serverMap {
+ serverList = append(serverList, server)
+ }
+ sort.Sort(servers(serverList))
+ return serverList, nil
+}
+
+// GetServer returns the virtual machine with the given ID
+func (c *Client) GetServer(id string) (server Server, err error) {
+ if err := c.get(`server/list?SUBID=`+id, &server); err != nil {
+ return Server{}, err
+ }
+ return server, nil
+}
+
+// CreateServer creates a new virtual machine on Vultr. ServerOptions are optional settings.
+func (c *Client) CreateServer(name string, regionID, planID, osID int, options *ServerOptions) (Server, error) {
+ values := url.Values{
+ "label": {name},
+ "DCID": {fmt.Sprintf("%v", regionID)},
+ "VPSPLANID": {fmt.Sprintf("%v", planID)},
+ "OSID": {fmt.Sprintf("%v", osID)},
+ }
+
+ if options != nil {
+ if options.IPXEChainURL != "" {
+ values.Add("ipxe_chain_url", options.IPXEChainURL)
+ }
+
+ if options.ISO != 0 {
+ values.Add("ISOID", fmt.Sprintf("%v", options.ISO))
+ }
+
+ if options.Script != 0 {
+ values.Add("SCRIPTID", fmt.Sprintf("%v", options.Script))
+ }
+
+ if options.UserData != "" {
+ values.Add("userdata", base64.StdEncoding.EncodeToString([]byte(options.UserData)))
+ }
+
+ if options.Snapshot != "" {
+ values.Add("SNAPSHOTID", options.Snapshot)
+ }
+
+ if options.SSHKey != "" {
+ values.Add("SSHKEYID", options.SSHKey)
+ }
+
+ if options.ReservedIP != "" {
+ values.Add("reserved_ip_v4", options.ReservedIP)
+ }
+
+ values.Add("enable_ipv6", "no")
+ if options.IPV6 {
+ values.Set("enable_ipv6", "yes")
+ }
+
+ values.Add("enable_private_network", "no")
+ if options.PrivateNetworking {
+ values.Set("enable_private_network", "yes")
+ }
+
+ values.Add("auto_backups", "no")
+ if options.AutoBackups {
+ values.Set("auto_backups", "yes")
+ }
+
+ values.Add("notify_activate", "yes")
+ if options.DontNotifyOnActivate {
+ values.Set("notify_activate", "no")
+ }
+
+ if options.Hostname != "" {
+ values.Add("hostname", options.Hostname)
+ }
+
+ if options.Tag != "" {
+ values.Add("tag", options.Tag)
+ }
+
+ if options.AppID != "" {
+ values.Add("APPID", options.AppID)
+ }
+
+ if options.FirewallGroupID != "" {
+ values.Add("FIREWALLGROUPID", options.FirewallGroupID)
+ }
+ }
+
+ var server Server
+ if err := c.post(`server/create`, values, &server); err != nil {
+ return Server{}, err
+ }
+ server.Name = name
+ server.RegionID = regionID
+ server.PlanID = planID
+
+ return server, nil
+}
+
+// RenameServer renames an existing virtual machine
+func (c *Client) RenameServer(id, name string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "label": {name},
+ }
+
+ if err := c.post(`server/label_set`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// TagServer replaces the tag on an existing virtual machine
+func (c *Client) TagServer(id, tag string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "tag": {tag},
+ }
+
+ if err := c.post(`server/tag_set`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// StartServer starts an existing virtual machine
+func (c *Client) StartServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/start`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// HaltServer stops an existing virtual machine
+func (c *Client) HaltServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/halt`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// RebootServer reboots an existing virtual machine
+func (c *Client) RebootServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/reboot`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ReinstallServer reinstalls the operating system on an existing virtual machine
+func (c *Client) ReinstallServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/reinstall`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ChangeOSofServer changes the virtual machine to a different operating system
+func (c *Client) ChangeOSofServer(id string, osID int) error {
+ values := url.Values{
+ "SUBID": {id},
+ "OSID": {fmt.Sprintf("%v", osID)},
+ }
+
+ if err := c.post(`server/os_change`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ListOSforServer lists all available operating systems to which an existing virtual machine can be changed
+func (c *Client) ListOSforServer(id string) (os []OS, err error) {
+ var osMap map[string]OS
+ if err := c.get(`server/os_change_list?SUBID=`+id, &osMap); err != nil {
+ return nil, err
+ }
+
+ for _, o := range osMap {
+ os = append(os, o)
+ }
+ sort.Sort(oses(os))
+ return os, nil
+}
+
+// AttachISOtoServer attaches an ISO image to an existing virtual machine and reboots it
+func (c *Client) AttachISOtoServer(id string, isoID int) error {
+ values := url.Values{
+ "SUBID": {id},
+ "ISOID": {fmt.Sprintf("%v", isoID)},
+ }
+
+ if err := c.post(`server/iso_attach`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DetachISOfromServer detaches the currently mounted ISO image from the virtual machine and reboots it
+func (c *Client) DetachISOfromServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/iso_detach`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// GetISOStatusofServer retrieves the current ISO image state of an existing virtual machine
+func (c *Client) GetISOStatusofServer(id string) (isoStatus ISOStatus, err error) {
+ if err := c.get(`server/iso_status?SUBID=`+id, &isoStatus); err != nil {
+ return ISOStatus{}, err
+ }
+ return isoStatus, nil
+}
+
+// DeleteServer deletes an existing virtual machine
+func (c *Client) DeleteServer(id string) error {
+ values := url.Values{
+ "SUBID": {id},
+ }
+
+ if err := c.post(`server/destroy`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetFirewallGroup adds a virtual machine to a firewall group
+func (c *Client) SetFirewallGroup(id, firewallgroup string) error {
+ values := url.Values{
+ "SUBID": {id},
+ "FIREWALLGROUPID": {firewallgroup},
+ }
+
+ if err := c.post(`server/firewall_group_set`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// UnsetFirewallGroup removes a virtual machine from a firewall group
+func (c *Client) UnsetFirewallGroup(id string) error {
+ return c.SetFirewallGroup(id, "0")
+}
+
+// BandwidthOfServer retrieves the bandwidth used by a virtual machine
+func (c *Client) BandwidthOfServer(id string) (bandwidth []map[string]string, err error) {
+ var bandwidthMap map[string][][]string
+ if err := c.get(`server/bandwidth?SUBID=`+id, &bandwidthMap); err != nil {
+ return nil, err
+ }
+
+ // parse incoming bytes
+ for _, b := range bandwidthMap["incoming_bytes"] {
+ bMap := make(map[string]string)
+ bMap["date"] = b[0]
+ bMap["incoming"] = b[1]
+ bandwidth = append(bandwidth, bMap)
+ }
+
+ // parse outgoing bytes (we'll assume that incoming and outgoing dates are always a match)
+ for _, b := range bandwidthMap["outgoing_bytes"] {
+ for i := range bandwidth {
+ if bandwidth[i]["date"] == b[0] {
+ bandwidth[i]["outgoing"] = b[1]
+ break
+ }
+ }
+ }
+
+ 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/vendor/github.com/JamesClonk/vultr/lib/snapshots.go b/vendor/github.com/JamesClonk/vultr/lib/snapshots.go
new file mode 100644
index 000000000..7e9969306
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/snapshots.go
@@ -0,0 +1,72 @@
+package lib
+
+import (
+ "net/url"
+ "sort"
+ "strings"
+)
+
+// Snapshot of a virtual machine on Vultr account
+type Snapshot struct {
+ ID string `json:"SNAPSHOTID"`
+ Description string `json:"description"`
+ Size string `json:"size"`
+ Status string `json:"status"`
+ Created string `json:"date_created"`
+}
+
+type snapshots []Snapshot
+
+func (s snapshots) Len() int { return len(s) }
+func (s snapshots) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s snapshots) Less(i, j int) bool {
+ // sort order: description, created
+ if strings.ToLower(s[i].Description) < strings.ToLower(s[j].Description) {
+ return true
+ } else if strings.ToLower(s[i].Description) > strings.ToLower(s[j].Description) {
+ return false
+ }
+ return s[i].Created < s[j].Created
+}
+
+// GetSnapshots retrieves a list of all snapshots on Vultr account
+func (c *Client) GetSnapshots() (snapshotList []Snapshot, err error) {
+ var snapshotMap map[string]Snapshot
+ if err := c.get(`snapshot/list`, &snapshotMap); err != nil {
+ return nil, err
+ }
+
+ for _, snapshot := range snapshotMap {
+ snapshotList = append(snapshotList, snapshot)
+ }
+ sort.Sort(snapshots(snapshotList))
+ return snapshotList, nil
+}
+
+// CreateSnapshot creates a new virtual machine snapshot
+func (c *Client) CreateSnapshot(id, description string) (Snapshot, error) {
+ values := url.Values{
+ "SUBID": {id},
+ "description": {description},
+ }
+
+ var snapshot Snapshot
+ if err := c.post(`snapshot/create`, values, &snapshot); err != nil {
+ return Snapshot{}, err
+ }
+ snapshot.Description = description
+
+ return snapshot, nil
+}
+
+// DeleteSnapshot deletes an existing virtual machine snapshot
+func (c *Client) DeleteSnapshot(id string) error {
+ values := url.Values{
+ "SNAPSHOTID": {id},
+ }
+
+ if err := c.post(`snapshot/destroy`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/JamesClonk/vultr/lib/sshkeys.go b/vendor/github.com/JamesClonk/vultr/lib/sshkeys.go
new file mode 100644
index 000000000..006e16f2d
--- /dev/null
+++ b/vendor/github.com/JamesClonk/vultr/lib/sshkeys.go
@@ -0,0 +1,82 @@
+package lib
+
+import (
+ "net/url"
+ "sort"
+ "strings"
+)
+
+// SSHKey on Vultr account
+type SSHKey struct {
+ ID string `json:"SSHKEYID"`
+ Name string `json:"name"`
+ Key string `json:"ssh_key"`
+ Created string `json:"date_created"`
+}
+
+type sshkeys []SSHKey
+
+func (s sshkeys) Len() int { return len(s) }
+func (s sshkeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s sshkeys) Less(i, j int) bool { return strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name) }
+
+// GetSSHKeys returns a list of SSHKeys from Vultr account
+func (c *Client) GetSSHKeys() (keys []SSHKey, err error) {
+ var keyMap map[string]SSHKey
+ if err := c.get(`sshkey/list`, &keyMap); err != nil {
+ return nil, err
+ }
+
+ for _, key := range keyMap {
+ keys = append(keys, key)
+ }
+ sort.Sort(sshkeys(keys))
+ return keys, nil
+}
+
+// CreateSSHKey creates new SSHKey on Vultr
+func (c *Client) CreateSSHKey(name, key string) (SSHKey, error) {
+ values := url.Values{
+ "name": {name},
+ "ssh_key": {key},
+ }
+
+ var sshKey SSHKey
+ if err := c.post(`sshkey/create`, values, &sshKey); err != nil {
+ return SSHKey{}, err
+ }
+ sshKey.Name = name
+ sshKey.Key = key
+
+ return sshKey, nil
+}
+
+// UpdateSSHKey updates an existing SSHKey entry
+func (c *Client) UpdateSSHKey(key SSHKey) error {
+ values := url.Values{
+ "SSHKEYID": {key.ID},
+ }
+ if key.Name != "" {
+ values.Add("name", key.Name)
+ }
+ if key.Key != "" {
+ values.Add("ssh_key", key.Key)
+ }
+
+ if err := c.post(`sshkey/update`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteSSHKey deletes an existing SSHKey from Vultr account
+func (c *Client) DeleteSSHKey(id string) error {
+ values := url.Values{
+ "SSHKEYID": {id},
+ }
+
+ if err := c.post(`sshkey/destroy`, values, nil); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/juju/ratelimit/LICENSE b/vendor/github.com/juju/ratelimit/LICENSE
new file mode 100644
index 000000000..ade9307b3
--- /dev/null
+++ b/vendor/github.com/juju/ratelimit/LICENSE
@@ -0,0 +1,191 @@
+All files in this repository are licensed as follows. If you contribute
+to this repository, it is assumed that you license your contribution
+under the same license unless you state otherwise.
+
+All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file.
+
+This software is licensed under the LGPLv3, included below.
+
+As a special exception to the GNU Lesser General Public License version 3
+("LGPL3"), the copyright holders of this Library give you permission to
+convey to a third party a Combined Work that links statically or dynamically
+to this Library without providing any Minimal Corresponding Source or
+Minimal Application Code as set out in 4d or providing the installation
+information set out in section 4e, provided that you comply with the other
+provisions of LGPL3 and provided that you meet, for the Application the
+terms and conditions of the license(s) which apply to the Application.
+
+Except as stated in this special exception, the provisions of LGPL3 will
+continue to comply in full to this Library. If you modify this Library, you
+may apply this exception to your version of this Library, but you are not
+obliged to do so. If you do not wish to do so, delete this exception
+statement from your version. This exception does not (and cannot) modify any
+license terms which apply to the Application, with which you must still
+comply.
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/vendor/github.com/juju/ratelimit/README.md b/vendor/github.com/juju/ratelimit/README.md
new file mode 100644
index 000000000..a0fdfe2b1
--- /dev/null
+++ b/vendor/github.com/juju/ratelimit/README.md
@@ -0,0 +1,117 @@
+# ratelimit
+--
+ import "github.com/juju/ratelimit"
+
+The ratelimit package provides an efficient token bucket implementation. See
+http://en.wikipedia.org/wiki/Token_bucket.
+
+## Usage
+
+#### func Reader
+
+```go
+func Reader(r io.Reader, bucket *Bucket) io.Reader
+```
+Reader returns a reader that is rate limited by the given token bucket. Each
+token in the bucket represents one byte.
+
+#### func Writer
+
+```go
+func Writer(w io.Writer, bucket *Bucket) io.Writer
+```
+Writer returns a writer that is rate limited by the given token bucket. Each
+token in the bucket represents one byte.
+
+#### type Bucket
+
+```go
+type Bucket struct {
+}
+```
+
+Bucket represents a token bucket that fills at a predetermined rate. Methods on
+Bucket may be called concurrently.
+
+#### func NewBucket
+
+```go
+func NewBucket(fillInterval time.Duration, capacity int64) *Bucket
+```
+NewBucket returns a new token bucket that fills at the rate of one token every
+fillInterval, up to the given maximum capacity. Both arguments must be positive.
+The bucket is initially full.
+
+#### func NewBucketWithQuantum
+
+```go
+func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket
+```
+NewBucketWithQuantum is similar to NewBucket, but allows the specification of
+the quantum size - quantum tokens are added every fillInterval.
+
+#### func NewBucketWithRate
+
+```go
+func NewBucketWithRate(rate float64, capacity int64) *Bucket
+```
+NewBucketWithRate returns a token bucket that fills the bucket at the rate of
+rate tokens per second up to the given maximum capacity. Because of limited
+clock resolution, at high rates, the actual rate may be up to 1% different from
+the specified rate.
+
+#### func (*Bucket) Rate
+
+```go
+func (tb *Bucket) Rate() float64
+```
+Rate returns the fill rate of the bucket, in tokens per second.
+
+#### func (*Bucket) Take
+
+```go
+func (tb *Bucket) Take(count int64) time.Duration
+```
+Take takes count tokens from the bucket without blocking. It returns the time
+that the caller should wait until the tokens are actually available.
+
+Note that if the request is irrevocable - there is no way to return tokens to
+the bucket once this method commits us to taking them.
+
+#### func (*Bucket) TakeAvailable
+
+```go
+func (tb *Bucket) TakeAvailable(count int64) int64
+```
+TakeAvailable takes up to count immediately available tokens from the bucket. It
+returns the number of tokens removed, or zero if there are no available tokens.
+It does not block.
+
+#### func (*Bucket) TakeMaxDuration
+
+```go
+func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool)
+```
+TakeMaxDuration is like Take, except that it will only take tokens from the
+bucket if the wait time for the tokens is no greater than maxWait.
+
+If it would take longer than maxWait for the tokens to become available, it does
+nothing and reports false, otherwise it returns the time that the caller should
+wait until the tokens are actually available, and reports true.
+
+#### func (*Bucket) Wait
+
+```go
+func (tb *Bucket) Wait(count int64)
+```
+Wait takes count tokens from the bucket, waiting until they are available.
+
+#### func (*Bucket) WaitMaxDuration
+
+```go
+func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool
+```
+WaitMaxDuration is like Wait except that it will only take tokens from the
+bucket if it needs to wait for no greater than maxWait. It reports whether any
+tokens have been removed from the bucket If no tokens have been removed, it
+returns immediately.
diff --git a/vendor/github.com/juju/ratelimit/ratelimit.go b/vendor/github.com/juju/ratelimit/ratelimit.go
new file mode 100644
index 000000000..1c3f25b2e
--- /dev/null
+++ b/vendor/github.com/juju/ratelimit/ratelimit.go
@@ -0,0 +1,284 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3 with static-linking exception.
+// See LICENCE file for details.
+
+// Package ratelimit provides an efficient token bucket implementation
+// that can be used to limit the rate of arbitrary things.
+// See http://en.wikipedia.org/wiki/Token_bucket.
+package ratelimit
+
+import (
+ "math"
+ "strconv"
+ "sync"
+ "time"
+)
+
+// Bucket represents a token bucket that fills at a predetermined rate.
+// Methods on Bucket may be called concurrently.
+type Bucket struct {
+ startTime time.Time
+ capacity int64
+ quantum int64
+ fillInterval time.Duration
+ clock Clock
+
+ // The mutex guards the fields following it.
+ mu sync.Mutex
+
+ // avail holds the number of available tokens
+ // in the bucket, as of availTick ticks from startTime.
+ // It will be negative when there are consumers
+ // waiting for tokens.
+ avail int64
+ availTick int64
+}
+
+// Clock is used to inject testable fakes.
+type Clock interface {
+ Now() time.Time
+ Sleep(d time.Duration)
+}
+
+// realClock implements Clock in terms of standard time functions.
+type realClock struct{}
+
+// Now is identical to time.Now.
+func (realClock) Now() time.Time {
+ return time.Now()
+}
+
+// Sleep is identical to time.Sleep.
+func (realClock) Sleep(d time.Duration) {
+ time.Sleep(d)
+}
+
+// NewBucket returns a new token bucket that fills at the
+// rate of one token every fillInterval, up to the given
+// maximum capacity. Both arguments must be
+// positive. The bucket is initially full.
+func NewBucket(fillInterval time.Duration, capacity int64) *Bucket {
+ return NewBucketWithClock(fillInterval, capacity, realClock{})
+}
+
+// NewBucketWithClock is identical to NewBucket but injects a testable clock
+// interface.
+func NewBucketWithClock(fillInterval time.Duration, capacity int64, clock Clock) *Bucket {
+ return NewBucketWithQuantumAndClock(fillInterval, capacity, 1, clock)
+}
+
+// rateMargin specifes the allowed variance of actual
+// rate from specified rate. 1% seems reasonable.
+const rateMargin = 0.01
+
+// NewBucketWithRate returns a token bucket that fills the bucket
+// at the rate of rate tokens per second up to the given
+// maximum capacity. Because of limited clock resolution,
+// at high rates, the actual rate may be up to 1% different from the
+// specified rate.
+func NewBucketWithRate(rate float64, capacity int64) *Bucket {
+ return NewBucketWithRateAndClock(rate, capacity, realClock{})
+}
+
+// NewBucketWithRateAndClock is identical to NewBucketWithRate but injects a
+// testable clock interface.
+func NewBucketWithRateAndClock(rate float64, capacity int64, clock Clock) *Bucket {
+ for quantum := int64(1); quantum < 1<<50; quantum = nextQuantum(quantum) {
+ fillInterval := time.Duration(1e9 * float64(quantum) / rate)
+ if fillInterval <= 0 {
+ continue
+ }
+ tb := NewBucketWithQuantumAndClock(fillInterval, capacity, quantum, clock)
+ if diff := math.Abs(tb.Rate() - rate); diff/rate <= rateMargin {
+ return tb
+ }
+ }
+ panic("cannot find suitable quantum for " + strconv.FormatFloat(rate, 'g', -1, 64))
+}
+
+// nextQuantum returns the next quantum to try after q.
+// We grow the quantum exponentially, but slowly, so we
+// get a good fit in the lower numbers.
+func nextQuantum(q int64) int64 {
+ q1 := q * 11 / 10
+ if q1 == q {
+ q1++
+ }
+ return q1
+}
+
+// NewBucketWithQuantum is similar to NewBucket, but allows
+// the specification of the quantum size - quantum tokens
+// are added every fillInterval.
+func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket {
+ return NewBucketWithQuantumAndClock(fillInterval, capacity, quantum, realClock{})
+}
+
+// NewBucketWithQuantumAndClock is identical to NewBucketWithQuantum but injects
+// a testable clock interface.
+func NewBucketWithQuantumAndClock(fillInterval time.Duration, capacity, quantum int64, clock Clock) *Bucket {
+ if fillInterval <= 0 {
+ panic("token bucket fill interval is not > 0")
+ }
+ if capacity <= 0 {
+ panic("token bucket capacity is not > 0")
+ }
+ if quantum <= 0 {
+ panic("token bucket quantum is not > 0")
+ }
+ return &Bucket{
+ clock: clock,
+ startTime: clock.Now(),
+ capacity: capacity,
+ quantum: quantum,
+ avail: capacity,
+ fillInterval: fillInterval,
+ }
+}
+
+// Wait takes count tokens from the bucket, waiting until they are
+// available.
+func (tb *Bucket) Wait(count int64) {
+ if d := tb.Take(count); d > 0 {
+ tb.clock.Sleep(d)
+ }
+}
+
+// WaitMaxDuration is like Wait except that it will
+// only take tokens from the bucket if it needs to wait
+// for no greater than maxWait. It reports whether
+// any tokens have been removed from the bucket
+// If no tokens have been removed, it returns immediately.
+func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {
+ d, ok := tb.TakeMaxDuration(count, maxWait)
+ if d > 0 {
+ tb.clock.Sleep(d)
+ }
+ return ok
+}
+
+const infinityDuration time.Duration = 0x7fffffffffffffff
+
+// Take takes count tokens from the bucket without blocking. It returns
+// the time that the caller should wait until the tokens are actually
+// available.
+//
+// Note that if the request is irrevocable - there is no way to return
+// tokens to the bucket once this method commits us to taking them.
+func (tb *Bucket) Take(count int64) time.Duration {
+ d, _ := tb.take(tb.clock.Now(), count, infinityDuration)
+ return d
+}
+
+// TakeMaxDuration is like Take, except that
+// it will only take tokens from the bucket if the wait
+// time for the tokens is no greater than maxWait.
+//
+// If it would take longer than maxWait for the tokens
+// to become available, it does nothing and reports false,
+// otherwise it returns the time that the caller should
+// wait until the tokens are actually available, and reports
+// true.
+func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) {
+ return tb.take(tb.clock.Now(), count, maxWait)
+}
+
+// TakeAvailable takes up to count immediately available tokens from the
+// bucket. It returns the number of tokens removed, or zero if there are
+// no available tokens. It does not block.
+func (tb *Bucket) TakeAvailable(count int64) int64 {
+ return tb.takeAvailable(tb.clock.Now(), count)
+}
+
+// takeAvailable is the internal version of TakeAvailable - it takes the
+// current time as an argument to enable easy testing.
+func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {
+ if count <= 0 {
+ return 0
+ }
+ tb.mu.Lock()
+ defer tb.mu.Unlock()
+
+ tb.adjust(now)
+ if tb.avail <= 0 {
+ return 0
+ }
+ if count > tb.avail {
+ count = tb.avail
+ }
+ tb.avail -= count
+ return count
+}
+
+// Available returns the number of available tokens. It will be negative
+// when there are consumers waiting for tokens. Note that if this
+// returns greater than zero, it does not guarantee that calls that take
+// tokens from the buffer will succeed, as the number of available
+// tokens could have changed in the meantime. This method is intended
+// primarily for metrics reporting and debugging.
+func (tb *Bucket) Available() int64 {
+ return tb.available(tb.clock.Now())
+}
+
+// available is the internal version of available - it takes the current time as
+// an argument to enable easy testing.
+func (tb *Bucket) available(now time.Time) int64 {
+ tb.mu.Lock()
+ defer tb.mu.Unlock()
+ tb.adjust(now)
+ return tb.avail
+}
+
+// Capacity returns the capacity that the bucket was created with.
+func (tb *Bucket) Capacity() int64 {
+ return tb.capacity
+}
+
+// Rate returns the fill rate of the bucket, in tokens per second.
+func (tb *Bucket) Rate() float64 {
+ return 1e9 * float64(tb.quantum) / float64(tb.fillInterval)
+}
+
+// take is the internal version of Take - it takes the current time as
+// an argument to enable easy testing.
+func (tb *Bucket) take(now time.Time, count int64, maxWait time.Duration) (time.Duration, bool) {
+ if count <= 0 {
+ return 0, true
+ }
+ tb.mu.Lock()
+ defer tb.mu.Unlock()
+
+ currentTick := tb.adjust(now)
+ avail := tb.avail - count
+ if avail >= 0 {
+ tb.avail = avail
+ return 0, true
+ }
+ // Round up the missing tokens to the nearest multiple
+ // of quantum - the tokens won't be available until
+ // that tick.
+ endTick := currentTick + (-avail+tb.quantum-1)/tb.quantum
+ endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval)
+ waitTime := endTime.Sub(now)
+ if waitTime > maxWait {
+ return 0, false
+ }
+ tb.avail = avail
+ return waitTime, true
+}
+
+// adjust adjusts the current bucket capacity based on the current time.
+// It returns the current tick.
+func (tb *Bucket) adjust(now time.Time) (currentTick int64) {
+ currentTick = int64(now.Sub(tb.startTime) / tb.fillInterval)
+
+ if tb.avail >= tb.capacity {
+ return
+ }
+ tb.avail += (currentTick - tb.availTick) * tb.quantum
+ if tb.avail > tb.capacity {
+ tb.avail = tb.capacity
+ }
+ tb.availTick = currentTick
+ return
+}
diff --git a/vendor/github.com/juju/ratelimit/reader.go b/vendor/github.com/juju/ratelimit/reader.go
new file mode 100644
index 000000000..6403bf78d
--- /dev/null
+++ b/vendor/github.com/juju/ratelimit/reader.go
@@ -0,0 +1,51 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3 with static-linking exception.
+// See LICENCE file for details.
+
+package ratelimit
+
+import "io"
+
+type reader struct {
+ r io.Reader
+ bucket *Bucket
+}
+
+// Reader returns a reader that is rate limited by
+// the given token bucket. Each token in the bucket
+// represents one byte.
+func Reader(r io.Reader, bucket *Bucket) io.Reader {
+ return &reader{
+ r: r,
+ bucket: bucket,
+ }
+}
+
+func (r *reader) Read(buf []byte) (int, error) {
+ n, err := r.r.Read(buf)
+ if n <= 0 {
+ return n, err
+ }
+ r.bucket.Wait(int64(n))
+ return n, err
+}
+
+type writer struct {
+ w io.Writer
+ bucket *Bucket
+}
+
+// Writer returns a reader that is rate limited by
+// the given token bucket. Each token in the bucket
+// represents one byte.
+func Writer(w io.Writer, bucket *Bucket) io.Writer {
+ return &writer{
+ w: w,
+ bucket: bucket,
+ }
+}
+
+func (w *writer) Write(buf []byte) (int, error) {
+ w.bucket.Wait(int64(len(buf)))
+ return w.w.Write(buf)
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 1924eab8e..a14ace350 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -31,6 +31,12 @@
"revision": "33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4",
"revisionTime": "2013-01-12T09:33:55Z"
},
+ {
+ "checksumSHA1": "E0NbOzBCBYViPKvM4XcloBu6GWE=",
+ "path": "github.com/JamesClonk/vultr/lib",
+ "revision": "2fd0705ce648e602e6c9c57329a174270a4f6688",
+ "revisionTime": "2017-08-08T19:54:39Z"
+ },
{
"checksumSHA1": "GZj+/EV9o0GyIsZ5/S8gSy3ceKM=",
"path": "github.com/TomOnTime/utfutil",
@@ -267,6 +273,12 @@
"revision": "3433f3ea46d9f8019119e7dd41274e112a2359a9",
"revisionTime": "2015-11-17T09:58:22-08:00"
},
+ {
+ "checksumSHA1": "a8Ge6pE7oxux9ZMZVAlyEeGzCng=",
+ "path": "github.com/juju/ratelimit",
+ "revision": "5b9ff866471762aa2ab2dced63c9fb6f53921342",
+ "revisionTime": "2017-05-23T01:21:41Z"
+ },
{
"path": "github.com/kolo/xmlrpc",
"revision": "0826b98aaa29c0766956cb40d45cf7482a597671",