github.com/IBM-Cloud/terraform@v0.6.4-0.20170726051544-8872b87621df/builtin/providers/triton/resource_machine.go (about)

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