github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/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/hashcode"
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  	"github.com/joyent/gosdc/cloudapi"
    12  )
    13  
    14  var (
    15  	machineStateRunning = "running"
    16  	machineStateStopped = "stopped"
    17  	machineStateDeleted = "deleted"
    18  
    19  	machineStateChangeTimeout       = 10 * time.Minute
    20  	machineStateChangeCheckInterval = 10 * time.Second
    21  
    22  	resourceMachineMetadataKeys = map[string]string{
    23  		// semantics: "schema_name": "metadata_name"
    24  		"root_authorized_keys": "root_authorized_keys",
    25  		"user_script":          "user-script",
    26  		"user_data":            "user-data",
    27  		"administrator_pw":     "administrator-pw",
    28  	}
    29  )
    30  
    31  func resourceMachine() *schema.Resource {
    32  	return &schema.Resource{
    33  		Create: resourceMachineCreate,
    34  		Exists: resourceMachineExists,
    35  		Read:   resourceMachineRead,
    36  		Update: resourceMachineUpdate,
    37  		Delete: resourceMachineDelete,
    38  		Importer: &schema.ResourceImporter{
    39  			State: resourceMachineImporter,
    40  		},
    41  
    42  		Schema: map[string]*schema.Schema{
    43  			"name": {
    44  				Description:  "friendly name",
    45  				Type:         schema.TypeString,
    46  				Optional:     true,
    47  				Computed:     true,
    48  				ValidateFunc: resourceMachineValidateName,
    49  			},
    50  			"type": {
    51  				Description: "machine type (smartmachine or virtualmachine)",
    52  				Type:        schema.TypeString,
    53  				Computed:    true,
    54  			},
    55  			"state": {
    56  				Description: "current state of the machine",
    57  				Type:        schema.TypeString,
    58  				Computed:    true,
    59  			},
    60  			"dataset": {
    61  				Description: "dataset URN the machine was provisioned with",
    62  				Type:        schema.TypeString,
    63  				Computed:    true,
    64  			},
    65  			"memory": {
    66  				Description: "amount of memory the machine has (in Mb)",
    67  				Type:        schema.TypeInt,
    68  				Computed:    true,
    69  			},
    70  			"disk": {
    71  				Description: "amount of disk the machine has (in Gb)",
    72  				Type:        schema.TypeInt,
    73  				Computed:    true,
    74  			},
    75  			"ips": {
    76  				Description: "IP addresses the machine has",
    77  				Type:        schema.TypeList,
    78  				Computed:    true,
    79  				Elem: &schema.Schema{
    80  					Type: schema.TypeString,
    81  				},
    82  			},
    83  			"tags": {
    84  				Description: "machine tags",
    85  				Type:        schema.TypeMap,
    86  				Optional:    true,
    87  			},
    88  			"created": {
    89  				Description: "when the machine was created",
    90  				Type:        schema.TypeString,
    91  				Computed:    true,
    92  			},
    93  			"updated": {
    94  				Description: "when the machine was update",
    95  				Type:        schema.TypeString,
    96  				Computed:    true,
    97  			},
    98  			"package": {
    99  				Description: "name of the package to use on provisioning",
   100  				Type:        schema.TypeString,
   101  				Required:    true,
   102  			},
   103  			"image": {
   104  				Description: "image UUID",
   105  				Type:        schema.TypeString,
   106  				Required:    true,
   107  				ForceNew:    true,
   108  				// TODO: validate that the UUID is valid
   109  			},
   110  			"primaryip": {
   111  				Description: "the primary (public) IP address for the machine",
   112  				Type:        schema.TypeString,
   113  				Computed:    true,
   114  			},
   115  			"nic": {
   116  				Description: "network interface",
   117  				Type:        schema.TypeSet,
   118  				Computed:    true,
   119  				Optional:    true,
   120  				Set: func(v interface{}) int {
   121  					m := v.(map[string]interface{})
   122  					return hashcode.String(m["network"].(string))
   123  				},
   124  				Elem: &schema.Resource{
   125  					Schema: map[string]*schema.Schema{
   126  						"ip": {
   127  							Description: "NIC's IPv4 address",
   128  							Computed:    true,
   129  							Type:        schema.TypeString,
   130  						},
   131  						"mac": {
   132  							Description: "NIC's MAC address",
   133  							Computed:    true,
   134  							Type:        schema.TypeString,
   135  						},
   136  						"primary": {
   137  							Description: "Whether this is the machine's primary NIC",
   138  							Computed:    true,
   139  							Type:        schema.TypeBool,
   140  						},
   141  						"netmask": {
   142  							Description: "IPv4 netmask",
   143  							Computed:    true,
   144  							Type:        schema.TypeString,
   145  						},
   146  						"gateway": {
   147  							Description: "IPv4 gateway",
   148  							Computed:    true,
   149  							Type:        schema.TypeString,
   150  						},
   151  						"state": {
   152  							Description: "describes the state of the NIC (e.g. provisioning, running, or stopped)",
   153  							Computed:    true,
   154  							Type:        schema.TypeString,
   155  						},
   156  						"network": {
   157  							Description: "Network ID this NIC is attached to",
   158  							Required:    true,
   159  							Type:        schema.TypeString,
   160  						},
   161  					},
   162  				},
   163  			},
   164  			"firewall_enabled": {
   165  				Description: "enable firewall for this machine",
   166  				Type:        schema.TypeBool,
   167  				Optional:    true,
   168  				Default:     false,
   169  			},
   170  			"domain_names": {
   171  				Description: "list of domain names from Triton's CNS",
   172  				Type:        schema.TypeList,
   173  				Computed:    true,
   174  				Elem: &schema.Schema{
   175  					Type: schema.TypeString,
   176  				},
   177  			},
   178  
   179  			// computed resources from metadata
   180  			"root_authorized_keys": {
   181  				Description: "authorized keys for the root user on this machine",
   182  				Type:        schema.TypeString,
   183  				Optional:    true,
   184  				Computed:    true,
   185  			},
   186  			"user_script": {
   187  				Description: "user script to run on boot (every boot on SmartMachines)",
   188  				Type:        schema.TypeString,
   189  				Optional:    true,
   190  				Computed:    true,
   191  			},
   192  			"user_data": {
   193  				Description: "copied to machine on boot",
   194  				Type:        schema.TypeString,
   195  				Optional:    true,
   196  				Computed:    true,
   197  			},
   198  			"administrator_pw": {
   199  				Description: "administrator's initial password (Windows only)",
   200  				Type:        schema.TypeString,
   201  				Optional:    true,
   202  				Computed:    true,
   203  			},
   204  
   205  			// deprecated fields
   206  			"networks": {
   207  				Description: "desired network IDs",
   208  				Type:        schema.TypeList,
   209  				Optional:    true,
   210  				Computed:    true,
   211  				Deprecated:  "Networks is deprecated, please use `nic`",
   212  				Elem: &schema.Schema{
   213  					Type: schema.TypeString,
   214  				},
   215  			},
   216  		},
   217  	}
   218  }
   219  
   220  func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
   221  	client := meta.(*cloudapi.Client)
   222  
   223  	var networks []string
   224  	for _, network := range d.Get("networks").([]interface{}) {
   225  		networks = append(networks, network.(string))
   226  	}
   227  	nics := d.Get("nic").(*schema.Set)
   228  	for _, nicI := range nics.List() {
   229  		nic := nicI.(map[string]interface{})
   230  		networks = append(networks, nic["network"].(string))
   231  	}
   232  
   233  	metadata := map[string]string{}
   234  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   235  		if v, ok := d.GetOk(schemaName); ok {
   236  			metadata[metadataKey] = v.(string)
   237  		}
   238  	}
   239  
   240  	tags := map[string]string{}
   241  	for k, v := range d.Get("tags").(map[string]interface{}) {
   242  		tags[k] = v.(string)
   243  	}
   244  
   245  	machine, err := client.CreateMachine(cloudapi.CreateMachineOpts{
   246  		Name:            d.Get("name").(string),
   247  		Package:         d.Get("package").(string),
   248  		Image:           d.Get("image").(string),
   249  		Networks:        networks,
   250  		Metadata:        metadata,
   251  		Tags:            tags,
   252  		FirewallEnabled: d.Get("firewall_enabled").(bool),
   253  	})
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout)
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	// refresh state after it provisions
   264  	d.SetId(machine.Id)
   265  	err = resourceMachineRead(d, meta)
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   274  	client := meta.(*cloudapi.Client)
   275  
   276  	machine, err := client.GetMachine(d.Id())
   277  
   278  	return machine != nil && err == nil, err
   279  }
   280  
   281  func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
   282  	client := meta.(*cloudapi.Client)
   283  
   284  	machine, err := client.GetMachine(d.Id())
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	nics, err := client.ListNICs(d.Id())
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	d.SetId(machine.Id)
   295  	d.Set("name", machine.Name)
   296  	d.Set("type", machine.Type)
   297  	d.Set("state", machine.State)
   298  	d.Set("dataset", machine.Dataset)
   299  	d.Set("memory", machine.Memory)
   300  	d.Set("disk", machine.Disk)
   301  	d.Set("ips", machine.IPs)
   302  	d.Set("tags", machine.Tags)
   303  	d.Set("created", machine.Created)
   304  	d.Set("updated", machine.Updated)
   305  	d.Set("package", machine.Package)
   306  	d.Set("image", machine.Image)
   307  	d.Set("primaryip", machine.PrimaryIP)
   308  	d.Set("firewall_enabled", machine.FirewallEnabled)
   309  	d.Set("domain_names", machine.DomainNames)
   310  
   311  	// create and update NICs
   312  	var (
   313  		machineNICs []map[string]interface{}
   314  		networks    []string
   315  	)
   316  	for _, nic := range nics {
   317  		machineNICs = append(
   318  			machineNICs,
   319  			map[string]interface{}{
   320  				"ip":      nic.IP,
   321  				"mac":     nic.MAC,
   322  				"primary": nic.Primary,
   323  				"netmask": nic.Netmask,
   324  				"gateway": nic.Gateway,
   325  				"state":   nic.State,
   326  				"network": nic.Network,
   327  			},
   328  		)
   329  		networks = append(networks, nic.Network)
   330  	}
   331  	d.Set("nic", machineNICs)
   332  	d.Set("networks", networks)
   333  
   334  	// computed attributes from metadata
   335  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   336  		d.Set(schemaName, machine.Metadata[metadataKey])
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
   343  	client := meta.(*cloudapi.Client)
   344  
   345  	d.Partial(true)
   346  
   347  	if d.HasChange("name") {
   348  		if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil {
   349  			return err
   350  		}
   351  
   352  		err := waitFor(
   353  			func() (bool, error) {
   354  				machine, err := client.GetMachine(d.Id())
   355  				return machine.Name == d.Get("name").(string), err
   356  			},
   357  			machineStateChangeCheckInterval,
   358  			1*time.Minute,
   359  		)
   360  		if err != nil {
   361  			return err
   362  		}
   363  
   364  		d.SetPartial("name")
   365  	}
   366  
   367  	if d.HasChange("tags") {
   368  		tags := map[string]string{}
   369  		for k, v := range d.Get("tags").(map[string]interface{}) {
   370  			tags[k] = v.(string)
   371  		}
   372  
   373  		var err error
   374  		if len(tags) == 0 {
   375  			err = client.DeleteMachineTags(d.Id())
   376  		} else {
   377  			_, err = client.ReplaceMachineTags(d.Id(), tags)
   378  		}
   379  		if err != nil {
   380  			return err
   381  		}
   382  
   383  		err = waitFor(
   384  			func() (bool, error) {
   385  				machine, err := client.GetMachine(d.Id())
   386  				return reflect.DeepEqual(machine.Tags, tags), err
   387  			},
   388  			machineStateChangeCheckInterval,
   389  			1*time.Minute,
   390  		)
   391  		if err != nil {
   392  			return err
   393  		}
   394  
   395  		d.SetPartial("tags")
   396  	}
   397  
   398  	if d.HasChange("package") {
   399  		if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil {
   400  			return err
   401  		}
   402  
   403  		err := waitFor(
   404  			func() (bool, error) {
   405  				machine, err := client.GetMachine(d.Id())
   406  				return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err
   407  			},
   408  			machineStateChangeCheckInterval,
   409  			machineStateChangeTimeout,
   410  		)
   411  		if err != nil {
   412  			return err
   413  		}
   414  
   415  		d.SetPartial("package")
   416  	}
   417  
   418  	if d.HasChange("firewall_enabled") {
   419  		var err error
   420  		if d.Get("firewall_enabled").(bool) {
   421  			err = client.EnableFirewallMachine(d.Id())
   422  		} else {
   423  			err = client.DisableFirewallMachine(d.Id())
   424  		}
   425  		if err != nil {
   426  			return err
   427  		}
   428  
   429  		err = waitFor(
   430  			func() (bool, error) {
   431  				machine, err := client.GetMachine(d.Id())
   432  				return machine.FirewallEnabled == d.Get("firewall_enabled").(bool), err
   433  			},
   434  			machineStateChangeCheckInterval,
   435  			machineStateChangeTimeout,
   436  		)
   437  
   438  		if err != nil {
   439  			return err
   440  		}
   441  
   442  		d.SetPartial("firewall_enabled")
   443  	}
   444  
   445  	if d.HasChange("nic") {
   446  		o, n := d.GetChange("nic")
   447  		if o == nil {
   448  			o = new(schema.Set)
   449  		}
   450  		if n == nil {
   451  			n = new(schema.Set)
   452  		}
   453  
   454  		oldNICs := o.(*schema.Set)
   455  		newNICs := o.(*schema.Set)
   456  
   457  		// add new NICs that are not in old NICs
   458  		for _, nicI := range newNICs.Difference(oldNICs).List() {
   459  			nic := nicI.(map[string]interface{})
   460  			fmt.Printf("adding %+v\n", nic)
   461  			_, err := client.AddNIC(d.Id(), nic["network"].(string))
   462  			if err != nil {
   463  				return err
   464  			}
   465  		}
   466  
   467  		// remove old NICs that are not in new NICs
   468  		for _, nicI := range oldNICs.Difference(newNICs).List() {
   469  			nic := nicI.(map[string]interface{})
   470  			fmt.Printf("removing %+v\n", nic)
   471  			err := client.RemoveNIC(d.Id(), nic["mac"].(string))
   472  			if err != nil {
   473  				return err
   474  			}
   475  		}
   476  
   477  		d.SetPartial("nic")
   478  	}
   479  
   480  	// metadata stuff
   481  	metadata := map[string]string{}
   482  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   483  		if d.HasChange(schemaName) {
   484  			metadata[metadataKey] = d.Get(schemaName).(string)
   485  		}
   486  	}
   487  	if len(metadata) > 0 {
   488  		_, err := client.UpdateMachineMetadata(d.Id(), metadata)
   489  		if err != nil {
   490  			return err
   491  		}
   492  
   493  		err = waitFor(
   494  			func() (bool, error) {
   495  				machine, err := client.GetMachine(d.Id())
   496  				for k, v := range metadata {
   497  					if provider_v, ok := machine.Metadata[k]; !ok || v != provider_v {
   498  						return false, err
   499  					}
   500  				}
   501  				return true, err
   502  			},
   503  			machineStateChangeCheckInterval,
   504  			1*time.Minute,
   505  		)
   506  		if err != nil {
   507  			return err
   508  		}
   509  
   510  		for schemaName := range resourceMachineMetadataKeys {
   511  			if d.HasChange(schemaName) {
   512  				d.SetPartial(schemaName)
   513  			}
   514  		}
   515  	}
   516  
   517  	d.Partial(false)
   518  
   519  	err := resourceMachineRead(d, meta)
   520  	if err != nil {
   521  		return err
   522  	}
   523  
   524  	return nil
   525  }
   526  
   527  func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error {
   528  	client := meta.(*cloudapi.Client)
   529  
   530  	state, err := readMachineState(client, d.Id())
   531  	if state != machineStateStopped {
   532  		err = client.StopMachine(d.Id())
   533  		if err != nil {
   534  			return err
   535  		}
   536  
   537  		waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout)
   538  	}
   539  
   540  	err = client.DeleteMachine(d.Id())
   541  	if err != nil {
   542  		return err
   543  	}
   544  
   545  	waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout)
   546  	return nil
   547  }
   548  
   549  func readMachineState(api *cloudapi.Client, id string) (string, error) {
   550  	machine, err := api.GetMachine(id)
   551  	if err != nil {
   552  		return "", err
   553  	}
   554  
   555  	return machine.State, nil
   556  }
   557  
   558  // waitForMachineState waits for a machine to be in the desired state (waiting
   559  // some seconds between each poll). If it doesn't reach the state within the
   560  // duration specified in `timeout`, it returns ErrMachineStateTimeout.
   561  func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error {
   562  	return waitFor(
   563  		func() (bool, error) {
   564  			currentState, err := readMachineState(api, id)
   565  			return currentState == state, err
   566  		},
   567  		machineStateChangeCheckInterval,
   568  		machineStateChangeTimeout,
   569  	)
   570  }
   571  
   572  func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) {
   573  	warnings = []string{}
   574  	errors = []error{}
   575  
   576  	r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`)
   577  	if !r.Match([]byte(value.(string))) {
   578  		errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name))
   579  	}
   580  
   581  	return warnings, errors
   582  }
   583  
   584  func resourceMachineImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
   585  	return []*schema.ResourceData{d}, nil
   586  }