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 }