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 @@ + + + Registrar @@ -98,6 +102,9 @@ + + + DNS Provider @@ -137,6 +144,9 @@ + + + ALIAS @@ -162,6 +172,9 @@ + + + SRV @@ -197,6 +210,9 @@ + + + PTR @@ -228,6 +244,9 @@ + + + CAA @@ -253,6 +272,9 @@ + + + TLSA @@ -272,6 +294,9 @@ + + + dual host @@ -305,6 +330,7 @@ + create-domains @@ -344,6 +370,9 @@ + + + no_purge @@ -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",