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