2014-09-03 17:37:33 -04:00
|
|
|
// 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 (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type DigitalOceanClientV2 struct {
|
|
|
|
// The http client for communicating
|
|
|
|
client *http.Client
|
|
|
|
|
|
|
|
// Credentials
|
|
|
|
APIToken string
|
|
|
|
|
|
|
|
// The base URL of the API
|
|
|
|
APIURL string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a new client for communicating with DO
|
|
|
|
func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
|
|
|
|
c := &DigitalOceanClientV2{
|
|
|
|
client: &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
Proxy: http.ProxyFromEnvironment,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
APIURL: url,
|
|
|
|
APIToken: token,
|
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates an SSH Key and returns it's id
|
|
|
|
func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
|
|
|
|
type KeyReq struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
PublicKey string `json:"public_key"`
|
|
|
|
}
|
|
|
|
type KeyRes struct {
|
|
|
|
SSHKey struct {
|
|
|
|
Id uint
|
|
|
|
Name string
|
|
|
|
Fingerprint string
|
|
|
|
PublicKey string `json:"public_key"`
|
|
|
|
} `json:"ssh_key"`
|
|
|
|
}
|
|
|
|
req := &KeyReq{Name: name, PublicKey: pub}
|
|
|
|
res := KeyRes{}
|
|
|
|
err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.SSHKey.Id, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroys an SSH key
|
|
|
|
func (d DigitalOceanClientV2) DestroyKey(id uint) error {
|
|
|
|
path := fmt.Sprintf("v2/account/keys/%v", id)
|
|
|
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a droplet and returns it's id
|
|
|
|
func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
|
|
|
type DropletReq struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Region string `json:"region"`
|
|
|
|
Size string `json:"size"`
|
|
|
|
Image string `json:"image"`
|
|
|
|
SSHKeys []string `json:"ssh_keys,omitempty"`
|
|
|
|
Backups bool `json:"backups,omitempty"`
|
|
|
|
IPv6 bool `json:"ipv6,omitempty"`
|
|
|
|
PrivateNetworking bool `json:"private_networking,omitempty"`
|
|
|
|
}
|
|
|
|
type DropletRes struct {
|
|
|
|
Droplet struct {
|
|
|
|
Id uint
|
|
|
|
Name string
|
|
|
|
Memory uint
|
|
|
|
VCPUS uint `json:"vcpus"`
|
|
|
|
Disk uint
|
|
|
|
Region Region
|
|
|
|
Image Image
|
|
|
|
Size Size
|
|
|
|
Locked bool
|
|
|
|
CreateAt string `json:"created_at"`
|
|
|
|
Status string
|
|
|
|
Networks struct {
|
|
|
|
V4 []struct {
|
|
|
|
IPAddr string `json:"ip_address"`
|
|
|
|
Netmask string
|
|
|
|
Gateway string
|
|
|
|
Type string
|
|
|
|
} `json:"v4,omitempty"`
|
|
|
|
V6 []struct {
|
|
|
|
IPAddr string `json:"ip_address"`
|
|
|
|
CIDR uint `json:"cidr"`
|
|
|
|
Gateway string
|
|
|
|
Type string
|
|
|
|
} `json:"v6,omitempty"`
|
|
|
|
}
|
|
|
|
Kernel struct {
|
|
|
|
Id uint
|
|
|
|
Name string
|
|
|
|
Version string
|
|
|
|
}
|
|
|
|
BackupIds []uint
|
|
|
|
SnapshotIds []uint
|
|
|
|
ActionIds []uint
|
|
|
|
Features []string `json:"features,omitempty"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
req := &DropletReq{Name: name}
|
|
|
|
res := DropletRes{}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Size = found_size.Slug
|
|
|
|
req.Image = found_image.Slug
|
|
|
|
req.Region = found_region.Slug
|
|
|
|
req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
|
|
|
|
req.PrivateNetworking = privateNetworking
|
|
|
|
|
|
|
|
err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.Droplet.Id, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroys a droplet
|
|
|
|
func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
|
|
|
|
path := fmt.Sprintf("v2/droplets/%v", id)
|
|
|
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Powers off a droplet
|
|
|
|
func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
|
|
|
|
type ActionReq struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
}
|
|
|
|
type ActionRes struct {
|
|
|
|
}
|
|
|
|
req := &ActionReq{Type: "power_off"}
|
|
|
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
|
|
return NewRequestV2(d, path, "POST", req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shutsdown a droplet. This is a "soft" shutdown.
|
|
|
|
func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
|
|
|
|
type ActionReq struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
}
|
|
|
|
type ActionRes struct {
|
|
|
|
}
|
|
|
|
req := &ActionReq{Type: "shutdown"}
|
|
|
|
|
|
|
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
|
|
return NewRequestV2(d, path, "POST", req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a snaphot of a droplet by it's ID
|
|
|
|
func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
|
|
|
|
type ActionReq struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
}
|
|
|
|
type ActionRes struct {
|
|
|
|
}
|
|
|
|
req := &ActionReq{Type: "snapshot", Name: name}
|
|
|
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
|
|
return NewRequestV2(d, path, "POST", req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns all available images.
|
|
|
|
func (d DigitalOceanClientV2) Images() ([]Image, error) {
|
|
|
|
res := ImagesResp{}
|
|
|
|
|
2014-10-28 09:05:37 -04:00
|
|
|
err := NewRequestV2(d, "v2/images?per_page=200", "GET", nil, &res)
|
2014-09-03 17:37:33 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.Images, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroys an image by its ID.
|
|
|
|
func (d DigitalOceanClientV2) DestroyImage(id uint) error {
|
|
|
|
path := fmt.Sprintf("v2/images/%d", id)
|
|
|
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns DO's string representation of status "off" "new" "active" etc.
|
|
|
|
func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
|
|
|
|
path := fmt.Sprintf("v2/droplets/%v", id)
|
|
|
|
type DropletRes struct {
|
|
|
|
Droplet struct {
|
|
|
|
Id uint
|
|
|
|
Name string
|
|
|
|
Memory uint
|
|
|
|
VCPUS uint `json:"vcpus"`
|
|
|
|
Disk uint
|
|
|
|
Region Region
|
|
|
|
Image Image
|
|
|
|
Size Size
|
|
|
|
Locked bool
|
|
|
|
CreateAt string `json:"created_at"`
|
|
|
|
Status string
|
|
|
|
Networks struct {
|
|
|
|
V4 []struct {
|
|
|
|
IPAddr string `json:"ip_address"`
|
|
|
|
Netmask string
|
|
|
|
Gateway string
|
|
|
|
Type string
|
|
|
|
} `json:"v4,omitempty"`
|
|
|
|
V6 []struct {
|
|
|
|
IPAddr string `json:"ip_address"`
|
|
|
|
CIDR uint `json:"cidr"`
|
|
|
|
Gateway string
|
|
|
|
Type string
|
|
|
|
} `json:"v6,omitempty"`
|
|
|
|
}
|
|
|
|
Kernel struct {
|
|
|
|
Id uint
|
|
|
|
Name string
|
|
|
|
Version string
|
|
|
|
}
|
|
|
|
BackupIds []uint
|
|
|
|
SnapshotIds []uint
|
|
|
|
ActionIds []uint
|
|
|
|
Features []string `json:"features,omitempty"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
res := DropletRes{}
|
|
|
|
err := NewRequestV2(d, path, "GET", nil, &res)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
var ip string
|
|
|
|
|
2014-11-12 14:57:29 -05:00
|
|
|
for _, n := range res.Droplet.Networks.V4 {
|
|
|
|
if n.Type == "public" {
|
|
|
|
ip = n.IPAddr
|
|
|
|
}
|
2014-09-03 17:37:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return ip, res.Droplet.Status, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sends an api request and returns a generic map[string]interface of
|
|
|
|
// the response.
|
|
|
|
func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
|
|
|
|
var err error
|
|
|
|
var request *http.Request
|
|
|
|
|
|
|
|
client := d.client
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
// Add the authentication parameters
|
|
|
|
url := fmt.Sprintf("%s/%s", d.APIURL, path)
|
|
|
|
if req != nil {
|
|
|
|
enc := json.NewEncoder(buf)
|
|
|
|
enc.Encode(req)
|
|
|
|
defer buf.Reset()
|
|
|
|
request, err = http.NewRequest(method, url, buf)
|
2014-10-30 15:16:21 -04:00
|
|
|
request.Header.Add("Content-Type", "application/json")
|
2014-09-03 17:37:33 -04:00
|
|
|
} else {
|
|
|
|
request, err = http.NewRequest(method, url, nil)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-10-30 15:16:21 -04:00
|
|
|
|
2014-09-03 17:37:33 -04:00
|
|
|
// Add the authentication parameters
|
|
|
|
request.Header.Add("Authorization", "Bearer "+d.APIToken)
|
2014-10-30 15:16:21 -04:00
|
|
|
if buf != nil {
|
|
|
|
log.Printf("sending new request to digitalocean: %s buffer: %s", url, buf)
|
|
|
|
} else {
|
|
|
|
log.Printf("sending new request to digitalocean: %s", url)
|
|
|
|
}
|
2014-09-03 17:37:33 -04:00
|
|
|
resp, err := client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if method == "DELETE" && resp.StatusCode == 204 {
|
|
|
|
if resp.Body != nil {
|
|
|
|
resp.Body.Close()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.Body == nil {
|
|
|
|
return errors.New("Request returned empty body")
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
resp.Body.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("response from digitalocean: %s", body)
|
|
|
|
|
|
|
|
err = json.Unmarshal(body, &res)
|
|
|
|
if err != nil {
|
|
|
|
return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
|
|
|
|
resp.StatusCode, body))
|
|
|
|
}
|
2014-10-30 15:16:21 -04:00
|
|
|
switch resp.StatusCode {
|
2014-11-02 10:47:11 -05:00
|
|
|
case 403, 401, 429, 422, 404, 503, 500:
|
2014-10-30 15:16:21 -04:00
|
|
|
return errors.New(fmt.Sprintf("digitalocean request error: %+v", res))
|
|
|
|
}
|
2014-09-03 17:37:33 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Regions() ([]Region, error) {
|
|
|
|
res := RegionsResp{}
|
2014-10-28 09:05:37 -04:00
|
|
|
err := NewRequestV2(d, "v2/regions?per_page=200", "GET", nil, &res)
|
2014-09-03 17:37:33 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.Regions, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Sizes() ([]Size, error) {
|
|
|
|
res := SizesResp{}
|
2014-10-28 09:05:37 -04:00
|
|
|
err := NewRequestV2(d, "v2/sizes?per_page=200", "GET", nil, &res)
|
2014-09-03 17:37:33 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.Sizes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d DigitalOceanClientV2) 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
|
|
|
|
}
|