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