github.com/sneal/packer@v0.5.2/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  	"strings"
    17  	"time"
    18  )
    19  
    20  const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
    21  
    22  type Image struct {
    23  	Id           uint
    24  	Name         string
    25  	Distribution string
    26  }
    27  
    28  type ImagesResp struct {
    29  	Images []Image
    30  }
    31  
    32  type Region struct {
    33  	Id   uint
    34  	Name string
    35  }
    36  
    37  type RegionsResp struct {
    38  	Regions []Region
    39  }
    40  
    41  type DigitalOceanClient struct {
    42  	// The http client for communicating
    43  	client *http.Client
    44  
    45  	// The base URL of the API
    46  	BaseURL string
    47  
    48  	// Credentials
    49  	ClientID string
    50  	APIKey   string
    51  }
    52  
    53  // Creates a new client for communicating with DO
    54  func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
    55  	c := &DigitalOceanClient{
    56  		client: &http.Client{
    57  			Transport: &http.Transport{
    58  				Proxy: http.ProxyFromEnvironment,
    59  			},
    60  		},
    61  		BaseURL:  DIGITALOCEAN_API_URL,
    62  		ClientID: client,
    63  		APIKey:   key,
    64  	}
    65  	return c
    66  }
    67  
    68  // Creates an SSH Key and returns it's id
    69  func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
    70  	params := url.Values{}
    71  	params.Set("name", name)
    72  	params.Set("ssh_pub_key", pub)
    73  
    74  	body, err := NewRequest(d, "ssh_keys/new", params)
    75  	if err != nil {
    76  		return 0, err
    77  	}
    78  
    79  	// Read the SSH key's ID we just created
    80  	key := body["ssh_key"].(map[string]interface{})
    81  	keyId := key["id"].(float64)
    82  	return uint(keyId), nil
    83  }
    84  
    85  // Destroys an SSH key
    86  func (d DigitalOceanClient) DestroyKey(id uint) error {
    87  	path := fmt.Sprintf("ssh_keys/%v/destroy", id)
    88  	_, err := NewRequest(d, path, url.Values{})
    89  	return err
    90  }
    91  
    92  // Creates a droplet and returns it's id
    93  func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint, privateNetworking bool) (uint, error) {
    94  	params := url.Values{}
    95  	params.Set("name", name)
    96  	params.Set("size_id", fmt.Sprintf("%v", size))
    97  	params.Set("image_id", fmt.Sprintf("%v", image))
    98  	params.Set("region_id", fmt.Sprintf("%v", region))
    99  	params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
   100  	params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
   101  
   102  	body, err := NewRequest(d, "droplets/new", params)
   103  	if err != nil {
   104  		return 0, err
   105  	}
   106  
   107  	// Read the Droplets ID
   108  	droplet := body["droplet"].(map[string]interface{})
   109  	dropletId := droplet["id"].(float64)
   110  	return uint(dropletId), err
   111  }
   112  
   113  // Destroys a droplet
   114  func (d DigitalOceanClient) DestroyDroplet(id uint) error {
   115  	path := fmt.Sprintf("droplets/%v/destroy", id)
   116  	_, err := NewRequest(d, path, url.Values{})
   117  	return err
   118  }
   119  
   120  // Powers off a droplet
   121  func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
   122  	path := fmt.Sprintf("droplets/%v/power_off", id)
   123  
   124  	_, err := NewRequest(d, path, url.Values{})
   125  
   126  	return err
   127  }
   128  
   129  // Shutsdown a droplet. This is a "soft" shutdown.
   130  func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
   131  	path := fmt.Sprintf("droplets/%v/shutdown", id)
   132  
   133  	_, err := NewRequest(d, path, url.Values{})
   134  
   135  	return err
   136  }
   137  
   138  // Creates a snaphot of a droplet by it's ID
   139  func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
   140  	path := fmt.Sprintf("droplets/%v/snapshot", id)
   141  
   142  	params := url.Values{}
   143  	params.Set("name", name)
   144  
   145  	_, err := NewRequest(d, path, params)
   146  
   147  	return err
   148  }
   149  
   150  // Returns all available images.
   151  func (d DigitalOceanClient) Images() ([]Image, error) {
   152  	resp, err := NewRequest(d, "images", url.Values{})
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	var result ImagesResp
   158  	if err := mapstructure.Decode(resp, &result); err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	return result.Images, nil
   163  }
   164  
   165  // Destroys an image by its ID.
   166  func (d DigitalOceanClient) DestroyImage(id uint) error {
   167  	path := fmt.Sprintf("images/%d/destroy", id)
   168  	_, err := NewRequest(d, path, url.Values{})
   169  	return err
   170  }
   171  
   172  // Returns DO's string representation of status "off" "new" "active" etc.
   173  func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
   174  	path := fmt.Sprintf("droplets/%v", id)
   175  
   176  	body, err := NewRequest(d, path, url.Values{})
   177  	if err != nil {
   178  		return "", "", err
   179  	}
   180  
   181  	var ip string
   182  
   183  	// Read the droplet's "status"
   184  	droplet := body["droplet"].(map[string]interface{})
   185  	status := droplet["status"].(string)
   186  
   187  	if droplet["ip_address"] != nil {
   188  		ip = droplet["ip_address"].(string)
   189  	}
   190  
   191  	return ip, status, err
   192  }
   193  
   194  // Sends an api request and returns a generic map[string]interface of
   195  // the response.
   196  func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
   197  	client := d.client
   198  
   199  	// Add the authentication parameters
   200  	params.Set("client_id", d.ClientID)
   201  	params.Set("api_key", d.APIKey)
   202  
   203  	url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
   204  
   205  	// Do some basic scrubbing so sensitive information doesn't appear in logs
   206  	scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
   207  	scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
   208  	log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
   209  
   210  	var lastErr error
   211  	for attempts := 1; attempts < 10; attempts++ {
   212  		resp, err := client.Get(url)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  
   217  		body, err := ioutil.ReadAll(resp.Body)
   218  		resp.Body.Close()
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  
   223  		log.Printf("response from digitalocean: %s", body)
   224  
   225  		var decodedResponse map[string]interface{}
   226  		err = json.Unmarshal(body, &decodedResponse)
   227  		if err != nil {
   228  			err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
   229  				resp.StatusCode, body))
   230  			return decodedResponse, err
   231  		}
   232  
   233  		// Check for errors sent by digitalocean
   234  		status := decodedResponse["status"].(string)
   235  		if status == "OK" {
   236  			return decodedResponse, nil
   237  		}
   238  
   239  		if status == "ERROR" {
   240  			statusRaw, ok := decodedResponse["error_message"]
   241  			if ok {
   242  				status = statusRaw.(string)
   243  			} else {
   244  				status = fmt.Sprintf(
   245  					"Unknown error. Full response body: %s", body)
   246  			}
   247  		}
   248  
   249  		lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
   250  			resp.StatusCode, status))
   251  		log.Println(lastErr)
   252  		if strings.Contains(status, "a pending event") {
   253  			// Retry, DigitalOcean sends these dumb "pending event"
   254  			// errors all the time.
   255  			time.Sleep(5 * time.Second)
   256  			continue
   257  		}
   258  
   259  		// Some other kind of error. Just return.
   260  		return decodedResponse, lastErr
   261  	}
   262  
   263  	return nil, lastErr
   264  }
   265  
   266  // Returns all available regions.
   267  func (d DigitalOceanClient) Regions() ([]Region, error) {
   268  	resp, err := NewRequest(d, "regions", url.Values{})
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  
   273  	var result RegionsResp
   274  	if err := mapstructure.Decode(resp, &result); err != nil {
   275  		return nil, err
   276  	}
   277  
   278  	return result.Regions, nil
   279  }
   280  
   281  func (d DigitalOceanClient) RegionName(region_id uint) (string, error) {
   282  	regions, err := d.Regions()
   283  	if err != nil {
   284  		return "", err
   285  	}
   286  
   287  	for _, region := range regions {
   288  		if region.Id == region_id {
   289  			return region.Name, nil
   290  		}
   291  	}
   292  
   293  	err = errors.New(fmt.Sprintf("Unknown region id %v", region_id))
   294  
   295  	return "", err
   296  }