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