-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Vasiliy Tolstov <v.tolstov@selfip.ru>
- Loading branch information
Showing
12 changed files
with
916 additions
and
419 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,382 @@ | ||
// All of the methods used to communicate with the digital_ocean API | ||
// are here. Their API is on a path to V2, so just plain JSON is used | ||
// in place of a proper client library for now. | ||
|
||
package digitalocean | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
type DigitalOceanClientV1 struct { | ||
// The http client for communicating | ||
client *http.Client | ||
|
||
// Credentials | ||
ClientID string | ||
APIKey string | ||
// The base URL of the API | ||
APIURL string | ||
} | ||
|
||
// Creates a new client for communicating with DO | ||
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 { | ||
c := &DigitalOceanClientV1{ | ||
client: &http.Client{ | ||
Transport: &http.Transport{ | ||
Proxy: http.ProxyFromEnvironment, | ||
}, | ||
}, | ||
APIURL: url, | ||
ClientID: client, | ||
APIKey: key, | ||
} | ||
return c | ||
} | ||
|
||
// Creates an SSH Key and returns it's id | ||
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) { | ||
params := url.Values{} | ||
params.Set("name", name) | ||
params.Set("ssh_pub_key", pub) | ||
|
||
body, err := NewRequestV1(d, "ssh_keys/new", params) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// Read the SSH key's ID we just created | ||
key := body["ssh_key"].(map[string]interface{}) | ||
keyId := key["id"].(float64) | ||
return uint(keyId), nil | ||
} | ||
|
||
// Destroys an SSH key | ||
func (d DigitalOceanClientV1) DestroyKey(id uint) error { | ||
path := fmt.Sprintf("ssh_keys/%v/destroy", id) | ||
_, err := NewRequestV1(d, path, url.Values{}) | ||
return err | ||
} | ||
|
||
// Creates a droplet and returns it's id | ||
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) { | ||
params := url.Values{} | ||
params.Set("name", name) | ||
|
||
found_size, err := d.Size(size) | ||
if err != nil { | ||
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err) | ||
} | ||
|
||
found_image, err := d.Image(image) | ||
if err != nil { | ||
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err) | ||
} | ||
|
||
found_region, err := d.Region(region) | ||
if err != nil { | ||
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err) | ||
} | ||
|
||
params.Set("size_slug", found_size.Slug) | ||
params.Set("image_slug", found_image.Slug) | ||
params.Set("region_slug", found_region.Slug) | ||
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId)) | ||
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking)) | ||
|
||
body, err := NewRequestV1(d, "droplets/new", params) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// Read the Droplets ID | ||
droplet := body["droplet"].(map[string]interface{}) | ||
dropletId := droplet["id"].(float64) | ||
return uint(dropletId), err | ||
} | ||
|
||
// Destroys a droplet | ||
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error { | ||
path := fmt.Sprintf("droplets/%v/destroy", id) | ||
_, err := NewRequestV1(d, path, url.Values{}) | ||
return err | ||
} | ||
|
||
// Powers off a droplet | ||
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error { | ||
path := fmt.Sprintf("droplets/%v/power_off", id) | ||
_, err := NewRequestV1(d, path, url.Values{}) | ||
return err | ||
} | ||
|
||
// Shutsdown a droplet. This is a "soft" shutdown. | ||
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error { | ||
path := fmt.Sprintf("droplets/%v/shutdown", id) | ||
_, err := NewRequestV1(d, path, url.Values{}) | ||
return err | ||
} | ||
|
||
// Creates a snaphot of a droplet by it's ID | ||
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error { | ||
path := fmt.Sprintf("droplets/%v/snapshot", id) | ||
|
||
params := url.Values{} | ||
params.Set("name", name) | ||
|
||
_, err := NewRequestV1(d, path, params) | ||
|
||
return err | ||
} | ||
|
||
// Returns all available images. | ||
func (d DigitalOceanClientV1) Images() ([]Image, error) { | ||
resp, err := NewRequestV1(d, "images", url.Values{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var result ImagesResp | ||
if err := mapstructure.Decode(resp, &result); err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.Images, nil | ||
} | ||
|
||
// Destroys an image by its ID. | ||
func (d DigitalOceanClientV1) DestroyImage(id uint) error { | ||
path := fmt.Sprintf("images/%d/destroy", id) | ||
_, err := NewRequestV1(d, path, url.Values{}) | ||
return err | ||
} | ||
|
||
// Returns DO's string representation of status "off" "new" "active" etc. | ||
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) { | ||
path := fmt.Sprintf("droplets/%v", id) | ||
|
||
body, err := NewRequestV1(d, path, url.Values{}) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
var ip string | ||
|
||
// Read the droplet's "status" | ||
droplet := body["droplet"].(map[string]interface{}) | ||
status := droplet["status"].(string) | ||
|
||
if droplet["ip_address"] != nil { | ||
ip = droplet["ip_address"].(string) | ||
} | ||
|
||
return ip, status, err | ||
} | ||
|
||
// Sends an api request and returns a generic map[string]interface of | ||
// the response. | ||
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) { | ||
client := d.client | ||
|
||
// Add the authentication parameters | ||
params.Set("client_id", d.ClientID) | ||
params.Set("api_key", d.APIKey) | ||
|
||
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode()) | ||
|
||
// Do some basic scrubbing so sensitive information doesn't appear in logs | ||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1) | ||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1) | ||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl) | ||
|
||
var lastErr error | ||
for attempts := 1; attempts < 10; attempts++ { | ||
resp, err := client.Get(url) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
body, err := ioutil.ReadAll(resp.Body) | ||
resp.Body.Close() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
log.Printf("response from digitalocean: %s", body) | ||
|
||
var decodedResponse map[string]interface{} | ||
err = json.Unmarshal(body, &decodedResponse) | ||
if err != nil { | ||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s", | ||
resp.StatusCode, body)) | ||
return decodedResponse, err | ||
} | ||
|
||
// Check for errors sent by digitalocean | ||
status := decodedResponse["status"].(string) | ||
if status == "OK" { | ||
return decodedResponse, nil | ||
} | ||
|
||
if status == "ERROR" { | ||
statusRaw, ok := decodedResponse["error_message"] | ||
if ok { | ||
status = statusRaw.(string) | ||
} else { | ||
status = fmt.Sprintf( | ||
"Unknown error. Full response body: %s", body) | ||
} | ||
} | ||
|
||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s", | ||
resp.StatusCode, status)) | ||
log.Println(lastErr) | ||
if strings.Contains(status, "a pending event") { | ||
// Retry, DigitalOcean sends these dumb "pending event" | ||
// errors all the time. | ||
time.Sleep(5 * time.Second) | ||
continue | ||
} | ||
|
||
// Some other kind of error. Just return. | ||
return decodedResponse, lastErr | ||
} | ||
|
||
return nil, lastErr | ||
} | ||
|
||
func (d DigitalOceanClientV1) Image(slug_or_name_or_id string) (Image, error) { | ||
images, err := d.Images() | ||
if err != nil { | ||
return Image{}, err | ||
} | ||
|
||
for _, image := range images { | ||
if strings.EqualFold(image.Slug, slug_or_name_or_id) { | ||
return image, nil | ||
} | ||
} | ||
|
||
for _, image := range images { | ||
if strings.EqualFold(image.Name, slug_or_name_or_id) { | ||
return image, nil | ||
} | ||
} | ||
|
||
for _, image := range images { | ||
id, err := strconv.Atoi(slug_or_name_or_id) | ||
if err == nil { | ||
if image.Id == uint(id) { | ||
return image, nil | ||
} | ||
} | ||
} | ||
|
||
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id)) | ||
|
||
return Image{}, err | ||
} | ||
|
||
// Returns all available regions. | ||
func (d DigitalOceanClientV1) Regions() ([]Region, error) { | ||
resp, err := NewRequestV1(d, "regions", url.Values{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var result RegionsResp | ||
if err := mapstructure.Decode(resp, &result); err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.Regions, nil | ||
} | ||
|
||
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) { | ||
regions, err := d.Regions() | ||
if err != nil { | ||
return Region{}, err | ||
} | ||
|
||
for _, region := range regions { | ||
if strings.EqualFold(region.Slug, slug_or_name_or_id) { | ||
return region, nil | ||
} | ||
} | ||
|
||
for _, region := range regions { | ||
if strings.EqualFold(region.Name, slug_or_name_or_id) { | ||
return region, nil | ||
} | ||
} | ||
|
||
for _, region := range regions { | ||
id, err := strconv.Atoi(slug_or_name_or_id) | ||
if err == nil { | ||
if region.Id == uint(id) { | ||
return region, nil | ||
} | ||
} | ||
} | ||
|
||
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id)) | ||
|
||
return Region{}, err | ||
} | ||
|
||
// Returns all available sizes. | ||
func (d DigitalOceanClientV1) Sizes() ([]Size, error) { | ||
resp, err := NewRequestV1(d, "sizes", url.Values{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var result SizesResp | ||
if err := mapstructure.Decode(resp, &result); err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.Sizes, nil | ||
} | ||
|
||
func (d DigitalOceanClientV1) Size(slug_or_name_or_id string) (Size, error) { | ||
sizes, err := d.Sizes() | ||
if err != nil { | ||
return Size{}, err | ||
} | ||
|
||
for _, size := range sizes { | ||
if strings.EqualFold(size.Slug, slug_or_name_or_id) { | ||
return size, nil | ||
} | ||
} | ||
|
||
for _, size := range sizes { | ||
if strings.EqualFold(size.Name, slug_or_name_or_id) { | ||
return size, nil | ||
} | ||
} | ||
|
||
for _, size := range sizes { | ||
id, err := strconv.Atoi(slug_or_name_or_id) | ||
if err == nil { | ||
if size.Id == uint(id) { | ||
return size, nil | ||
} | ||
} | ||
} | ||
|
||
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id)) | ||
|
||
return Size{}, err | ||
} |
Oops, something went wrong.