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