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  }