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