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