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