github.com/hobbeswalsh/terraform@v0.3.7-0.20150619183303-ad17cf55a0fa/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 return fmt.Errorf("Error retrieving droplet: %s", err) 169 } 170 171 if droplet.ImageSlug() != "" { 172 d.Set("image", droplet.ImageSlug()) 173 } else { 174 d.Set("image", droplet.ImageId()) 175 } 176 177 d.Set("name", droplet.Name) 178 d.Set("region", droplet.RegionSlug()) 179 d.Set("size", droplet.SizeSlug) 180 d.Set("status", droplet.Status) 181 d.Set("locked", droplet.IsLocked()) 182 183 if droplet.IPV6Address("public") != "" { 184 d.Set("ipv6", true) 185 d.Set("ipv6_address", droplet.IPV6Address("public")) 186 d.Set("ipv6_address_private", droplet.IPV6Address("private")) 187 } 188 189 d.Set("ipv4_address", droplet.IPV4Address("public")) 190 191 if droplet.NetworkingType() == "private" { 192 d.Set("private_networking", true) 193 d.Set("ipv4_address_private", droplet.IPV4Address("private")) 194 } 195 196 // Initialize the connection info 197 d.SetConnInfo(map[string]string{ 198 "type": "ssh", 199 "host": droplet.IPV4Address("public"), 200 }) 201 202 return nil 203 } 204 205 func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error { 206 client := meta.(*digitalocean.Client) 207 208 if d.HasChange("size") { 209 oldSize, newSize := d.GetChange("size") 210 211 err := client.PowerOff(d.Id()) 212 213 if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { 214 return fmt.Errorf( 215 "Error powering off droplet (%s): %s", d.Id(), err) 216 } 217 218 // Wait for power off 219 _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client) 220 if err != nil { 221 return fmt.Errorf( 222 "Error waiting for droplet (%s) to become powered off: %s", d.Id(), err) 223 } 224 225 // Resize the droplet 226 err = client.Resize(d.Id(), newSize.(string)) 227 if err != nil { 228 newErr := powerOnAndWait(d, meta) 229 if newErr != nil { 230 return fmt.Errorf( 231 "Error powering on droplet (%s) after failed resize: %s", d.Id(), err) 232 } 233 return fmt.Errorf( 234 "Error resizing droplet (%s): %s", d.Id(), err) 235 } 236 237 // Wait for the size to change 238 _, err = WaitForDropletAttribute( 239 d, newSize.(string), []string{"", oldSize.(string)}, "size", meta) 240 241 if err != nil { 242 newErr := powerOnAndWait(d, meta) 243 if newErr != nil { 244 return fmt.Errorf( 245 "Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err) 246 } 247 return fmt.Errorf( 248 "Error waiting for resize droplet (%s) to finish: %s", d.Id(), err) 249 } 250 251 err = client.PowerOn(d.Id()) 252 253 if err != nil { 254 return fmt.Errorf( 255 "Error powering on droplet (%s) after resize: %s", d.Id(), err) 256 } 257 258 // Wait for power off 259 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta) 260 if err != nil { 261 return err 262 } 263 } 264 265 if d.HasChange("name") { 266 oldName, newName := d.GetChange("name") 267 268 // Rename the droplet 269 err := client.Rename(d.Id(), newName.(string)) 270 271 if err != nil { 272 return fmt.Errorf( 273 "Error renaming droplet (%s): %s", d.Id(), err) 274 } 275 276 // Wait for the name to change 277 _, err = WaitForDropletAttribute( 278 d, newName.(string), []string{"", oldName.(string)}, "name", meta) 279 280 if err != nil { 281 return fmt.Errorf( 282 "Error waiting for rename droplet (%s) to finish: %s", d.Id(), err) 283 } 284 } 285 286 // As there is no way to disable private networking, 287 // we only check if it needs to be enabled 288 if d.HasChange("private_networking") && d.Get("private_networking").(bool) { 289 err := client.EnablePrivateNetworking(d.Id()) 290 291 if err != nil { 292 return fmt.Errorf( 293 "Error enabling private networking for droplet (%s): %s", d.Id(), err) 294 } 295 296 // Wait for the private_networking to turn on 297 _, err = WaitForDropletAttribute( 298 d, "true", []string{"", "false"}, "private_networking", meta) 299 300 return fmt.Errorf( 301 "Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err) 302 } 303 304 // As there is no way to disable IPv6, we only check if it needs to be enabled 305 if d.HasChange("ipv6") && d.Get("ipv6").(bool) { 306 err := client.EnableIPV6s(d.Id()) 307 308 if err != nil { 309 return fmt.Errorf( 310 "Error turning on ipv6 for droplet (%s): %s", d.Id(), err) 311 } 312 313 // Wait for ipv6 to turn on 314 _, err = WaitForDropletAttribute( 315 d, "true", []string{"", "false"}, "ipv6", meta) 316 317 if err != nil { 318 return fmt.Errorf( 319 "Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err) 320 } 321 } 322 323 return resourceDigitalOceanDropletRead(d, meta) 324 } 325 326 func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error { 327 client := meta.(*digitalocean.Client) 328 329 _, err := WaitForDropletAttribute( 330 d, "false", []string{"", "true"}, "locked", meta) 331 332 if err != nil { 333 return fmt.Errorf( 334 "Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err) 335 } 336 337 log.Printf("[INFO] Deleting droplet: %s", d.Id()) 338 339 // Destroy the droplet 340 err = client.DestroyDroplet(d.Id()) 341 342 // Handle remotely destroyed droplets 343 if err != nil && strings.Contains(err.Error(), "404 Not Found") { 344 return nil 345 } 346 347 if err != nil { 348 return fmt.Errorf("Error deleting droplet: %s", err) 349 } 350 351 return nil 352 } 353 354 func WaitForDropletAttribute( 355 d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) { 356 // Wait for the droplet so we can get the networking attributes 357 // that show up after a while 358 log.Printf( 359 "[INFO] Waiting for droplet (%s) to have %s of %s", 360 d.Id(), attribute, target) 361 362 stateConf := &resource.StateChangeConf{ 363 Pending: pending, 364 Target: target, 365 Refresh: newDropletStateRefreshFunc(d, attribute, meta), 366 Timeout: 60 * time.Minute, 367 Delay: 10 * time.Second, 368 MinTimeout: 3 * time.Second, 369 370 // This is a hack around DO API strangeness. 371 // https://github.com/hashicorp/terraform/issues/481 372 // 373 NotFoundChecks: 60, 374 } 375 376 return stateConf.WaitForState() 377 } 378 379 // TODO This function still needs a little more refactoring to make it 380 // cleaner and more efficient 381 func newDropletStateRefreshFunc( 382 d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc { 383 client := meta.(*digitalocean.Client) 384 return func() (interface{}, string, error) { 385 err := resourceDigitalOceanDropletRead(d, meta) 386 if err != nil { 387 return nil, "", err 388 } 389 390 // If the droplet is locked, continue waiting. We can 391 // only perform actions on unlocked droplets, so it's 392 // pointless to look at that status 393 if d.Get("locked").(string) == "true" { 394 log.Println("[DEBUG] Droplet is locked, skipping status check and retrying") 395 return nil, "", nil 396 } 397 398 // See if we can access our attribute 399 if attr, ok := d.GetOk(attribute); ok { 400 // Retrieve the droplet properties 401 droplet, err := client.RetrieveDroplet(d.Id()) 402 if err != nil { 403 return nil, "", fmt.Errorf("Error retrieving droplet: %s", err) 404 } 405 406 return &droplet, attr.(string), nil 407 } 408 409 return nil, "", nil 410 } 411 } 412 413 // Powers on the droplet and waits for it to be active 414 func powerOnAndWait(d *schema.ResourceData, meta interface{}) error { 415 client := meta.(*digitalocean.Client) 416 err := client.PowerOn(d.Id()) 417 if err != nil { 418 return err 419 } 420 421 // Wait for power on 422 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client) 423 if err != nil { 424 return err 425 } 426 427 return nil 428 }