github.com/tonnydourado/packer@v0.6.1-0.20140701134019-5d0cd9676a37/builder/digitalocean/api.go (about)

     1  // All of the methods used to communicate with the digital_ocean API
     2  // are here. Their API is on a path to V2, so just plain JSON is used
     3  // in place of a proper client library for now.
     4  
     5  package digitalocean
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"github.com/mitchellh/mapstructure"
    12  	"io/ioutil"
    13  	"log"
    14  	"net/http"
    15  	"net/url"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  )
    20  
    21  const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
    22  
    23  type Image struct {
    24  	Id           uint
    25  	Name         string
    26  	Slug         string
    27  	Distribution string
    28  }
    29  
    30  type ImagesResp struct {
    31  	Images []Image
    32  }
    33  
    34  type Region struct {
    35  	Id   uint
    36  	Name string
    37  	Slug string
    38  }
    39  
    40  type RegionsResp struct {
    41  	Regions []Region
    42  }
    43  
    44  type Size struct {
    45  	Id   uint
    46  	Name string
    47  	Slug string
    48  }
    49  
    50  type SizesResp struct {
    51  	Sizes []Size
    52  }
    53  
    54  type DigitalOceanClient struct {
    55  	// The http client for communicating
    56  	client *http.Client
    57  
    58  	// The base URL of the API
    59  	BaseURL string
    60  
    61  	// Credentials
    62  	ClientID string
    63  	APIKey   string
    64  }
    65  
    66  // Creates a new client for communicating with DO
    67  func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
    68  	c := &DigitalOceanClient{
    69  		client: &http.Client{
    70  			Transport: &http.Transport{
    71  				Proxy: http.ProxyFromEnvironment,
    72  			},
    73  		},
    74  		BaseURL:  DIGITALOCEAN_API_URL,
    75  		ClientID: client,
    76  		APIKey:   key,
    77  	}
    78  	return c
    79  }
    80  
    81  // Creates an SSH Key and returns it's id
    82  func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
    83  	params := url.Values{}
    84  	params.Set("name", name)
    85  	params.Set("ssh_pub_key", pub)
    86  
    87  	body, err := NewRequest(d, "ssh_keys/new", params)
    88  	if err != nil {
    89  		return 0, err
    90  	}
    91  
    92  	// Read the SSH key's ID we just created
    93  	key := body["ssh_key"].(map[string]interface{})
    94  	keyId := key["id"].(float64)
    95  	return uint(keyId), nil
    96  }
    97  
    98  // Destroys an SSH key
    99  func (d DigitalOceanClient) DestroyKey(id uint) error {
   100  	path := fmt.Sprintf("ssh_keys/%v/destroy", id)
   101  	_, err := NewRequest(d, path, url.Values{})
   102  	return err
   103  }
   104  
   105  // Creates a droplet and returns it's id
   106  func (d DigitalOceanClient) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
   107  	params := url.Values{}
   108  	params.Set("name", name)
   109  
   110  	found_size, err := d.Size(size)
   111  	if err != nil {
   112  		return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
   113  	}
   114  
   115  	found_image, err := d.Image(image)
   116  	if err != nil {
   117  		return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
   118  	}
   119  
   120  	found_region, err := d.Region(region)
   121  	if err != nil {
   122  		return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
   123  	}
   124  
   125  	params.Set("size_slug", found_size.Slug)
   126  	params.Set("image_slug", found_image.Slug)
   127  	params.Set("region_slug", found_region.Slug)
   128  	params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
   129  	params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
   130  
   131  	body, err := NewRequest(d, "droplets/new", params)
   132  	if err != nil {
   133  		return 0, err
   134  	}
   135  
   136  	// Read the Droplets ID
   137  	droplet := body["droplet"].(map[string]interface{})
   138  	dropletId := droplet["id"].(float64)
   139  	return uint(dropletId), err
   140  }
   141  
   142  // Destroys a droplet
   143  func (d DigitalOceanClient) DestroyDroplet(id uint) error {
   144  	path := fmt.Sprintf("droplets/%v/destroy", id)
   145  	_, err := NewRequest(d, path, url.Values{})
   146  	return err
   147  }
   148  
   149  // Powers off a droplet
   150  func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
   151  	path := fmt.Sprintf("droplets/%v/power_off", id)
   152  
   153  	_, err := NewRequest(d, path, url.Values{})
   154  
   155  	return err
   156  }
   157  
   158  // Shutsdown a droplet. This is a "soft" shutdown.
   159  func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
   160  	path := fmt.Sprintf("droplets/%v/shutdown", id)
   161  
   162  	_, err := NewRequest(d, path, url.Values{})
   163  
   164  	return err
   165  }
   166  
   167  // Creates a snaphot of a droplet by it's ID
   168  func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
   169  	path := fmt.Sprintf("droplets/%v/snapshot", id)
   170  
   171  	params := url.Values{}
   172  	params.Set("name", name)
   173  
   174  	_, err := NewRequest(d, path, params)
   175  
   176  	return err
   177  }
   178  
   179  // Returns all available images.
   180  func (d DigitalOceanClient) Images() ([]Image, error) {
   181  	resp, err := NewRequest(d, "images", url.Values{})
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	var result ImagesResp
   187  	if err := mapstructure.Decode(resp, &result); err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	return result.Images, nil
   192  }
   193  
   194  // Destroys an image by its ID.
   195  func (d DigitalOceanClient) DestroyImage(id uint) error {
   196  	path := fmt.Sprintf("images/%d/destroy", id)
   197  	_, err := NewRequest(d, path, url.Values{})
   198  	return err
   199  }
   200  
   201  // Returns DO's string representation of status "off" "new" "active" etc.
   202  func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
   203  	path := fmt.Sprintf("droplets/%v", id)
   204  
   205  	body, err := NewRequest(d, path, url.Values{})
   206  	if err != nil {
   207  		return "", "", err
   208  	}
   209  
   210  	var ip string
   211  
   212  	// Read the droplet's "status"
   213  	droplet := body["droplet"].(map[string]interface{})
   214  	status := droplet["status"].(string)
   215  
   216  	if droplet["ip_address"] != nil {
   217  		ip = droplet["ip_address"].(string)
   218  	}
   219  
   220  	return ip, status, err
   221  }
   222  
   223  // Sends an api request and returns a generic map[string]interface of
   224  // the response.
   225  func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
   226  	client := d.client
   227  
   228  	// Add the authentication parameters
   229  	params.Set("client_id", d.ClientID)
   230  	params.Set("api_key", d.APIKey)
   231  
   232  	url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
   233  
   234  	// Do some basic scrubbing so sensitive information doesn't appear in logs
   235  	scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
   236  	scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
   237  	log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
   238  
   239  	var lastErr error
   240  	for attempts := 1; attempts < 10; attempts++ {
   241  		resp, err := client.Get(url)
   242  		if err != nil {
   243  			return nil, err
   244  		}
   245  
   246  		body, err := ioutil.ReadAll(resp.Body)
   247  		resp.Body.Close()
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  
   252  		log.Printf("response from digitalocean: %s", body)
   253  
   254  		var decodedResponse map[string]interface{}
   255  		err = json.Unmarshal(body, &decodedResponse)
   256  		if err != nil {
   257  			err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
   258  				resp.StatusCode, body))
   259  			return decodedResponse, err
   260  		}
   261  
   262  		// Check for errors sent by digitalocean
   263  		status := decodedResponse["status"].(string)
   264  		if status == "OK" {
   265  			return decodedResponse, nil
   266  		}
   267  
   268  		if status == "ERROR" {
   269  			statusRaw, ok := decodedResponse["error_message"]
   270  			if ok {
   271  				status = statusRaw.(string)
   272  			} else {
   273  				status = fmt.Sprintf(
   274  					"Unknown error. Full response body: %s", body)
   275  			}
   276  		}
   277  
   278  		lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
   279  			resp.StatusCode, status))
   280  		log.Println(lastErr)
   281  		if strings.Contains(status, "a pending event") {
   282  			// Retry, DigitalOcean sends these dumb "pending event"
   283  			// errors all the time.
   284  			time.Sleep(5 * time.Second)
   285  			continue
   286  		}
   287  
   288  		// Some other kind of error. Just return.
   289  		return decodedResponse, lastErr
   290  	}
   291  
   292  	return nil, lastErr
   293  }
   294  
   295  func (d DigitalOceanClient) Image(slug_or_name_or_id string) (Image, error) {
   296  	images, err := d.Images()
   297  	if err != nil {
   298  		return Image{}, err
   299  	}
   300  
   301  	for _, image := range images {
   302  		if strings.EqualFold(image.Slug, slug_or_name_or_id) {
   303  			return image, nil
   304  		}
   305  	}
   306  
   307  	for _, image := range images {
   308  		if strings.EqualFold(image.Name, slug_or_name_or_id) {
   309  			return image, nil
   310  		}
   311  	}
   312  
   313  	for _, image := range images {
   314  		id, err := strconv.Atoi(slug_or_name_or_id)
   315  		if err == nil {
   316  			if image.Id == uint(id) {
   317  				return image, nil
   318  			}
   319  		}
   320  	}
   321  
   322  	err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
   323  
   324  	return Image{}, err
   325  }
   326  
   327  // Returns all available regions.
   328  func (d DigitalOceanClient) Regions() ([]Region, error) {
   329  	resp, err := NewRequest(d, "regions", url.Values{})
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  
   334  	var result RegionsResp
   335  	if err := mapstructure.Decode(resp, &result); err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	return result.Regions, nil
   340  }
   341  
   342  func (d DigitalOceanClient) Region(slug_or_name_or_id string) (Region, error) {
   343  	regions, err := d.Regions()
   344  	if err != nil {
   345  		return Region{}, err
   346  	}
   347  
   348  	for _, region := range regions {
   349  		if strings.EqualFold(region.Slug, slug_or_name_or_id) {
   350  			return region, nil
   351  		}
   352  	}
   353  
   354  	for _, region := range regions {
   355  		if strings.EqualFold(region.Name, slug_or_name_or_id) {
   356  			return region, nil
   357  		}
   358  	}
   359  
   360  	for _, region := range regions {
   361  		id, err := strconv.Atoi(slug_or_name_or_id)
   362  		if err == nil {
   363  			if region.Id == uint(id) {
   364  				return region, nil
   365  			}
   366  		}
   367  	}
   368  
   369  	err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
   370  
   371  	return Region{}, err
   372  }
   373  
   374  // Returns all available sizes.
   375  func (d DigitalOceanClient) Sizes() ([]Size, error) {
   376  	resp, err := NewRequest(d, "sizes", url.Values{})
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	var result SizesResp
   382  	if err := mapstructure.Decode(resp, &result); err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	return result.Sizes, nil
   387  }
   388  
   389  func (d DigitalOceanClient) Size(slug_or_name_or_id string) (Size, error) {
   390  	sizes, err := d.Sizes()
   391  	if err != nil {
   392  		return Size{}, err
   393  	}
   394  
   395  	for _, size := range sizes {
   396  		if strings.EqualFold(size.Slug, slug_or_name_or_id) {
   397  			return size, nil
   398  		}
   399  	}
   400  
   401  	for _, size := range sizes {
   402  		if strings.EqualFold(size.Name, slug_or_name_or_id) {
   403  			return size, nil
   404  		}
   405  	}
   406  
   407  	for _, size := range sizes {
   408  		id, err := strconv.Atoi(slug_or_name_or_id)
   409  		if err == nil {
   410  			if size.Id == uint(id) {
   411  				return size, nil
   412  			}
   413  		}
   414  	}
   415  
   416  	err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
   417  
   418  	return Size{}, err
   419  }