github.com/keshavdv/terraform@v0.7.0-rc2.0.20160711232630-d69256dcb425/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  		Importer: &schema.ResourceImporter{
    22  			State: schema.ImportStatePassthrough,
    23  		},
    24  
    25  		Schema: map[string]*schema.Schema{
    26  			"image": &schema.Schema{
    27  				Type:     schema.TypeString,
    28  				Required: true,
    29  				ForceNew: true,
    30  			},
    31  
    32  			"name": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Required: true,
    35  			},
    36  
    37  			"region": &schema.Schema{
    38  				Type:     schema.TypeString,
    39  				Required: true,
    40  				ForceNew: true,
    41  				StateFunc: func(val interface{}) string {
    42  					// DO API V2 region slug is always lowercase
    43  					return strings.ToLower(val.(string))
    44  				},
    45  			},
    46  
    47  			"size": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Required: true,
    50  				StateFunc: func(val interface{}) string {
    51  					// DO API V2 size slug is always lowercase
    52  					return strings.ToLower(val.(string))
    53  				},
    54  			},
    55  
    56  			"status": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Computed: true,
    59  			},
    60  
    61  			"locked": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Computed: true,
    64  			},
    65  
    66  			"backups": &schema.Schema{
    67  				Type:     schema.TypeBool,
    68  				Optional: true,
    69  			},
    70  
    71  			"ipv6": &schema.Schema{
    72  				Type:     schema.TypeBool,
    73  				Optional: true,
    74  			},
    75  
    76  			"ipv6_address": &schema.Schema{
    77  				Type:     schema.TypeString,
    78  				Computed: true,
    79  			},
    80  
    81  			"ipv6_address_private": &schema.Schema{
    82  				Type:     schema.TypeString,
    83  				Computed: true,
    84  			},
    85  
    86  			"private_networking": &schema.Schema{
    87  				Type:     schema.TypeBool,
    88  				Optional: true,
    89  			},
    90  
    91  			"ipv4_address": &schema.Schema{
    92  				Type:     schema.TypeString,
    93  				Computed: true,
    94  			},
    95  
    96  			"ipv4_address_private": &schema.Schema{
    97  				Type:     schema.TypeString,
    98  				Computed: true,
    99  			},
   100  
   101  			"ssh_keys": &schema.Schema{
   102  				Type:     schema.TypeList,
   103  				Optional: true,
   104  				Elem:     &schema.Schema{Type: schema.TypeString},
   105  			},
   106  
   107  			"tags": &schema.Schema{
   108  				Type:     schema.TypeList,
   109  				Optional: true,
   110  				Elem:     &schema.Schema{Type: schema.TypeString},
   111  			},
   112  
   113  			"user_data": &schema.Schema{
   114  				Type:     schema.TypeString,
   115  				Optional: true,
   116  				ForceNew: true,
   117  			},
   118  		},
   119  	}
   120  }
   121  
   122  func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error {
   123  	client := meta.(*godo.Client)
   124  
   125  	// Build up our creation options
   126  	opts := &godo.DropletCreateRequest{
   127  		Image: godo.DropletCreateImage{
   128  			Slug: d.Get("image").(string),
   129  		},
   130  		Name:   d.Get("name").(string),
   131  		Region: d.Get("region").(string),
   132  		Size:   d.Get("size").(string),
   133  	}
   134  
   135  	if attr, ok := d.GetOk("backups"); ok {
   136  		opts.Backups = attr.(bool)
   137  	}
   138  
   139  	if attr, ok := d.GetOk("ipv6"); ok {
   140  		opts.IPv6 = attr.(bool)
   141  	}
   142  
   143  	if attr, ok := d.GetOk("private_networking"); ok {
   144  		opts.PrivateNetworking = attr.(bool)
   145  	}
   146  
   147  	if attr, ok := d.GetOk("user_data"); ok {
   148  		opts.UserData = attr.(string)
   149  	}
   150  
   151  	// Get configured ssh_keys
   152  	sshKeys := d.Get("ssh_keys.#").(int)
   153  	if sshKeys > 0 {
   154  		opts.SSHKeys = make([]godo.DropletCreateSSHKey, 0, sshKeys)
   155  		for i := 0; i < sshKeys; i++ {
   156  			key := fmt.Sprintf("ssh_keys.%d", i)
   157  			sshKeyRef := d.Get(key).(string)
   158  
   159  			var sshKey godo.DropletCreateSSHKey
   160  			// sshKeyRef can be either an ID or a fingerprint
   161  			if id, err := strconv.Atoi(sshKeyRef); err == nil {
   162  				sshKey.ID = id
   163  			} else {
   164  				sshKey.Fingerprint = sshKeyRef
   165  			}
   166  
   167  			opts.SSHKeys = append(opts.SSHKeys, sshKey)
   168  		}
   169  	}
   170  
   171  	log.Printf("[DEBUG] Droplet create configuration: %#v", opts)
   172  
   173  	droplet, _, err := client.Droplets.Create(opts)
   174  
   175  	if err != nil {
   176  		return fmt.Errorf("Error creating droplet: %s", err)
   177  	}
   178  
   179  	// Assign the droplets id
   180  	d.SetId(strconv.Itoa(droplet.ID))
   181  
   182  	log.Printf("[INFO] Droplet ID: %s", d.Id())
   183  
   184  	_, err = WaitForDropletAttribute(d, "active", []string{"new"}, "status", meta)
   185  	if err != nil {
   186  		return fmt.Errorf(
   187  			"Error waiting for droplet (%s) to become ready: %s", d.Id(), err)
   188  	}
   189  
   190  	// droplet needs to be active in order to set tags
   191  	err = setTags(client, d)
   192  	if err != nil {
   193  		return fmt.Errorf("Error setting tags: %s", err)
   194  	}
   195  
   196  	return resourceDigitalOceanDropletRead(d, meta)
   197  }
   198  
   199  func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error {
   200  	client := meta.(*godo.Client)
   201  
   202  	id, err := strconv.Atoi(d.Id())
   203  	if err != nil {
   204  		return fmt.Errorf("invalid droplet id: %v", err)
   205  	}
   206  
   207  	// Retrieve the droplet properties for updating the state
   208  	droplet, resp, err := client.Droplets.Get(id)
   209  	if err != nil {
   210  		// check if the droplet no longer exists.
   211  		if resp != nil && resp.StatusCode == 404 {
   212  			log.Printf("[WARN] DigitalOcean Droplet (%s) not found", d.Id())
   213  			d.SetId("")
   214  			return nil
   215  		}
   216  
   217  		return fmt.Errorf("Error retrieving droplet: %s", err)
   218  	}
   219  
   220  	if droplet.Image.Slug != "" {
   221  		d.Set("image", droplet.Image.Slug)
   222  	} else {
   223  		d.Set("image", droplet.Image.ID)
   224  	}
   225  
   226  	d.Set("name", droplet.Name)
   227  	d.Set("region", droplet.Region.Slug)
   228  	d.Set("size", droplet.Size.Slug)
   229  	d.Set("status", droplet.Status)
   230  	d.Set("locked", strconv.FormatBool(droplet.Locked))
   231  
   232  	if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" {
   233  		d.Set("ipv6", true)
   234  		d.Set("ipv6_address", publicIPv6)
   235  		d.Set("ipv6_address_private", findIPv6AddrByType(droplet, "private"))
   236  	}
   237  
   238  	d.Set("ipv4_address", findIPv4AddrByType(droplet, "public"))
   239  
   240  	if privateIPv4 := findIPv4AddrByType(droplet, "private"); privateIPv4 != "" {
   241  		d.Set("private_networking", true)
   242  		d.Set("ipv4_address_private", privateIPv4)
   243  	}
   244  
   245  	// Initialize the connection info
   246  	d.SetConnInfo(map[string]string{
   247  		"type": "ssh",
   248  		"host": findIPv4AddrByType(droplet, "public"),
   249  	})
   250  
   251  	d.Set("tags", droplet.Tags)
   252  
   253  	return nil
   254  }
   255  
   256  func findIPv6AddrByType(d *godo.Droplet, addrType string) string {
   257  	for _, addr := range d.Networks.V6 {
   258  		if addr.Type == addrType {
   259  			return addr.IPAddress
   260  		}
   261  	}
   262  	return ""
   263  }
   264  
   265  func findIPv4AddrByType(d *godo.Droplet, addrType string) string {
   266  	for _, addr := range d.Networks.V4 {
   267  		if addr.Type == addrType {
   268  			return addr.IPAddress
   269  		}
   270  	}
   271  	return ""
   272  }
   273  
   274  func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error {
   275  	client := meta.(*godo.Client)
   276  
   277  	id, err := strconv.Atoi(d.Id())
   278  	if err != nil {
   279  		return fmt.Errorf("invalid droplet id: %v", err)
   280  	}
   281  
   282  	if d.HasChange("size") {
   283  		oldSize, newSize := d.GetChange("size")
   284  
   285  		_, _, err = client.DropletActions.PowerOff(id)
   286  		if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") {
   287  			return fmt.Errorf(
   288  				"Error powering off droplet (%s): %s", d.Id(), err)
   289  		}
   290  
   291  		// Wait for power off
   292  		_, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client)
   293  		if err != nil {
   294  			return fmt.Errorf(
   295  				"Error waiting for droplet (%s) to become powered off: %s", d.Id(), err)
   296  		}
   297  
   298  		// Resize the droplet
   299  		_, _, err = client.DropletActions.Resize(id, newSize.(string), true)
   300  		if err != nil {
   301  			newErr := powerOnAndWait(d, meta)
   302  			if newErr != nil {
   303  				return fmt.Errorf(
   304  					"Error powering on droplet (%s) after failed resize: %s", d.Id(), err)
   305  			}
   306  			return fmt.Errorf(
   307  				"Error resizing droplet (%s): %s", d.Id(), err)
   308  		}
   309  
   310  		// Wait for the size to change
   311  		_, err = WaitForDropletAttribute(
   312  			d, newSize.(string), []string{"", oldSize.(string)}, "size", meta)
   313  
   314  		if err != nil {
   315  			newErr := powerOnAndWait(d, meta)
   316  			if newErr != nil {
   317  				return fmt.Errorf(
   318  					"Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err)
   319  			}
   320  			return fmt.Errorf(
   321  				"Error waiting for resize droplet (%s) to finish: %s", d.Id(), err)
   322  		}
   323  
   324  		_, _, err = client.DropletActions.PowerOn(id)
   325  
   326  		if err != nil {
   327  			return fmt.Errorf(
   328  				"Error powering on droplet (%s) after resize: %s", d.Id(), err)
   329  		}
   330  
   331  		// Wait for power off
   332  		_, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta)
   333  		if err != nil {
   334  			return err
   335  		}
   336  	}
   337  
   338  	if d.HasChange("name") {
   339  		oldName, newName := d.GetChange("name")
   340  
   341  		// Rename the droplet
   342  		_, _, err = client.DropletActions.Rename(id, newName.(string))
   343  
   344  		if err != nil {
   345  			return fmt.Errorf(
   346  				"Error renaming droplet (%s): %s", d.Id(), err)
   347  		}
   348  
   349  		// Wait for the name to change
   350  		_, err = WaitForDropletAttribute(
   351  			d, newName.(string), []string{"", oldName.(string)}, "name", meta)
   352  
   353  		if err != nil {
   354  			return fmt.Errorf(
   355  				"Error waiting for rename droplet (%s) to finish: %s", d.Id(), err)
   356  		}
   357  	}
   358  
   359  	// As there is no way to disable private networking,
   360  	// we only check if it needs to be enabled
   361  	if d.HasChange("private_networking") && d.Get("private_networking").(bool) {
   362  		_, _, err = client.DropletActions.EnablePrivateNetworking(id)
   363  
   364  		if err != nil {
   365  			return fmt.Errorf(
   366  				"Error enabling private networking for droplet (%s): %s", d.Id(), err)
   367  		}
   368  
   369  		// Wait for the private_networking to turn on
   370  		_, err = WaitForDropletAttribute(
   371  			d, "true", []string{"", "false"}, "private_networking", meta)
   372  
   373  		return fmt.Errorf(
   374  			"Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err)
   375  	}
   376  
   377  	// As there is no way to disable IPv6, we only check if it needs to be enabled
   378  	if d.HasChange("ipv6") && d.Get("ipv6").(bool) {
   379  		_, _, err = client.DropletActions.EnableIPv6(id)
   380  
   381  		if err != nil {
   382  			return fmt.Errorf(
   383  				"Error turning on ipv6 for droplet (%s): %s", d.Id(), err)
   384  		}
   385  
   386  		// Wait for ipv6 to turn on
   387  		_, err = WaitForDropletAttribute(
   388  			d, "true", []string{"", "false"}, "ipv6", meta)
   389  
   390  		if err != nil {
   391  			return fmt.Errorf(
   392  				"Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err)
   393  		}
   394  	}
   395  
   396  	if d.HasChange("tags") {
   397  		err = setTags(client, d)
   398  		if err != nil {
   399  			return fmt.Errorf("Error updating tags: %s", err)
   400  		}
   401  	}
   402  
   403  	return resourceDigitalOceanDropletRead(d, meta)
   404  }
   405  
   406  func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error {
   407  	client := meta.(*godo.Client)
   408  
   409  	id, err := strconv.Atoi(d.Id())
   410  	if err != nil {
   411  		return fmt.Errorf("invalid droplet id: %v", err)
   412  	}
   413  
   414  	_, err = WaitForDropletAttribute(
   415  		d, "false", []string{"", "true"}, "locked", meta)
   416  
   417  	if err != nil {
   418  		return fmt.Errorf(
   419  			"Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err)
   420  	}
   421  
   422  	log.Printf("[INFO] Deleting droplet: %s", d.Id())
   423  
   424  	// Destroy the droplet
   425  	_, err = client.Droplets.Delete(id)
   426  
   427  	// Handle remotely destroyed droplets
   428  	if err != nil && strings.Contains(err.Error(), "404 Not Found") {
   429  		return nil
   430  	}
   431  
   432  	if err != nil {
   433  		return fmt.Errorf("Error deleting droplet: %s", err)
   434  	}
   435  
   436  	return nil
   437  }
   438  
   439  func WaitForDropletAttribute(
   440  	d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
   441  	// Wait for the droplet so we can get the networking attributes
   442  	// that show up after a while
   443  	log.Printf(
   444  		"[INFO] Waiting for droplet (%s) to have %s of %s",
   445  		d.Id(), attribute, target)
   446  
   447  	stateConf := &resource.StateChangeConf{
   448  		Pending:    pending,
   449  		Target:     []string{target},
   450  		Refresh:    newDropletStateRefreshFunc(d, attribute, meta),
   451  		Timeout:    60 * time.Minute,
   452  		Delay:      10 * time.Second,
   453  		MinTimeout: 3 * time.Second,
   454  
   455  		// This is a hack around DO API strangeness.
   456  		// https://github.com/hashicorp/terraform/issues/481
   457  		//
   458  		NotFoundChecks: 60,
   459  	}
   460  
   461  	return stateConf.WaitForState()
   462  }
   463  
   464  // TODO This function still needs a little more refactoring to make it
   465  // cleaner and more efficient
   466  func newDropletStateRefreshFunc(
   467  	d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
   468  	client := meta.(*godo.Client)
   469  	return func() (interface{}, string, error) {
   470  		id, err := strconv.Atoi(d.Id())
   471  		if err != nil {
   472  			return nil, "", err
   473  		}
   474  
   475  		err = resourceDigitalOceanDropletRead(d, meta)
   476  		if err != nil {
   477  			return nil, "", err
   478  		}
   479  
   480  		// If the droplet is locked, continue waiting. We can
   481  		// only perform actions on unlocked droplets, so it's
   482  		// pointless to look at that status
   483  		if d.Get("locked").(string) == "true" {
   484  			log.Println("[DEBUG] Droplet is locked, skipping status check and retrying")
   485  			return nil, "", nil
   486  		}
   487  
   488  		// See if we can access our attribute
   489  		if attr, ok := d.GetOk(attribute); ok {
   490  			// Retrieve the droplet properties
   491  			droplet, _, err := client.Droplets.Get(id)
   492  			if err != nil {
   493  				return nil, "", fmt.Errorf("Error retrieving droplet: %s", err)
   494  			}
   495  
   496  			return &droplet, attr.(string), nil
   497  		}
   498  
   499  		return nil, "", nil
   500  	}
   501  }
   502  
   503  // Powers on the droplet and waits for it to be active
   504  func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
   505  	id, err := strconv.Atoi(d.Id())
   506  	if err != nil {
   507  		return fmt.Errorf("invalid droplet id: %v", err)
   508  	}
   509  
   510  	client := meta.(*godo.Client)
   511  	_, _, err = client.DropletActions.PowerOn(id)
   512  	if err != nil {
   513  		return err
   514  	}
   515  
   516  	// Wait for power on
   517  	_, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client)
   518  	if err != nil {
   519  		return err
   520  	}
   521  
   522  	return nil
   523  }