github.com/alouche/packer@v0.3.7/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 DigitalOceanClient struct { 33 // The http client for communicating 34 client *http.Client 35 36 // The base URL of the API 37 BaseURL string 38 39 // Credentials 40 ClientID string 41 APIKey string 42 } 43 44 // Creates a new client for communicating with DO 45 func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient { 46 c := &DigitalOceanClient{ 47 client: &http.Client{ 48 Transport: &http.Transport{ 49 Proxy: http.ProxyFromEnvironment, 50 }, 51 }, 52 BaseURL: DIGITALOCEAN_API_URL, 53 ClientID: client, 54 APIKey: key, 55 } 56 return c 57 } 58 59 // Creates an SSH Key and returns it's id 60 func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { 61 params := url.Values{} 62 params.Set("name", name) 63 params.Set("ssh_pub_key", pub) 64 65 body, err := NewRequest(d, "ssh_keys/new", params) 66 if err != nil { 67 return 0, err 68 } 69 70 // Read the SSH key's ID we just created 71 key := body["ssh_key"].(map[string]interface{}) 72 keyId := key["id"].(float64) 73 return uint(keyId), nil 74 } 75 76 // Destroys an SSH key 77 func (d DigitalOceanClient) DestroyKey(id uint) error { 78 path := fmt.Sprintf("ssh_keys/%v/destroy", id) 79 _, err := NewRequest(d, path, url.Values{}) 80 return err 81 } 82 83 // Creates a droplet and returns it's id 84 func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) { 85 params := url.Values{} 86 params.Set("name", name) 87 params.Set("size_id", fmt.Sprintf("%v", size)) 88 params.Set("image_id", fmt.Sprintf("%v", image)) 89 params.Set("region_id", fmt.Sprintf("%v", region)) 90 params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId)) 91 92 body, err := NewRequest(d, "droplets/new", params) 93 if err != nil { 94 return 0, err 95 } 96 97 // Read the Droplets ID 98 droplet := body["droplet"].(map[string]interface{}) 99 dropletId := droplet["id"].(float64) 100 return uint(dropletId), err 101 } 102 103 // Destroys a droplet 104 func (d DigitalOceanClient) DestroyDroplet(id uint) error { 105 path := fmt.Sprintf("droplets/%v/destroy", id) 106 _, err := NewRequest(d, path, url.Values{}) 107 return err 108 } 109 110 // Powers off a droplet 111 func (d DigitalOceanClient) PowerOffDroplet(id uint) error { 112 path := fmt.Sprintf("droplets/%v/power_off", id) 113 114 _, err := NewRequest(d, path, url.Values{}) 115 116 return err 117 } 118 119 // Shutsdown a droplet. This is a "soft" shutdown. 120 func (d DigitalOceanClient) ShutdownDroplet(id uint) error { 121 path := fmt.Sprintf("droplets/%v/shutdown", id) 122 123 _, err := NewRequest(d, path, url.Values{}) 124 125 return err 126 } 127 128 // Creates a snaphot of a droplet by it's ID 129 func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { 130 path := fmt.Sprintf("droplets/%v/snapshot", id) 131 132 params := url.Values{} 133 params.Set("name", name) 134 135 _, err := NewRequest(d, path, params) 136 137 return err 138 } 139 140 // Returns all available images. 141 func (d DigitalOceanClient) Images() ([]Image, error) { 142 resp, err := NewRequest(d, "images", url.Values{}) 143 if err != nil { 144 return nil, err 145 } 146 147 var result ImagesResp 148 if err := mapstructure.Decode(resp, &result); err != nil { 149 return nil, err 150 } 151 152 return result.Images, nil 153 } 154 155 // Destroys an image by its ID. 156 func (d DigitalOceanClient) DestroyImage(id uint) error { 157 path := fmt.Sprintf("images/%d/destroy", id) 158 _, err := NewRequest(d, path, url.Values{}) 159 return err 160 } 161 162 // Returns DO's string representation of status "off" "new" "active" etc. 163 func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) { 164 path := fmt.Sprintf("droplets/%v", id) 165 166 body, err := NewRequest(d, path, url.Values{}) 167 if err != nil { 168 return "", "", err 169 } 170 171 var ip string 172 173 // Read the droplet's "status" 174 droplet := body["droplet"].(map[string]interface{}) 175 status := droplet["status"].(string) 176 177 if droplet["ip_address"] != nil { 178 ip = droplet["ip_address"].(string) 179 } 180 181 return ip, status, err 182 } 183 184 // Sends an api request and returns a generic map[string]interface of 185 // the response. 186 func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) { 187 client := d.client 188 189 // Add the authentication parameters 190 params.Set("client_id", d.ClientID) 191 params.Set("api_key", d.APIKey) 192 193 url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode()) 194 195 // Do some basic scrubbing so sensitive information doesn't appear in logs 196 scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1) 197 scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1) 198 log.Printf("sending new request to digitalocean: %s", scrubbedUrl) 199 200 var lastErr error 201 for attempts := 1; attempts < 10; attempts++ { 202 resp, err := client.Get(url) 203 if err != nil { 204 return nil, err 205 } 206 207 body, err := ioutil.ReadAll(resp.Body) 208 resp.Body.Close() 209 if err != nil { 210 return nil, err 211 } 212 213 log.Printf("response from digitalocean: %s", body) 214 215 var decodedResponse map[string]interface{} 216 err = json.Unmarshal(body, &decodedResponse) 217 if err != nil { 218 err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s", 219 resp.StatusCode, body)) 220 return decodedResponse, err 221 } 222 223 // Check for errors sent by digitalocean 224 status := decodedResponse["status"].(string) 225 if status == "OK" { 226 return decodedResponse, nil 227 } 228 229 if status == "ERROR" { 230 status = decodedResponse["error_message"].(string) 231 } 232 233 lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s", 234 resp.StatusCode, status)) 235 log.Println(lastErr) 236 if strings.Contains(status, "a pending event") { 237 // Retry, DigitalOcean sends these dumb "pending event" 238 // errors all the time. 239 time.Sleep(5 * time.Second) 240 continue 241 } 242 243 // Some other kind of error. Just return. 244 return decodedResponse, lastErr 245 } 246 247 return nil, lastErr 248 }