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  }