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