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