587 lines
14 KiB
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
|
|
}
|