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