github.com/anuaimi/terraform@v0.6.4-0.20150904235404-2bf9aec61da8/builtin/providers/digitalocean/resource_digitalocean_droplet.go (about) 1 package digitalocean 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/resource" 10 "github.com/hashicorp/terraform/helper/schema" 11 "github.com/pearkes/digitalocean" 12 ) 13 14 func resourceDigitalOceanDroplet() *schema.Resource { 15 return &schema.Resource{ 16 Create: resourceDigitalOceanDropletCreate, 17 Read: resourceDigitalOceanDropletRead, 18 Update: resourceDigitalOceanDropletUpdate, 19 Delete: resourceDigitalOceanDropletDelete, 20 21 Schema: map[string]*schema.Schema{ 22 "image": &schema.Schema{ 23 Type: schema.TypeString, 24 Required: true, 25 ForceNew: true, 26 }, 27 28 "name": &schema.Schema{ 29 Type: schema.TypeString, 30 Required: true, 31 }, 32 33 "region": &schema.Schema{ 34 Type: schema.TypeString, 35 Required: true, 36 ForceNew: true, 37 }, 38 39 "size": &schema.Schema{ 40 Type: schema.TypeString, 41 Required: true, 42 }, 43 44 "status": &schema.Schema{ 45 Type: schema.TypeString, 46 Computed: true, 47 }, 48 49 "locked": &schema.Schema{ 50 Type: schema.TypeString, 51 Computed: true, 52 }, 53 54 "backups": &schema.Schema{ 55 Type: schema.TypeBool, 56 Optional: true, 57 }, 58 59 "ipv6": &schema.Schema{ 60 Type: schema.TypeBool, 61 Optional: true, 62 }, 63 64 "ipv6_address": &schema.Schema{ 65 Type: schema.TypeString, 66 Computed: true, 67 }, 68 69 "ipv6_address_private": &schema.Schema{ 70 Type: schema.TypeString, 71 Computed: true, 72 }, 73 74 "private_networking": &schema.Schema{ 75 Type: schema.TypeBool, 76 Optional: true, 77 }, 78 79 "ipv4_address": &schema.Schema{ 80 Type: schema.TypeString, 81 Computed: true, 82 }, 83 84 "ipv4_address_private": &schema.Schema{ 85 Type: schema.TypeString, 86 Computed: true, 87 }, 88 89 "ssh_keys": &schema.Schema{ 90 Type: schema.TypeList, 91 Optional: true, 92 Elem: &schema.Schema{Type: schema.TypeString}, 93 }, 94 95 "user_data": &schema.Schema{ 96 Type: schema.TypeString, 97 Optional: true, 98 }, 99 }, 100 } 101 } 102 103 func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error { 104 client := meta.(*digitalocean.Client) 105 106 // Build up our creation options 107 opts := &digitalocean.CreateDroplet{ 108 Image: d.Get("image").(string), 109 Name: d.Get("name").(string), 110 Region: d.Get("region").(string), 111 Size: d.Get("size").(string), 112 } 113 114 if attr, ok := d.GetOk("backups"); ok { 115 opts.Backups = attr.(bool) 116 } 117 118 if attr, ok := d.GetOk("ipv6"); ok { 119 opts.IPV6 = attr.(bool) 120 } 121 122 if attr, ok := d.GetOk("private_networking"); ok { 123 opts.PrivateNetworking = attr.(bool) 124 } 125 126 if attr, ok := d.GetOk("user_data"); ok { 127 opts.UserData = attr.(string) 128 } 129 130 // Get configured ssh_keys 131 ssh_keys := d.Get("ssh_keys.#").(int) 132 if ssh_keys > 0 { 133 opts.SSHKeys = make([]string, 0, ssh_keys) 134 for i := 0; i < ssh_keys; i++ { 135 key := fmt.Sprintf("ssh_keys.%d", i) 136 opts.SSHKeys = append(opts.SSHKeys, d.Get(key).(string)) 137 } 138 } 139 140 log.Printf("[DEBUG] Droplet create configuration: %#v", opts) 141 142 id, err := client.CreateDroplet(opts) 143 144 if err != nil { 145 return fmt.Errorf("Error creating droplet: %s", err) 146 } 147 148 // Assign the droplets id 149 d.SetId(id) 150 151 log.Printf("[INFO] Droplet ID: %s", d.Id()) 152 153 _, err = WaitForDropletAttribute(d, "active", []string{"new"}, "status", meta) 154 if err != nil { 155 return fmt.Errorf( 156 "Error waiting for droplet (%s) to become ready: %s", d.Id(), err) 157 } 158 159 return resourceDigitalOceanDropletRead(d, meta) 160 } 161 162 func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error { 163 client := meta.(*digitalocean.Client) 164 165 // Retrieve the droplet properties for updating the state 166 droplet, err := client.RetrieveDroplet(d.Id()) 167 if err != nil { 168 // check if the droplet no longer exists. 169 if err.Error() == "Error retrieving droplet: API Error: 404 Not Found" { 170 d.SetId("") 171 return nil 172 } 173 174 return fmt.Errorf("Error retrieving droplet: %s", err) 175 } 176 177 if droplet.ImageSlug() != "" { 178 d.Set("image", droplet.ImageSlug()) 179 } else { 180 d.Set("image", droplet.ImageId()) 181 } 182 183 d.Set("name", droplet.Name) 184 d.Set("region", droplet.RegionSlug()) 185 d.Set("size", droplet.SizeSlug) 186 d.Set("status", droplet.Status) 187 d.Set("locked", droplet.IsLocked()) 188 189 if droplet.IPV6Address("public") != "" { 190 d.Set("ipv6", true) 191 d.Set("ipv6_address", droplet.IPV6Address("public")) 192 d.Set("ipv6_address_private", droplet.IPV6Address("private")) 193 } 194 195 d.Set("ipv4_address", droplet.IPV4Address("public")) 196 197 if droplet.NetworkingType() == "private" { 198 d.Set("private_networking", true) 199 d.Set("ipv4_address_private", droplet.IPV4Address("private")) 200 } 201 202 // Initialize the connection info 203 d.SetConnInfo(map[string]string{ 204 "type": "ssh", 205 "host": droplet.IPV4Address("public"), 206 }) 207 208 return nil 209 } 210 211 func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error { 212 client := meta.(*digitalocean.Client) 213 214 if d.HasChange("size") { 215 oldSize, newSize := d.GetChange("size") 216 217 err := client.PowerOff(d.Id()) 218 219 if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { 220 return fmt.Errorf( 221 "Error powering off droplet (%s): %s", d.Id(), err) 222 } 223 224 // Wait for power off 225 _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client) 226 if err != nil { 227 return fmt.Errorf( 228 "Error waiting for droplet (%s) to become powered off: %s", d.Id(), err) 229 } 230 231 // Resize the droplet 232 err = client.Resize(d.Id(), newSize.(string)) 233 if err != nil { 234 newErr := powerOnAndWait(d, meta) 235 if newErr != nil { 236 return fmt.Errorf( 237 "Error powering on droplet (%s) after failed resize: %s", d.Id(), err) 238 } 239 return fmt.Errorf( 240 "Error resizing droplet (%s): %s", d.Id(), err) 241 } 242 243 // Wait for the size to change 244 _, err = WaitForDropletAttribute( 245 d, newSize.(string), []string{"", oldSize.(string)}, "size", meta) 246 247 if err != nil { 248 newErr := powerOnAndWait(d, meta) 249 if newErr != nil { 250 return fmt.Errorf( 251 "Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err) 252 } 253 return fmt.Errorf( 254 "Error waiting for resize droplet (%s) to finish: %s", d.Id(), err) 255 } 256 257 err = client.PowerOn(d.Id()) 258 259 if err != nil { 260 return fmt.Errorf( 261 "Error powering on droplet (%s) after resize: %s", d.Id(), err) 262 } 263 264 // Wait for power off 265 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta) 266 if err != nil { 267 return err 268 } 269 } 270 271 if d.HasChange("name") { 272 oldName, newName := d.GetChange("name") 273 274 // Rename the droplet 275 err := client.Rename(d.Id(), newName.(string)) 276 277 if err != nil { 278 return fmt.Errorf( 279 "Error renaming droplet (%s): %s", d.Id(), err) 280 } 281 282 // Wait for the name to change 283 _, err = WaitForDropletAttribute( 284 d, newName.(string), []string{"", oldName.(string)}, "name", meta) 285 286 if err != nil { 287 return fmt.Errorf( 288 "Error waiting for rename droplet (%s) to finish: %s", d.Id(), err) 289 } 290 } 291 292 // As there is no way to disable private networking, 293 // we only check if it needs to be enabled 294 if d.HasChange("private_networking") && d.Get("private_networking").(bool) { 295 err := client.EnablePrivateNetworking(d.Id()) 296 297 if err != nil { 298 return fmt.Errorf( 299 "Error enabling private networking for droplet (%s): %s", d.Id(), err) 300 } 301 302 // Wait for the private_networking to turn on 303 _, err = WaitForDropletAttribute( 304 d, "true", []string{"", "false"}, "private_networking", meta) 305 306 return fmt.Errorf( 307 "Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err) 308 } 309 310 // As there is no way to disable IPv6, we only check if it needs to be enabled 311 if d.HasChange("ipv6") && d.Get("ipv6").(bool) { 312 err := client.EnableIPV6s(d.Id()) 313 314 if err != nil { 315 return fmt.Errorf( 316 "Error turning on ipv6 for droplet (%s): %s", d.Id(), err) 317 } 318 319 // Wait for ipv6 to turn on 320 _, err = WaitForDropletAttribute( 321 d, "true", []string{"", "false"}, "ipv6", meta) 322 323 if err != nil { 324 return fmt.Errorf( 325 "Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err) 326 } 327 } 328 329 return resourceDigitalOceanDropletRead(d, meta) 330 } 331 332 func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error { 333 client := meta.(*digitalocean.Client) 334 335 _, err := WaitForDropletAttribute( 336 d, "false", []string{"", "true"}, "locked", meta) 337 338 if err != nil { 339 return fmt.Errorf( 340 "Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err) 341 } 342 343 log.Printf("[INFO] Deleting droplet: %s", d.Id()) 344 345 // Destroy the droplet 346 err = client.DestroyDroplet(d.Id()) 347 348 // Handle remotely destroyed droplets 349 if err != nil && strings.Contains(err.Error(), "404 Not Found") { 350 return nil 351 } 352 353 if err != nil { 354 return fmt.Errorf("Error deleting droplet: %s", err) 355 } 356 357 return nil 358 } 359 360 func WaitForDropletAttribute( 361 d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) { 362 // Wait for the droplet so we can get the networking attributes 363 // that show up after a while 364 log.Printf( 365 "[INFO] Waiting for droplet (%s) to have %s of %s", 366 d.Id(), attribute, target) 367 368 stateConf := &resource.StateChangeConf{ 369 Pending: pending, 370 Target: target, 371 Refresh: newDropletStateRefreshFunc(d, attribute, meta), 372 Timeout: 60 * time.Minute, 373 Delay: 10 * time.Second, 374 MinTimeout: 3 * time.Second, 375 376 // This is a hack around DO API strangeness. 377 // https://github.com/hashicorp/terraform/issues/481 378 // 379 NotFoundChecks: 60, 380 } 381 382 return stateConf.WaitForState() 383 } 384 385 // TODO This function still needs a little more refactoring to make it 386 // cleaner and more efficient 387 func newDropletStateRefreshFunc( 388 d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc { 389 client := meta.(*digitalocean.Client) 390 return func() (interface{}, string, error) { 391 err := resourceDigitalOceanDropletRead(d, meta) 392 if err != nil { 393 return nil, "", err 394 } 395 396 // If the droplet is locked, continue waiting. We can 397 // only perform actions on unlocked droplets, so it's 398 // pointless to look at that status 399 if d.Get("locked").(string) == "true" { 400 log.Println("[DEBUG] Droplet is locked, skipping status check and retrying") 401 return nil, "", nil 402 } 403 404 // See if we can access our attribute 405 if attr, ok := d.GetOk(attribute); ok { 406 // Retrieve the droplet properties 407 droplet, err := client.RetrieveDroplet(d.Id()) 408 if err != nil { 409 return nil, "", fmt.Errorf("Error retrieving droplet: %s", err) 410 } 411 412 return &droplet, attr.(string), nil 413 } 414 415 return nil, "", nil 416 } 417 } 418 419 // Powers on the droplet and waits for it to be active 420 func powerOnAndWait(d *schema.ResourceData, meta interface{}) error { 421 client := meta.(*digitalocean.Client) 422 err := client.PowerOn(d.Id()) 423 if err != nil { 424 return err 425 } 426 427 // Wait for power on 428 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client) 429 if err != nil { 430 return err 431 } 432 433 return nil 434 }