github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/triton/resource_machine.go (about)

     1  package triton
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"time"
     7  
     8  	"github.com/hashicorp/terraform/helper/hashcode"
     9  	"github.com/hashicorp/terraform/helper/resource"
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  	"github.com/joyent/triton-go"
    12  )
    13  
    14  var (
    15  	machineStateRunning = "running"
    16  	machineStateDeleted = "deleted"
    17  
    18  	machineStateChangeTimeout = 10 * time.Minute
    19  
    20  	resourceMachineMetadataKeys = map[string]string{
    21  		// semantics: "schema_name": "metadata_name"
    22  		"root_authorized_keys": "root_authorized_keys",
    23  		"user_script":          "user-script",
    24  		"user_data":            "user-data",
    25  		"administrator_pw":     "administrator-pw",
    26  		"cloud_config":         "cloud-init:user-data",
    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  		Timeouts: slowResourceTimeout,
    38  		Importer: &schema.ResourceImporter{
    39  			State: schema.ImportStatePassthrough,
    40  		},
    41  
    42  		Schema: map[string]*schema.Schema{
    43  			"name": {
    44  				Description:  "Friendly name for machine",
    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  			"dataset": {
    56  				Description: "Dataset URN with which the machine was provisioned",
    57  				Type:        schema.TypeString,
    58  				Computed:    true,
    59  			},
    60  			"memory": {
    61  				Description: "Amount of memory allocated to the machine (in Mb)",
    62  				Type:        schema.TypeInt,
    63  				Computed:    true,
    64  			},
    65  			"disk": {
    66  				Description: "Amount of disk allocated to the machine (in Gb)",
    67  				Type:        schema.TypeInt,
    68  				Computed:    true,
    69  			},
    70  			"ips": {
    71  				Description: "IP addresses assigned to the machine",
    72  				Type:        schema.TypeList,
    73  				Computed:    true,
    74  				Elem: &schema.Schema{
    75  					Type: schema.TypeString,
    76  				},
    77  			},
    78  			"tags": {
    79  				Description: "Machine tags",
    80  				Type:        schema.TypeMap,
    81  				Optional:    true,
    82  			},
    83  			"created": {
    84  				Description: "When the machine was created",
    85  				Type:        schema.TypeString,
    86  				Computed:    true,
    87  			},
    88  			"updated": {
    89  				Description: "When the machine was updated",
    90  				Type:        schema.TypeString,
    91  				Computed:    true,
    92  			},
    93  			"package": {
    94  				Description: "The package for use for provisioning",
    95  				Type:        schema.TypeString,
    96  				Required:    true,
    97  			},
    98  			"image": {
    99  				Description: "UUID of the image",
   100  				Type:        schema.TypeString,
   101  				Required:    true,
   102  				ForceNew:    true,
   103  			},
   104  			"primaryip": {
   105  				Description: "Primary (public) IP address for the machine",
   106  				Type:        schema.TypeString,
   107  				Computed:    true,
   108  			},
   109  			"nic": {
   110  				Description: "Network interface",
   111  				Type:        schema.TypeSet,
   112  				Computed:    true,
   113  				Optional:    true,
   114  				Set: func(v interface{}) int {
   115  					m := v.(map[string]interface{})
   116  					return hashcode.String(m["network"].(string))
   117  				},
   118  				Elem: &schema.Resource{
   119  					Schema: map[string]*schema.Schema{
   120  						"ip": {
   121  							Description: "NIC's IPv4 address",
   122  							Computed:    true,
   123  							Type:        schema.TypeString,
   124  						},
   125  						"mac": {
   126  							Description: "NIC's MAC address",
   127  							Computed:    true,
   128  							Type:        schema.TypeString,
   129  						},
   130  						"primary": {
   131  							Description: "Whether this is the machine's primary NIC",
   132  							Computed:    true,
   133  							Type:        schema.TypeBool,
   134  						},
   135  						"netmask": {
   136  							Description: "IPv4 netmask",
   137  							Computed:    true,
   138  							Type:        schema.TypeString,
   139  						},
   140  						"gateway": {
   141  							Description: "IPv4 gateway",
   142  							Computed:    true,
   143  							Type:        schema.TypeString,
   144  						},
   145  						"network": {
   146  							Description: "ID of the network to which the NIC is attached",
   147  							Required:    true,
   148  							Type:        schema.TypeString,
   149  						},
   150  						"state": {
   151  							Description: "Provisioning state of the NIC",
   152  							Computed:    true,
   153  							Type:        schema.TypeString,
   154  						},
   155  					},
   156  				},
   157  			},
   158  			"firewall_enabled": {
   159  				Description: "Whether to enable the firewall for this machine",
   160  				Type:        schema.TypeBool,
   161  				Optional:    true,
   162  				Default:     false,
   163  			},
   164  			"domain_names": {
   165  				Description: "List of domain names from Triton CNS",
   166  				Type:        schema.TypeList,
   167  				Computed:    true,
   168  				Elem: &schema.Schema{
   169  					Type: schema.TypeString,
   170  				},
   171  			},
   172  
   173  			// computed resources from metadata
   174  			"root_authorized_keys": {
   175  				Description: "Authorized keys for the root user on this machine",
   176  				Type:        schema.TypeString,
   177  				Optional:    true,
   178  				Computed:    true,
   179  			},
   180  			"user_script": {
   181  				Description: "User script to run on boot (every boot on SmartMachines)",
   182  				Type:        schema.TypeString,
   183  				Optional:    true,
   184  				Computed:    true,
   185  			},
   186  			"cloud_config": {
   187  				Description: "copied to machine on boot",
   188  				Type:        schema.TypeString,
   189  				Optional:    true,
   190  				Computed:    true,
   191  			},
   192  			"user_data": {
   193  				Description: "Data 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.(*triton.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.Machines().CreateMachine(&triton.CreateMachineInput{
   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  	d.SetId(machine.ID)
   259  	stateConf := &resource.StateChangeConf{
   260  		Target: []string{fmt.Sprintf(machineStateRunning)},
   261  		Refresh: func() (interface{}, string, error) {
   262  			getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   263  				ID: d.Id(),
   264  			})
   265  			if err != nil {
   266  				return nil, "", err
   267  			}
   268  
   269  			return getResp, getResp.State, nil
   270  		},
   271  		Timeout:    machineStateChangeTimeout,
   272  		MinTimeout: 3 * time.Second,
   273  	}
   274  	_, err = stateConf.WaitForState()
   275  	if err != nil {
   276  		return err
   277  	}
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	// refresh state after it provisions
   283  	return resourceMachineRead(d, meta)
   284  }
   285  
   286  func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   287  	client := meta.(*triton.Client)
   288  
   289  	return resourceExists(client.Machines().GetMachine(&triton.GetMachineInput{
   290  		ID: d.Id(),
   291  	}))
   292  }
   293  
   294  func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
   295  	client := meta.(*triton.Client)
   296  
   297  	machine, err := client.Machines().GetMachine(&triton.GetMachineInput{
   298  		ID: d.Id(),
   299  	})
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	nics, err := client.Machines().ListNICs(&triton.ListNICsInput{
   305  		MachineID: d.Id(),
   306  	})
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	d.Set("name", machine.Name)
   312  	d.Set("type", machine.Type)
   313  	d.Set("state", machine.State)
   314  	d.Set("dataset", machine.Image)
   315  	d.Set("image", machine.Image)
   316  	d.Set("memory", machine.Memory)
   317  	d.Set("disk", machine.Disk)
   318  	d.Set("ips", machine.IPs)
   319  	d.Set("tags", machine.Tags)
   320  	d.Set("created", machine.Created)
   321  	d.Set("updated", machine.Updated)
   322  	d.Set("package", machine.Package)
   323  	d.Set("image", machine.Image)
   324  	d.Set("primaryip", machine.PrimaryIP)
   325  	d.Set("firewall_enabled", machine.FirewallEnabled)
   326  	d.Set("domain_names", machine.DomainNames)
   327  
   328  	// create and update NICs
   329  	var (
   330  		machineNICs []map[string]interface{}
   331  		networks    []string
   332  	)
   333  	for _, nic := range nics {
   334  		machineNICs = append(
   335  			machineNICs,
   336  			map[string]interface{}{
   337  				"ip":      nic.IP,
   338  				"mac":     nic.MAC,
   339  				"primary": nic.Primary,
   340  				"netmask": nic.Netmask,
   341  				"gateway": nic.Gateway,
   342  				"state":   nic.State,
   343  				"network": nic.Network,
   344  			},
   345  		)
   346  		networks = append(networks, nic.Network)
   347  	}
   348  	d.Set("nic", machineNICs)
   349  	d.Set("networks", networks)
   350  
   351  	// computed attributes from metadata
   352  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   353  		d.Set(schemaName, machine.Metadata[metadataKey])
   354  	}
   355  
   356  	return nil
   357  }
   358  
   359  func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
   360  	client := meta.(*triton.Client)
   361  
   362  	d.Partial(true)
   363  
   364  	if d.HasChange("name") {
   365  		oldNameInterface, newNameInterface := d.GetChange("name")
   366  		oldName := oldNameInterface.(string)
   367  		newName := newNameInterface.(string)
   368  
   369  		err := client.Machines().RenameMachine(&triton.RenameMachineInput{
   370  			ID:   d.Id(),
   371  			Name: newName,
   372  		})
   373  		if err != nil {
   374  			return err
   375  		}
   376  
   377  		stateConf := &resource.StateChangeConf{
   378  			Pending: []string{oldName},
   379  			Target:  []string{newName},
   380  			Refresh: func() (interface{}, string, error) {
   381  				getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   382  					ID: d.Id(),
   383  				})
   384  				if err != nil {
   385  					return nil, "", err
   386  				}
   387  
   388  				return getResp, getResp.Name, nil
   389  			},
   390  			Timeout:    machineStateChangeTimeout,
   391  			MinTimeout: 3 * time.Second,
   392  		}
   393  		_, err = stateConf.WaitForState()
   394  		if err != nil {
   395  			return err
   396  		}
   397  
   398  		d.SetPartial("name")
   399  	}
   400  
   401  	if d.HasChange("tags") {
   402  		tags := map[string]string{}
   403  		for k, v := range d.Get("tags").(map[string]interface{}) {
   404  			tags[k] = v.(string)
   405  		}
   406  
   407  		var err error
   408  		if len(tags) == 0 {
   409  			err = client.Machines().DeleteMachineTags(&triton.DeleteMachineTagsInput{
   410  				ID: d.Id(),
   411  			})
   412  		} else {
   413  			err = client.Machines().ReplaceMachineTags(&triton.ReplaceMachineTagsInput{
   414  				ID:   d.Id(),
   415  				Tags: tags,
   416  			})
   417  		}
   418  		if err != nil {
   419  			return err
   420  		}
   421  
   422  		expectedTagsMD5 := stableMapHash(tags)
   423  		stateConf := &resource.StateChangeConf{
   424  			Target: []string{expectedTagsMD5},
   425  			Refresh: func() (interface{}, string, error) {
   426  				getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   427  					ID: d.Id(),
   428  				})
   429  				if err != nil {
   430  					return nil, "", err
   431  				}
   432  
   433  				return getResp, stableMapHash(getResp.Tags), nil
   434  			},
   435  			Timeout:    machineStateChangeTimeout,
   436  			MinTimeout: 3 * time.Second,
   437  		}
   438  		_, err = stateConf.WaitForState()
   439  		if err != nil {
   440  			return err
   441  		}
   442  
   443  		d.SetPartial("tags")
   444  	}
   445  
   446  	if d.HasChange("package") {
   447  		newPackage := d.Get("package").(string)
   448  
   449  		err := client.Machines().ResizeMachine(&triton.ResizeMachineInput{
   450  			ID:      d.Id(),
   451  			Package: newPackage,
   452  		})
   453  		if err != nil {
   454  			return err
   455  		}
   456  
   457  		stateConf := &resource.StateChangeConf{
   458  			Target: []string{fmt.Sprintf("%s@%s", newPackage, "running")},
   459  			Refresh: func() (interface{}, string, error) {
   460  				getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   461  					ID: d.Id(),
   462  				})
   463  				if err != nil {
   464  					return nil, "", err
   465  				}
   466  
   467  				return getResp, fmt.Sprintf("%s@%s", getResp.Package, getResp.State), nil
   468  			},
   469  			Timeout:    machineStateChangeTimeout,
   470  			MinTimeout: 3 * time.Second,
   471  		}
   472  		_, err = stateConf.WaitForState()
   473  		if err != nil {
   474  			return err
   475  		}
   476  
   477  		d.SetPartial("package")
   478  	}
   479  
   480  	if d.HasChange("firewall_enabled") {
   481  		enable := d.Get("firewall_enabled").(bool)
   482  
   483  		var err error
   484  		if enable {
   485  			err = client.Machines().EnableMachineFirewall(&triton.EnableMachineFirewallInput{
   486  				ID: d.Id(),
   487  			})
   488  		} else {
   489  			err = client.Machines().DisableMachineFirewall(&triton.DisableMachineFirewallInput{
   490  				ID: d.Id(),
   491  			})
   492  		}
   493  		if err != nil {
   494  			return err
   495  		}
   496  
   497  		stateConf := &resource.StateChangeConf{
   498  			Target: []string{fmt.Sprintf("%t", enable)},
   499  			Refresh: func() (interface{}, string, error) {
   500  				getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   501  					ID: d.Id(),
   502  				})
   503  				if err != nil {
   504  					return nil, "", err
   505  				}
   506  
   507  				return getResp, fmt.Sprintf("%t", getResp.FirewallEnabled), nil
   508  			},
   509  			Timeout:    machineStateChangeTimeout,
   510  			MinTimeout: 3 * time.Second,
   511  		}
   512  		_, err = stateConf.WaitForState()
   513  		if err != nil {
   514  			return err
   515  		}
   516  
   517  		d.SetPartial("firewall_enabled")
   518  	}
   519  
   520  	if d.HasChange("nic") {
   521  		o, n := d.GetChange("nic")
   522  		if o == nil {
   523  			o = new(schema.Set)
   524  		}
   525  		if n == nil {
   526  			n = new(schema.Set)
   527  		}
   528  
   529  		oldNICs := o.(*schema.Set)
   530  		newNICs := n.(*schema.Set)
   531  
   532  		for _, nicI := range newNICs.Difference(oldNICs).List() {
   533  			nic := nicI.(map[string]interface{})
   534  			if _, err := client.Machines().AddNIC(&triton.AddNICInput{
   535  				MachineID: d.Id(),
   536  				Network:   nic["network"].(string),
   537  			}); err != nil {
   538  				return err
   539  			}
   540  		}
   541  
   542  		for _, nicI := range oldNICs.Difference(newNICs).List() {
   543  			nic := nicI.(map[string]interface{})
   544  			if err := client.Machines().RemoveNIC(&triton.RemoveNICInput{
   545  				MachineID: d.Id(),
   546  				MAC:       nic["mac"].(string),
   547  			}); err != nil {
   548  				return err
   549  			}
   550  		}
   551  
   552  		d.SetPartial("nic")
   553  	}
   554  
   555  	metadata := map[string]string{}
   556  	for schemaName, metadataKey := range resourceMachineMetadataKeys {
   557  		if d.HasChange(schemaName) {
   558  			metadata[metadataKey] = d.Get(schemaName).(string)
   559  		}
   560  	}
   561  	if len(metadata) > 0 {
   562  		if _, err := client.Machines().UpdateMachineMetadata(&triton.UpdateMachineMetadataInput{
   563  			ID:       d.Id(),
   564  			Metadata: metadata,
   565  		}); err != nil {
   566  			return err
   567  		}
   568  
   569  		stateConf := &resource.StateChangeConf{
   570  			Target: []string{"converged"},
   571  			Refresh: func() (interface{}, string, error) {
   572  				getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   573  					ID: d.Id(),
   574  				})
   575  				if err != nil {
   576  					return nil, "", err
   577  				}
   578  
   579  				for k, v := range metadata {
   580  					if upstream, ok := getResp.Metadata[k]; !ok || v != upstream {
   581  						return getResp, "converging", nil
   582  					}
   583  				}
   584  
   585  				return getResp, "converged", nil
   586  			},
   587  			Timeout:    machineStateChangeTimeout,
   588  			MinTimeout: 3 * time.Second,
   589  		}
   590  		_, err := stateConf.WaitForState()
   591  		if err != nil {
   592  			return err
   593  		}
   594  
   595  		for schemaName := range resourceMachineMetadataKeys {
   596  			if d.HasChange(schemaName) {
   597  				d.SetPartial(schemaName)
   598  			}
   599  		}
   600  	}
   601  
   602  	d.Partial(false)
   603  
   604  	return resourceMachineRead(d, meta)
   605  }
   606  
   607  func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error {
   608  	client := meta.(*triton.Client)
   609  
   610  	err := client.Machines().DeleteMachine(&triton.DeleteMachineInput{
   611  		ID: d.Id(),
   612  	})
   613  	if err != nil {
   614  		return err
   615  	}
   616  
   617  	stateConf := &resource.StateChangeConf{
   618  		Target: []string{machineStateDeleted},
   619  		Refresh: func() (interface{}, string, error) {
   620  			getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
   621  				ID: d.Id(),
   622  			})
   623  			if err != nil {
   624  				if triton.IsResourceNotFound(err) {
   625  					return nil, "deleted", nil
   626  				}
   627  				return nil, "", err
   628  			}
   629  
   630  			return getResp, getResp.State, nil
   631  		},
   632  		Timeout:    machineStateChangeTimeout,
   633  		MinTimeout: 3 * time.Second,
   634  	}
   635  	_, err = stateConf.WaitForState()
   636  	if err != nil {
   637  		return err
   638  	}
   639  
   640  	return nil
   641  }
   642  
   643  func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) {
   644  	warnings = []string{}
   645  	errors = []error{}
   646  
   647  	r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`)
   648  	if !r.Match([]byte(value.(string))) {
   649  		errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name))
   650  	}
   651  
   652  	return warnings, errors
   653  }