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 }