package triton import ( "fmt" "reflect" "regexp" "time" "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, 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, }, "networks": { Description: "desired network IDs", Type: schema.TypeList, Optional: true, Computed: true, // TODO: this really should ForceNew but the Network IDs don't seem to // be returned by the API, meaning if we track them here TF will replace // the resource on every run. // ForceNew: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, "firewall_enabled": { Description: "enable firewall for this machine", Type: schema.TypeBool, Optional: true, Default: false, }, // 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, }, }, } } 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)) } 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 } 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("networks", machine.Networks) d.Set("firewall_enabled", machine.FirewallEnabled) // 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") } // 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()) return reflect.DeepEqual(machine.Metadata, metadata), 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 }