github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/builtin/providers/triton/resource_machine.go (about)

     1  package triton
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"regexp"
     7  	"time"
     8  
     9  	"github.com/hashicorp/terraform/helper/schema"
    10  	"github.com/joyent/gosdc/cloudapi"
    11  )
    12  
    13  var (
    14  	machineStateRunning = "running"
    15  	machineStateStopped = "stopped"
    16  	machineStateDeleted = "deleted"
    17  
    18  	machineStateChangeTimeout       = 10 * time.Minute
    19  	machineStateChangeCheckInterval = 10 * time.Second
    20  
    21  	resourceMachineMetadataKeys = map[string]string{
    22  		// semantics: "schema_name": "metadata_name"
    23  		"root_authorized_keys": "root_authorized_keys",
    24  		"user_script":          "user-script",
    25  		"user_data":            "user-data",
    26  		"administrator_pw":     "administrator-pw",
    27  	}
    28  )
    29  
    30  func resourceMachine() *schema.Resource {
    31  	return &schema.Resource{
    32  		Create: resourceMachineCreate,
    33  		Exists: resourceMachineExists,
    34  		Read:   resourceMachineRead,
    35  		Update: resourceMachineUpdate,
    36  		Delete: resourceMachineDelete,
    37  
    38  		Schema: map[string]*schema.Schema{
    39  			"name": {
    40  				Description:  "friendly name",
    41  				Type:         schema.TypeString,
    42  				Optional:     true,
    43  				Computed:     true,
    44  				ValidateFunc: resourceMachineValidateName,
    45  			},
    46  			"type": {
    47  				Description: "machine type (smartmachine or virtualmachine)",
    48  				Type:        schema.TypeString,
    49  				Computed:    true,
    50  			},
    51  			"state": {
    52  				Description: "current state of the machine",
    53  				Type:        schema.TypeString,
    54  				Computed:    true,
    55  			},
    56  			"dataset": {
    57  				Description: "dataset URN the machine was provisioned with",
    58  				Type:        schema.TypeString,
    59  				Computed:    true,
    60  			},
    61  			"memory": {
    62  				Description: "amount of memory the machine has (in Mb)",
    63  				Type:        schema.TypeInt,
    64  				Computed:    true,
    65  			},
    66  			"disk": {
    67  				Description: "amount of disk the machine has (in Gb)",
    68  				Type:        schema.TypeInt,
    69  				Computed:    true,
    70  			},
    71  			"ips": {
    72  				Description: "IP addresses the machine has",
    73  				Type:        schema.TypeList,
    74  				Computed:    true,
    75  				Elem: &schema.Schema{
    76  					Type: schema.TypeString,
    77  				},
    78  			},
    79  			"tags": {
    80  				Description: "machine tags",
    81  				Type:        schema.TypeMap,
    82  				Optional:    true,
    83  			},
    84  			"created": {
    85  				Description: "when the machine was created",
    86  				Type:        schema.TypeString,
    87  				Computed:    true,
    88  			},
    89  			"updated": {
    90  				Description: "when the machine was update",
    91  				Type:        schema.TypeString,
    92  				Computed:    true,
    93  			},
    94  			"package": {
    95  				Description: "name of the package to use on provisioning",
    96  				Type:        schema.TypeString,
    97  				Required:    true,
    98  			},
    99  			"image": {
   100  				Description: "image UUID",
   101  				Type:        schema.TypeString,
   102  				Required:    true,
   103  				ForceNew:    true,
   104  				// TODO: validate that the UUID is valid
   105  			},
   106  			"primaryip": {
   107  				Description: "the primary (public) IP address for the machine",
   108  				Type:        schema.TypeString,
   109  				Computed:    true,
   110  			},
   111  			"networks": {
   112  				Description: "desired network IDs",
   113  				Type:        schema.TypeList,
   114  				Optional:    true,
   115  				Computed:    true,
   116  				// TODO: this really should ForceNew but the Network IDs don't seem to
   117  				// be returned by the API, meaning if we track them here TF will replace
   118  				// the resource on every run.
   119  				// ForceNew:    true,
   120  				Elem: &schema.Schema{
   121  					Type: schema.TypeString,
   122  				},
   123  			},
   124  			"firewall_enabled": {
   125  				Description: "enable firewall for this machine",
   126  				Type:        schema.TypeBool,
   127  				Optional:    true,
   128  				Default:     false,
   129  			},
   130  
   131  			// computed resources from metadata
   132  			"root_authorized_keys": {
   133  				Description: "authorized keys for the root user on this machine",
   134  				Type:        schema.TypeString,
   135  				Optional:    true,
   136  				Computed:    true,
   137  			},
   138  			"user_script": {
   139  				Description: "user script to run on boot (every boot on SmartMachines)",
   140  				Type:        schema.TypeString,
   141  				Optional:    true,
   142  				Computed:    true,
   143  			},
   144  			"user_data": {
   145  				Description: "copied to machine on boot",
   146  				Type:        schema.TypeString,
   147  				Optional:    true,
   148  				Computed:    true,
   149  			},
   150  			"administrator_pw": {
   151  				Description: "administrator's initial password (Windows only)",
   152  				Type:        schema.TypeString,
   153  				Optional:    true,
   154  				Computed:    true,
   155  			},
   156  		},
   157  	}
   158  }
   159  
   160  func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
   161  	client := meta.(*cloudapi.Client)
   162  
   163  	var networks []string
   164  	for _, network := range d.Get("networks").([]interface{}) {
   165  		networks = append(networks, network.(string))
   166  	}
   167  
   168  	metadata := map[string]string{}
   169  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   170  		if v, ok := d.GetOk(schemaName); ok {
   171  			metadata[metadataKey] = v.(string)
   172  		}
   173  	}
   174  
   175  	tags := map[string]string{}
   176  	for k, v := range d.Get("tags").(map[string]interface{}) {
   177  		tags[k] = v.(string)
   178  	}
   179  
   180  	machine, err := client.CreateMachine(cloudapi.CreateMachineOpts{
   181  		Name:            d.Get("name").(string),
   182  		Package:         d.Get("package").(string),
   183  		Image:           d.Get("image").(string),
   184  		Networks:        networks,
   185  		Metadata:        metadata,
   186  		Tags:            tags,
   187  		FirewallEnabled: d.Get("firewall_enabled").(bool),
   188  	})
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	// refresh state after it provisions
   199  	d.SetId(machine.Id)
   200  	err = resourceMachineRead(d, meta)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   209  	client := meta.(*cloudapi.Client)
   210  
   211  	machine, err := client.GetMachine(d.Id())
   212  
   213  	return machine != nil && err == nil, err
   214  }
   215  
   216  func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
   217  	client := meta.(*cloudapi.Client)
   218  
   219  	machine, err := client.GetMachine(d.Id())
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	d.SetId(machine.Id)
   225  	d.Set("name", machine.Name)
   226  	d.Set("type", machine.Type)
   227  	d.Set("state", machine.State)
   228  	d.Set("dataset", machine.Dataset)
   229  	d.Set("memory", machine.Memory)
   230  	d.Set("disk", machine.Disk)
   231  	d.Set("ips", machine.IPs)
   232  	d.Set("tags", machine.Tags)
   233  	d.Set("created", machine.Created)
   234  	d.Set("updated", machine.Updated)
   235  	d.Set("package", machine.Package)
   236  	d.Set("image", machine.Image)
   237  	d.Set("primaryip", machine.PrimaryIP)
   238  	d.Set("networks", machine.Networks)
   239  	d.Set("firewall_enabled", machine.FirewallEnabled)
   240  
   241  	// computed attributes from metadata
   242  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   243  		d.Set(schemaName, machine.Metadata[metadataKey])
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
   250  	client := meta.(*cloudapi.Client)
   251  
   252  	d.Partial(true)
   253  
   254  	if d.HasChange("name") {
   255  		if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil {
   256  			return err
   257  		}
   258  
   259  		err := waitFor(
   260  			func() (bool, error) {
   261  				machine, err := client.GetMachine(d.Id())
   262  				return machine.Name == d.Get("name").(string), err
   263  			},
   264  			machineStateChangeCheckInterval,
   265  			1*time.Minute,
   266  		)
   267  		if err != nil {
   268  			return err
   269  		}
   270  
   271  		d.SetPartial("name")
   272  	}
   273  
   274  	if d.HasChange("tags") {
   275  		tags := map[string]string{}
   276  		for k, v := range d.Get("tags").(map[string]interface{}) {
   277  			tags[k] = v.(string)
   278  		}
   279  
   280  		var err error
   281  		if len(tags) == 0 {
   282  			err = client.DeleteMachineTags(d.Id())
   283  		} else {
   284  			_, err = client.ReplaceMachineTags(d.Id(), tags)
   285  		}
   286  		if err != nil {
   287  			return err
   288  		}
   289  
   290  		err = waitFor(
   291  			func() (bool, error) {
   292  				machine, err := client.GetMachine(d.Id())
   293  				return reflect.DeepEqual(machine.Tags, tags), err
   294  			},
   295  			machineStateChangeCheckInterval,
   296  			1*time.Minute,
   297  		)
   298  		if err != nil {
   299  			return err
   300  		}
   301  
   302  		d.SetPartial("tags")
   303  	}
   304  
   305  	if d.HasChange("package") {
   306  		if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil {
   307  			return err
   308  		}
   309  
   310  		err := waitFor(
   311  			func() (bool, error) {
   312  				machine, err := client.GetMachine(d.Id())
   313  				return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err
   314  			},
   315  			machineStateChangeCheckInterval,
   316  			machineStateChangeTimeout,
   317  		)
   318  		if err != nil {
   319  			return err
   320  		}
   321  
   322  		d.SetPartial("package")
   323  	}
   324  
   325  	if d.HasChange("firewall_enabled") {
   326  		var err error
   327  		if d.Get("firewall_enabled").(bool) {
   328  			err = client.EnableFirewallMachine(d.Id())
   329  		} else {
   330  			err = client.DisableFirewallMachine(d.Id())
   331  		}
   332  		if err != nil {
   333  			return err
   334  		}
   335  
   336  		d.SetPartial("firewall_enabled")
   337  	}
   338  
   339  	// metadata stuff
   340  	metadata := map[string]string{}
   341  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   342  		if d.HasChange(schemaName) {
   343  			metadata[metadataKey] = d.Get(schemaName).(string)
   344  		}
   345  	}
   346  	if len(metadata) > 0 {
   347  		_, err := client.UpdateMachineMetadata(d.Id(), metadata)
   348  		if err != nil {
   349  			return err
   350  		}
   351  
   352  		err = waitFor(
   353  			func() (bool, error) {
   354  				machine, err := client.GetMachine(d.Id())
   355  				return reflect.DeepEqual(machine.Metadata, metadata), err
   356  			},
   357  			machineStateChangeCheckInterval,
   358  			1*time.Minute,
   359  		)
   360  		if err != nil {
   361  			return err
   362  		}
   363  
   364  		for schemaName := range resourceMachineMetadataKeys {
   365  			if d.HasChange(schemaName) {
   366  				d.SetPartial(schemaName)
   367  			}
   368  		}
   369  	}
   370  
   371  	d.Partial(false)
   372  
   373  	err := resourceMachineRead(d, meta)
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	return nil
   379  }
   380  
   381  func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error {
   382  	client := meta.(*cloudapi.Client)
   383  
   384  	state, err := readMachineState(client, d.Id())
   385  	if state != machineStateStopped {
   386  		err = client.StopMachine(d.Id())
   387  		if err != nil {
   388  			return err
   389  		}
   390  
   391  		waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout)
   392  	}
   393  
   394  	err = client.DeleteMachine(d.Id())
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout)
   400  	return nil
   401  }
   402  
   403  func readMachineState(api *cloudapi.Client, id string) (string, error) {
   404  	machine, err := api.GetMachine(id)
   405  	if err != nil {
   406  		return "", err
   407  	}
   408  
   409  	return machine.State, nil
   410  }
   411  
   412  // waitForMachineState waits for a machine to be in the desired state (waiting
   413  // some seconds between each poll). If it doesn't reach the state within the
   414  // duration specified in `timeout`, it returns ErrMachineStateTimeout.
   415  func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error {
   416  	return waitFor(
   417  		func() (bool, error) {
   418  			currentState, err := readMachineState(api, id)
   419  			return currentState == state, err
   420  		},
   421  		machineStateChangeCheckInterval,
   422  		machineStateChangeTimeout,
   423  	)
   424  }
   425  
   426  func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) {
   427  	warnings = []string{}
   428  	errors = []error{}
   429  
   430  	r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`)
   431  	if !r.Match([]byte(value.(string))) {
   432  		errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name))
   433  	}
   434  
   435  	return warnings, errors
   436  }