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