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