terraform/builtin/providers/triton/resource_machine.go

587 lines
14 KiB
Go

package triton
import (
"fmt"
"reflect"
"regexp"
"time"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gosdc/cloudapi"
)
var (
machineStateRunning = "running"
machineStateStopped = "stopped"
machineStateDeleted = "deleted"
machineStateChangeTimeout = 10 * time.Minute
machineStateChangeCheckInterval = 10 * time.Second
resourceMachineMetadataKeys = map[string]string{
// semantics: "schema_name": "metadata_name"
"root_authorized_keys": "root_authorized_keys",
"user_script": "user-script",
"user_data": "user-data",
"administrator_pw": "administrator-pw",
}
)
func resourceMachine() *schema.Resource {
return &schema.Resource{
Create: resourceMachineCreate,
Exists: resourceMachineExists,
Read: resourceMachineRead,
Update: resourceMachineUpdate,
Delete: resourceMachineDelete,
Importer: &schema.ResourceImporter{
State: resourceMachineImporter,
},
Schema: map[string]*schema.Schema{
"name": {
Description: "friendly name",
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: resourceMachineValidateName,
},
"type": {
Description: "machine type (smartmachine or virtualmachine)",
Type: schema.TypeString,
Computed: true,
},
"state": {
Description: "current state of the machine",
Type: schema.TypeString,
Computed: true,
},
"dataset": {
Description: "dataset URN the machine was provisioned with",
Type: schema.TypeString,
Computed: true,
},
"memory": {
Description: "amount of memory the machine has (in Mb)",
Type: schema.TypeInt,
Computed: true,
},
"disk": {
Description: "amount of disk the machine has (in Gb)",
Type: schema.TypeInt,
Computed: true,
},
"ips": {
Description: "IP addresses the machine has",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"tags": {
Description: "machine tags",
Type: schema.TypeMap,
Optional: true,
},
"created": {
Description: "when the machine was created",
Type: schema.TypeString,
Computed: true,
},
"updated": {
Description: "when the machine was update",
Type: schema.TypeString,
Computed: true,
},
"package": {
Description: "name of the package to use on provisioning",
Type: schema.TypeString,
Required: true,
},
"image": {
Description: "image UUID",
Type: schema.TypeString,
Required: true,
ForceNew: true,
// TODO: validate that the UUID is valid
},
"primaryip": {
Description: "the primary (public) IP address for the machine",
Type: schema.TypeString,
Computed: true,
},
"nic": {
Description: "network interface",
Type: schema.TypeSet,
Computed: true,
Optional: true,
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return hashcode.String(m["network"].(string))
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"ip": {
Description: "NIC's IPv4 address",
Computed: true,
Type: schema.TypeString,
},
"mac": {
Description: "NIC's MAC address",
Computed: true,
Type: schema.TypeString,
},
"primary": {
Description: "Whether this is the machine's primary NIC",
Computed: true,
Type: schema.TypeBool,
},
"netmask": {
Description: "IPv4 netmask",
Computed: true,
Type: schema.TypeString,
},
"gateway": {
Description: "IPv4 gateway",
Computed: true,
Type: schema.TypeString,
},
"state": {
Description: "describes the state of the NIC (e.g. provisioning, running, or stopped)",
Computed: true,
Type: schema.TypeString,
},
"network": {
Description: "Network ID this NIC is attached to",
Required: true,
Type: schema.TypeString,
},
},
},
},
"firewall_enabled": {
Description: "enable firewall for this machine",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"domain_names": {
Description: "list of domain names from Triton's CNS",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
// computed resources from metadata
"root_authorized_keys": {
Description: "authorized keys for the root user on this machine",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"user_script": {
Description: "user script to run on boot (every boot on SmartMachines)",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"user_data": {
Description: "copied to machine on boot",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"administrator_pw": {
Description: "administrator's initial password (Windows only)",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
// deprecated fields
"networks": {
Description: "desired network IDs",
Type: schema.TypeList,
Optional: true,
Computed: true,
Deprecated: "Networks is deprecated, please use `nic`",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}
func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
var networks []string
for _, network := range d.Get("networks").([]interface{}) {
networks = append(networks, network.(string))
}
nics := d.Get("nic").(*schema.Set)
for _, nicI := range nics.List() {
nic := nicI.(map[string]interface{})
networks = append(networks, nic["network"].(string))
}
metadata := map[string]string{}
for schemaName, metadataKey := range resourceMachineMetadataKeys {
if v, ok := d.GetOk(schemaName); ok {
metadata[metadataKey] = v.(string)
}
}
tags := map[string]string{}
for k, v := range d.Get("tags").(map[string]interface{}) {
tags[k] = v.(string)
}
machine, err := client.CreateMachine(cloudapi.CreateMachineOpts{
Name: d.Get("name").(string),
Package: d.Get("package").(string),
Image: d.Get("image").(string),
Networks: networks,
Metadata: metadata,
Tags: tags,
FirewallEnabled: d.Get("firewall_enabled").(bool),
})
if err != nil {
return err
}
err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout)
if err != nil {
return err
}
// refresh state after it provisions
d.SetId(machine.Id)
err = resourceMachineRead(d, meta)
if err != nil {
return err
}
return nil
}
func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
machine, err := client.GetMachine(d.Id())
return machine != nil && err == nil, err
}
func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
machine, err := client.GetMachine(d.Id())
if err != nil {
return err
}
nics, err := client.ListNICs(d.Id())
if err != nil {
return err
}
d.SetId(machine.Id)
d.Set("name", machine.Name)
d.Set("type", machine.Type)
d.Set("state", machine.State)
d.Set("dataset", machine.Dataset)
d.Set("memory", machine.Memory)
d.Set("disk", machine.Disk)
d.Set("ips", machine.IPs)
d.Set("tags", machine.Tags)
d.Set("created", machine.Created)
d.Set("updated", machine.Updated)
d.Set("package", machine.Package)
d.Set("image", machine.Image)
d.Set("primaryip", machine.PrimaryIP)
d.Set("firewall_enabled", machine.FirewallEnabled)
d.Set("domain_names", machine.DomainNames)
// create and update NICs
var (
machineNICs []map[string]interface{}
networks []string
)
for _, nic := range nics {
machineNICs = append(
machineNICs,
map[string]interface{}{
"ip": nic.IP,
"mac": nic.MAC,
"primary": nic.Primary,
"netmask": nic.Netmask,
"gateway": nic.Gateway,
"state": nic.State,
"network": nic.Network,
},
)
networks = append(networks, nic.Network)
}
d.Set("nic", machineNICs)
d.Set("networks", networks)
// computed attributes from metadata
for schemaName, metadataKey := range resourceMachineMetadataKeys {
d.Set(schemaName, machine.Metadata[metadataKey])
}
return nil
}
func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
d.Partial(true)
if d.HasChange("name") {
if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil {
return err
}
err := waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.Name == d.Get("name").(string), err
},
machineStateChangeCheckInterval,
1*time.Minute,
)
if err != nil {
return err
}
d.SetPartial("name")
}
if d.HasChange("tags") {
tags := map[string]string{}
for k, v := range d.Get("tags").(map[string]interface{}) {
tags[k] = v.(string)
}
var err error
if len(tags) == 0 {
err = client.DeleteMachineTags(d.Id())
} else {
_, err = client.ReplaceMachineTags(d.Id(), tags)
}
if err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return reflect.DeepEqual(machine.Tags, tags), err
},
machineStateChangeCheckInterval,
1*time.Minute,
)
if err != nil {
return err
}
d.SetPartial("tags")
}
if d.HasChange("package") {
if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil {
return err
}
err := waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
if err != nil {
return err
}
d.SetPartial("package")
}
if d.HasChange("firewall_enabled") {
var err error
if d.Get("firewall_enabled").(bool) {
err = client.EnableFirewallMachine(d.Id())
} else {
err = client.DisableFirewallMachine(d.Id())
}
if err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.FirewallEnabled == d.Get("firewall_enabled").(bool), err
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
if err != nil {
return err
}
d.SetPartial("firewall_enabled")
}
if d.HasChange("nic") {
o, n := d.GetChange("nic")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
oldNICs := o.(*schema.Set)
newNICs := o.(*schema.Set)
// add new NICs that are not in old NICs
for _, nicI := range newNICs.Difference(oldNICs).List() {
nic := nicI.(map[string]interface{})
fmt.Printf("adding %+v\n", nic)
_, err := client.AddNIC(d.Id(), nic["network"].(string))
if err != nil {
return err
}
}
// remove old NICs that are not in new NICs
for _, nicI := range oldNICs.Difference(newNICs).List() {
nic := nicI.(map[string]interface{})
fmt.Printf("removing %+v\n", nic)
err := client.RemoveNIC(d.Id(), nic["mac"].(string))
if err != nil {
return err
}
}
d.SetPartial("nic")
}
// metadata stuff
metadata := map[string]string{}
for schemaName, metadataKey := range resourceMachineMetadataKeys {
if d.HasChange(schemaName) {
metadata[metadataKey] = d.Get(schemaName).(string)
}
}
if len(metadata) > 0 {
_, err := client.UpdateMachineMetadata(d.Id(), metadata)
if err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
for k, v := range metadata {
if provider_v, ok := machine.Metadata[k]; !ok || v != provider_v {
return false, err
}
}
return true, err
},
machineStateChangeCheckInterval,
1*time.Minute,
)
if err != nil {
return err
}
for schemaName := range resourceMachineMetadataKeys {
if d.HasChange(schemaName) {
d.SetPartial(schemaName)
}
}
}
d.Partial(false)
err := resourceMachineRead(d, meta)
if err != nil {
return err
}
return nil
}
func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
state, err := readMachineState(client, d.Id())
if state != machineStateStopped {
err = client.StopMachine(d.Id())
if err != nil {
return err
}
waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout)
}
err = client.DeleteMachine(d.Id())
if err != nil {
return err
}
waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout)
return nil
}
func readMachineState(api *cloudapi.Client, id string) (string, error) {
machine, err := api.GetMachine(id)
if err != nil {
return "", err
}
return machine.State, nil
}
// waitForMachineState waits for a machine to be in the desired state (waiting
// some seconds between each poll). If it doesn't reach the state within the
// duration specified in `timeout`, it returns ErrMachineStateTimeout.
func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error {
return waitFor(
func() (bool, error) {
currentState, err := readMachineState(api, id)
return currentState == state, err
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
}
func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) {
warnings = []string{}
errors = []error{}
r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`)
if !r.Match([]byte(value.(string))) {
errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name))
}
return warnings, errors
}
func resourceMachineImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{d}, nil
}