github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/builder/digitalocean/api_v2.go (about)

     1  // are here. Their API is on a path to V2, so just plain JSON is used
     2  // in place of a proper client library for now.
     3  
     4  package digitalocean
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"log"
    13  	"net/http"
    14  	"strconv"
    15  	"strings"
    16  )
    17  
    18  type DigitalOceanClientV2 struct {
    19  	// The http client for communicating
    20  	client *http.Client
    21  
    22  	// Credentials
    23  	APIToken string
    24  
    25  	// The base URL of the API
    26  	APIURL string
    27  }
    28  
    29  // Creates a new client for communicating with DO
    30  func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
    31  	c := &DigitalOceanClientV2{
    32  		client: &http.Client{
    33  			Transport: &http.Transport{
    34  				Proxy: http.ProxyFromEnvironment,
    35  			},
    36  		},
    37  		APIURL:   url,
    38  		APIToken: token,
    39  	}
    40  	return c
    41  }
    42  
    43  // Creates an SSH Key and returns it's id
    44  func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
    45  	type KeyReq struct {
    46  		Name      string `json:"name"`
    47  		PublicKey string `json:"public_key"`
    48  	}
    49  	type KeyRes struct {
    50  		SSHKey struct {
    51  			Id          uint
    52  			Name        string
    53  			Fingerprint string
    54  			PublicKey   string `json:"public_key"`
    55  		} `json:"ssh_key"`
    56  	}
    57  	req := &KeyReq{Name: name, PublicKey: pub}
    58  	res := KeyRes{}
    59  	err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
    60  	if err != nil {
    61  		return 0, err
    62  	}
    63  
    64  	return res.SSHKey.Id, err
    65  }
    66  
    67  // Destroys an SSH key
    68  func (d DigitalOceanClientV2) DestroyKey(id uint) error {
    69  	path := fmt.Sprintf("v2/account/keys/%v", id)
    70  	return NewRequestV2(d, path, "DELETE", nil, nil)
    71  }
    72  
    73  // Creates a droplet and returns it's id
    74  func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
    75  	type DropletReq struct {
    76  		Name              string   `json:"name"`
    77  		Region            string   `json:"region"`
    78  		Size              string   `json:"size"`
    79  		Image             string   `json:"image"`
    80  		SSHKeys           []string `json:"ssh_keys,omitempty"`
    81  		Backups           bool     `json:"backups,omitempty"`
    82  		IPv6              bool     `json:"ipv6,omitempty"`
    83  		PrivateNetworking bool     `json:"private_networking,omitempty"`
    84  	}
    85  	type DropletRes struct {
    86  		Droplet struct {
    87  			Id       uint
    88  			Name     string
    89  			Memory   uint
    90  			VCPUS    uint `json:"vcpus"`
    91  			Disk     uint
    92  			Region   Region
    93  			Image    Image
    94  			Size     Size
    95  			Locked   bool
    96  			CreateAt string `json:"created_at"`
    97  			Status   string
    98  			Networks struct {
    99  				V4 []struct {
   100  					IPAddr  string `json:"ip_address"`
   101  					Netmask string
   102  					Gateway string
   103  					Type    string
   104  				} `json:"v4,omitempty"`
   105  				V6 []struct {
   106  					IPAddr  string `json:"ip_address"`
   107  					CIDR    uint   `json:"cidr"`
   108  					Gateway string
   109  					Type    string
   110  				} `json:"v6,omitempty"`
   111  			}
   112  			Kernel struct {
   113  				Id      uint
   114  				Name    string
   115  				Version string
   116  			}
   117  			BackupIds   []uint
   118  			SnapshotIds []uint
   119  			ActionIds   []uint
   120  			Features    []string `json:"features,omitempty"`
   121  		}
   122  	}
   123  	req := &DropletReq{Name: name}
   124  	res := DropletRes{}
   125  
   126  	found_size, err := d.Size(size)
   127  	if err != nil {
   128  		return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
   129  	}
   130  
   131  	found_image, err := d.Image(image)
   132  	if err != nil {
   133  		return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
   134  	}
   135  
   136  	found_region, err := d.Region(region)
   137  	if err != nil {
   138  		return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
   139  	}
   140  
   141  	req.Size = found_size.Slug
   142  	req.Image = found_image.Slug
   143  	req.Region = found_region.Slug
   144  	req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
   145  	req.PrivateNetworking = privateNetworking
   146  
   147  	err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
   148  	if err != nil {
   149  		return 0, err
   150  	}
   151  
   152  	return res.Droplet.Id, err
   153  }
   154  
   155  // Destroys a droplet
   156  func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
   157  	path := fmt.Sprintf("v2/droplets/%v", id)
   158  	return NewRequestV2(d, path, "DELETE", nil, nil)
   159  }
   160  
   161  // Powers off a droplet
   162  func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
   163  	type ActionReq struct {
   164  		Type string `json:"type"`
   165  	}
   166  	type ActionRes struct {
   167  	}
   168  	req := &ActionReq{Type: "power_off"}
   169  	path := fmt.Sprintf("v2/droplets/%v/actions", id)
   170  	return NewRequestV2(d, path, "POST", req, nil)
   171  }
   172  
   173  // Shutsdown a droplet. This is a "soft" shutdown.
   174  func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
   175  	type ActionReq struct {
   176  		Type string `json:"type"`
   177  	}
   178  	type ActionRes struct {
   179  	}
   180  	req := &ActionReq{Type: "shutdown"}
   181  
   182  	path := fmt.Sprintf("v2/droplets/%v/actions", id)
   183  	return NewRequestV2(d, path, "POST", req, nil)
   184  }
   185  
   186  // Creates a snaphot of a droplet by it's ID
   187  func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
   188  	type ActionReq struct {
   189  		Type string `json:"type"`
   190  		Name string `json:"name"`
   191  	}
   192  	type ActionRes struct {
   193  	}
   194  	req := &ActionReq{Type: "snapshot", Name: name}
   195  	path := fmt.Sprintf("v2/droplets/%v/actions", id)
   196  	return NewRequestV2(d, path, "POST", req, nil)
   197  }
   198  
   199  // Returns all available images.
   200  func (d DigitalOceanClientV2) Images() ([]Image, error) {
   201  	res := ImagesResp{}
   202  
   203  	err := NewRequestV2(d, "v2/images?per_page=200", "GET", nil, &res)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	return res.Images, nil
   209  }
   210  
   211  // Destroys an image by its ID.
   212  func (d DigitalOceanClientV2) DestroyImage(id uint) error {
   213  	path := fmt.Sprintf("v2/images/%d", id)
   214  	return NewRequestV2(d, path, "DELETE", nil, nil)
   215  }
   216  
   217  // Returns DO's string representation of status "off" "new" "active" etc.
   218  func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
   219  	path := fmt.Sprintf("v2/droplets/%v", id)
   220  	type DropletRes struct {
   221  		Droplet struct {
   222  			Id       uint
   223  			Name     string
   224  			Memory   uint
   225  			VCPUS    uint `json:"vcpus"`
   226  			Disk     uint
   227  			Region   Region
   228  			Image    Image
   229  			Size     Size
   230  			Locked   bool
   231  			CreateAt string `json:"created_at"`
   232  			Status   string
   233  			Networks struct {
   234  				V4 []struct {
   235  					IPAddr  string `json:"ip_address"`
   236  					Netmask string
   237  					Gateway string
   238  					Type    string
   239  				} `json:"v4,omitempty"`
   240  				V6 []struct {
   241  					IPAddr  string `json:"ip_address"`
   242  					CIDR    uint   `json:"cidr"`
   243  					Gateway string
   244  					Type    string
   245  				} `json:"v6,omitempty"`
   246  			}
   247  			Kernel struct {
   248  				Id      uint
   249  				Name    string
   250  				Version string
   251  			}
   252  			BackupIds   []uint
   253  			SnapshotIds []uint
   254  			ActionIds   []uint
   255  			Features    []string `json:"features,omitempty"`
   256  		}
   257  	}
   258  	res := DropletRes{}
   259  	err := NewRequestV2(d, path, "GET", nil, &res)
   260  	if err != nil {
   261  		return "", "", err
   262  	}
   263  	var ip string
   264  
   265  	for _, n := range res.Droplet.Networks.V4 {
   266  		if n.Type == "public" {
   267  			ip = n.IPAddr
   268  		}
   269  	}
   270  
   271  	return ip, res.Droplet.Status, err
   272  }
   273  
   274  // Sends an api request and returns a generic map[string]interface of
   275  // the response.
   276  func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
   277  	var err error
   278  	var request *http.Request
   279  
   280  	client := d.client
   281  
   282  	buf := new(bytes.Buffer)
   283  	// Add the authentication parameters
   284  	url := fmt.Sprintf("%s/%s", d.APIURL, path)
   285  	if req != nil {
   286  		enc := json.NewEncoder(buf)
   287  		enc.Encode(req)
   288  		defer buf.Reset()
   289  		request, err = http.NewRequest(method, url, buf)
   290  		request.Header.Add("Content-Type", "application/json")
   291  	} else {
   292  		request, err = http.NewRequest(method, url, nil)
   293  	}
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	// Add the authentication parameters
   299  	request.Header.Add("Authorization", "Bearer "+d.APIToken)
   300  	if buf != nil {
   301  		log.Printf("sending new request to digitalocean: %s buffer: %s", url, buf)
   302  	} else {
   303  		log.Printf("sending new request to digitalocean: %s", url)
   304  	}
   305  	resp, err := client.Do(request)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	if method == "DELETE" && resp.StatusCode == 204 {
   311  		if resp.Body != nil {
   312  			resp.Body.Close()
   313  		}
   314  		return nil
   315  	}
   316  
   317  	if resp.Body == nil {
   318  		return errors.New("Request returned empty body")
   319  	}
   320  
   321  	body, err := ioutil.ReadAll(resp.Body)
   322  	resp.Body.Close()
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	log.Printf("response from digitalocean: %s", body)
   328  
   329  	err = json.Unmarshal(body, &res)
   330  	if err != nil {
   331  		return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
   332  			resp.StatusCode, body))
   333  	}
   334  	switch resp.StatusCode {
   335  	case 403, 401, 429, 422, 404, 503, 500:
   336  		return errors.New(fmt.Sprintf("digitalocean request error: %+v", res))
   337  	}
   338  	return nil
   339  }
   340  
   341  func (d DigitalOceanClientV2) Image(slug_or_name_or_id string) (Image, error) {
   342  	images, err := d.Images()
   343  	if err != nil {
   344  		return Image{}, err
   345  	}
   346  
   347  	for _, image := range images {
   348  		if strings.EqualFold(image.Slug, slug_or_name_or_id) {
   349  			return image, nil
   350  		}
   351  	}
   352  
   353  	for _, image := range images {
   354  		if strings.EqualFold(image.Name, slug_or_name_or_id) {
   355  			return image, nil
   356  		}
   357  	}
   358  
   359  	for _, image := range images {
   360  		id, err := strconv.Atoi(slug_or_name_or_id)
   361  		if err == nil {
   362  			if image.Id == uint(id) {
   363  				return image, nil
   364  			}
   365  		}
   366  	}
   367  
   368  	err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
   369  
   370  	return Image{}, err
   371  }
   372  
   373  // Returns all available regions.
   374  func (d DigitalOceanClientV2) Regions() ([]Region, error) {
   375  	res := RegionsResp{}
   376  	err := NewRequestV2(d, "v2/regions?per_page=200", "GET", nil, &res)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	return res.Regions, nil
   382  }
   383  
   384  func (d DigitalOceanClientV2) Region(slug_or_name_or_id string) (Region, error) {
   385  	regions, err := d.Regions()
   386  	if err != nil {
   387  		return Region{}, err
   388  	}
   389  
   390  	for _, region := range regions {
   391  		if strings.EqualFold(region.Slug, slug_or_name_or_id) {
   392  			return region, nil
   393  		}
   394  	}
   395  
   396  	for _, region := range regions {
   397  		if strings.EqualFold(region.Name, slug_or_name_or_id) {
   398  			return region, nil
   399  		}
   400  	}
   401  
   402  	for _, region := range regions {
   403  		id, err := strconv.Atoi(slug_or_name_or_id)
   404  		if err == nil {
   405  			if region.Id == uint(id) {
   406  				return region, nil
   407  			}
   408  		}
   409  	}
   410  
   411  	err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
   412  
   413  	return Region{}, err
   414  }
   415  
   416  // Returns all available sizes.
   417  func (d DigitalOceanClientV2) Sizes() ([]Size, error) {
   418  	res := SizesResp{}
   419  	err := NewRequestV2(d, "v2/sizes?per_page=200", "GET", nil, &res)
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  
   424  	return res.Sizes, nil
   425  }
   426  
   427  func (d DigitalOceanClientV2) Size(slug_or_name_or_id string) (Size, error) {
   428  	sizes, err := d.Sizes()
   429  	if err != nil {
   430  		return Size{}, err
   431  	}
   432  
   433  	for _, size := range sizes {
   434  		if strings.EqualFold(size.Slug, slug_or_name_or_id) {
   435  			return size, nil
   436  		}
   437  	}
   438  
   439  	for _, size := range sizes {
   440  		if strings.EqualFold(size.Name, slug_or_name_or_id) {
   441  			return size, nil
   442  		}
   443  	}
   444  
   445  	for _, size := range sizes {
   446  		id, err := strconv.Atoi(slug_or_name_or_id)
   447  		if err == nil {
   448  			if size.Id == uint(id) {
   449  				return size, nil
   450  			}
   451  		}
   452  	}
   453  
   454  	err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
   455  
   456  	return Size{}, err
   457  }