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