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