546 lines
15 KiB
Go
546 lines
15 KiB
Go
package softlayer
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
|
|
"encoding/base64"
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
datatypes "github.com/maximilien/softlayer-go/data_types"
|
|
"github.com/maximilien/softlayer-go/softlayer"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
func resourceSoftLayerVirtualGuest() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: resourceSoftLayerVirtualGuestCreate,
|
|
Read: resourceSoftLayerVirtualGuestRead,
|
|
Update: resourceSoftLayerVirtualGuestUpdate,
|
|
Delete: resourceSoftLayerVirtualGuestDelete,
|
|
Exists: resourceSoftLayerVirtualGuestExists,
|
|
Schema: map[string]*schema.Schema{
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
|
|
"domain": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
|
|
"image": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"hourly_billing": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"private_network_only": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: false,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"region": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"cpu": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
// TODO: This fields for now requires recreation, because currently for some reason SoftLayer resets "dedicated_acct_host_only"
|
|
// TODO: flag to false, while upgrading CPUs. That problem is reported to SoftLayer team. "ForceNew" can be set back
|
|
// TODO: to false as soon as it is fixed at their side. Also corresponding test for virtual guest upgrade will be uncommented.
|
|
ForceNew: true,
|
|
},
|
|
|
|
"ram": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
|
|
memoryInMB := float64(v.(int))
|
|
|
|
// Validate memory to match gigs format
|
|
remaining := math.Mod(memoryInMB, 1024)
|
|
if remaining > 0 {
|
|
suggested := math.Ceil(memoryInMB/1024) * 1024
|
|
errors = append(errors, fmt.Errorf(
|
|
"Invalid 'ram' value %d megabytes, must be a multiple of 1024 (e.g. use %d)", int(memoryInMB), int(suggested)))
|
|
}
|
|
|
|
return
|
|
},
|
|
},
|
|
|
|
"dedicated_acct_host_only": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"frontend_vlan_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"backend_vlan_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"disks": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Schema{Type: schema.TypeInt},
|
|
},
|
|
|
|
"public_network_speed": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 1000,
|
|
},
|
|
|
|
"ipv4_address": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"ipv4_address_private": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"ssh_keys": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Schema{Type: schema.TypeInt},
|
|
},
|
|
|
|
"user_data": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"local_disk": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"post_install_script_uri": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: nil,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"block_device_template_group_gid": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func getNameForBlockDevice(i int) string {
|
|
// skip 1, which is reserved for the swap disk.
|
|
// so we get 0, 2, 3, 4, 5 ...
|
|
if i == 0 {
|
|
return "0"
|
|
} else {
|
|
return strconv.Itoa(i + 1)
|
|
}
|
|
}
|
|
|
|
func getBlockDevices(d *schema.ResourceData) []datatypes.BlockDevice {
|
|
numBlocks := d.Get("disks.#").(int)
|
|
if numBlocks == 0 {
|
|
return nil
|
|
} else {
|
|
blocks := make([]datatypes.BlockDevice, 0, numBlocks)
|
|
for i := 0; i < numBlocks; i++ {
|
|
blockRef := fmt.Sprintf("disks.%d", i)
|
|
name := getNameForBlockDevice(i)
|
|
capacity := d.Get(blockRef).(int)
|
|
block := datatypes.BlockDevice{
|
|
Device: name,
|
|
DiskImage: datatypes.DiskImage{
|
|
Capacity: capacity,
|
|
},
|
|
}
|
|
blocks = append(blocks, block)
|
|
}
|
|
return blocks
|
|
}
|
|
}
|
|
|
|
func resourceSoftLayerVirtualGuestCreate(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*Client).virtualGuestService
|
|
if client == nil {
|
|
return fmt.Errorf("The client was nil.")
|
|
}
|
|
|
|
dc := datatypes.Datacenter{
|
|
Name: d.Get("region").(string),
|
|
}
|
|
|
|
networkComponent := datatypes.NetworkComponents{
|
|
MaxSpeed: d.Get("public_network_speed").(int),
|
|
}
|
|
|
|
privateNetworkOnly := d.Get("private_network_only").(bool)
|
|
opts := datatypes.SoftLayer_Virtual_Guest_Template{
|
|
Hostname: d.Get("name").(string),
|
|
Domain: d.Get("domain").(string),
|
|
HourlyBillingFlag: d.Get("hourly_billing").(bool),
|
|
PrivateNetworkOnlyFlag: privateNetworkOnly,
|
|
Datacenter: dc,
|
|
StartCpus: d.Get("cpu").(int),
|
|
MaxMemory: d.Get("ram").(int),
|
|
NetworkComponents: []datatypes.NetworkComponents{networkComponent},
|
|
BlockDevices: getBlockDevices(d),
|
|
LocalDiskFlag: d.Get("local_disk").(bool),
|
|
PostInstallScriptUri: d.Get("post_install_script_uri").(string),
|
|
}
|
|
|
|
if dedicatedAcctHostOnly, ok := d.GetOk("dedicated_acct_host_only"); ok {
|
|
opts.DedicatedAccountHostOnlyFlag = dedicatedAcctHostOnly.(bool)
|
|
}
|
|
|
|
if globalIdentifier, ok := d.GetOk("block_device_template_group_gid"); ok {
|
|
opts.BlockDeviceTemplateGroup = &datatypes.BlockDeviceTemplateGroup{
|
|
GlobalIdentifier: globalIdentifier.(string),
|
|
}
|
|
}
|
|
|
|
if operatingSystemReferenceCode, ok := d.GetOk("image"); ok {
|
|
opts.OperatingSystemReferenceCode = operatingSystemReferenceCode.(string)
|
|
}
|
|
|
|
// Apply frontend VLAN if provided
|
|
if param, ok := d.GetOk("frontend_vlan_id"); ok {
|
|
frontendVlanId, err := strconv.Atoi(param.(string))
|
|
if err != nil {
|
|
return fmt.Errorf("Not a valid frontend ID, must be an integer: %s", err)
|
|
}
|
|
opts.PrimaryNetworkComponent = &datatypes.PrimaryNetworkComponent{
|
|
NetworkVlan: datatypes.NetworkVlan{Id: (frontendVlanId)},
|
|
}
|
|
}
|
|
|
|
// Apply backend VLAN if provided
|
|
if param, ok := d.GetOk("backend_vlan_id"); ok {
|
|
backendVlanId, err := strconv.Atoi(param.(string))
|
|
if err != nil {
|
|
return fmt.Errorf("Not a valid backend ID, must be an integer: %s", err)
|
|
}
|
|
opts.PrimaryBackendNetworkComponent = &datatypes.PrimaryBackendNetworkComponent{
|
|
NetworkVlan: datatypes.NetworkVlan{Id: (backendVlanId)},
|
|
}
|
|
}
|
|
|
|
if userData, ok := d.GetOk("user_data"); ok {
|
|
opts.UserData = []datatypes.UserData{
|
|
datatypes.UserData{
|
|
Value: userData.(string),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Get configured ssh_keys
|
|
ssh_keys := d.Get("ssh_keys.#").(int)
|
|
if ssh_keys > 0 {
|
|
opts.SshKeys = make([]datatypes.SshKey, 0, ssh_keys)
|
|
for i := 0; i < ssh_keys; i++ {
|
|
key := fmt.Sprintf("ssh_keys.%d", i)
|
|
id := d.Get(key).(int)
|
|
sshKey := datatypes.SshKey{
|
|
Id: id,
|
|
}
|
|
opts.SshKeys = append(opts.SshKeys, sshKey)
|
|
}
|
|
}
|
|
|
|
log.Printf("[INFO] Creating virtual machine")
|
|
|
|
guest, err := client.CreateObject(opts)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating virtual guest: %s", err)
|
|
}
|
|
|
|
d.SetId(fmt.Sprintf("%d", guest.Id))
|
|
|
|
log.Printf("[INFO] Virtual Machine ID: %s", d.Id())
|
|
|
|
// wait for machine availability
|
|
_, err = WaitForNoActiveTransactions(d, meta)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error waiting for virtual machine (%s) to become ready: %s", d.Id(), err)
|
|
}
|
|
|
|
if !privateNetworkOnly {
|
|
_, err = WaitForPublicIpAvailable(d, meta)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error waiting for virtual machine (%s) public ip to become ready: %s", d.Id(), err)
|
|
}
|
|
}
|
|
|
|
return resourceSoftLayerVirtualGuestRead(d, meta)
|
|
}
|
|
|
|
func resourceSoftLayerVirtualGuestRead(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*Client).virtualGuestService
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
|
|
}
|
|
result, err := client.GetObject(id)
|
|
if err != nil {
|
|
return fmt.Errorf("Error retrieving virtual guest: %s", err)
|
|
}
|
|
|
|
d.Set("name", result.Hostname)
|
|
d.Set("domain", result.Domain)
|
|
if result.Datacenter != nil {
|
|
d.Set("region", result.Datacenter.Name)
|
|
}
|
|
d.Set("public_network_speed", result.NetworkComponents[0].MaxSpeed)
|
|
d.Set("cpu", result.StartCpus)
|
|
d.Set("ram", result.MaxMemory)
|
|
d.Set("dedicated_acct_host_only", result.DedicatedAccountHostOnlyFlag)
|
|
d.Set("has_public_ip", result.PrimaryIpAddress != "")
|
|
d.Set("ipv4_address", result.PrimaryIpAddress)
|
|
d.Set("ipv4_address_private", result.PrimaryBackendIpAddress)
|
|
d.Set("private_network_only", result.PrivateNetworkOnlyFlag)
|
|
d.Set("hourly_billing", result.HourlyBillingFlag)
|
|
d.Set("local_disk", result.LocalDiskFlag)
|
|
d.Set("frontend_vlan_id", result.PrimaryNetworkComponent.NetworkVlan.Id)
|
|
d.Set("backend_vlan_id", result.PrimaryBackendNetworkComponent.NetworkVlan.Id)
|
|
|
|
userData := result.UserData
|
|
if userData != nil && len(userData) > 0 {
|
|
data, err := base64.StdEncoding.DecodeString(userData[0].Value)
|
|
if err != nil {
|
|
log.Printf("Can't base64 decode user data %s. error: %s", userData, err)
|
|
d.Set("user_data", userData)
|
|
} else {
|
|
d.Set("user_data", string(data))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceSoftLayerVirtualGuestUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*Client).virtualGuestService
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
|
|
}
|
|
result, err := client.GetObject(id)
|
|
if err != nil {
|
|
return fmt.Errorf("Error retrieving virtual guest: %s", err)
|
|
}
|
|
|
|
// Update "name" and "domain" fields if present and changed
|
|
// Those are the only fields, which could be updated
|
|
if d.HasChange("name") || d.HasChange("domain") {
|
|
result.Hostname = d.Get("name").(string)
|
|
result.Domain = d.Get("domain").(string)
|
|
|
|
_, err = client.EditObject(id, result)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't update virtual guest: %s", err)
|
|
}
|
|
}
|
|
|
|
// Set user data if provided and not empty
|
|
if d.HasChange("user_data") {
|
|
client.SetMetadata(id, d.Get("user_data").(string))
|
|
}
|
|
|
|
// Upgrade "cpu", "ram" and "nic_speed" if provided and changed
|
|
upgradeOptions := softlayer.UpgradeOptions{}
|
|
if d.HasChange("cpu") {
|
|
upgradeOptions.Cpus = d.Get("cpu").(int)
|
|
}
|
|
if d.HasChange("ram") {
|
|
memoryInMB := float64(d.Get("ram").(int))
|
|
|
|
// Convert memory to GB, as softlayer only allows to upgrade RAM in Gigs
|
|
// Must be already validated at this step
|
|
upgradeOptions.MemoryInGB = int(memoryInMB / 1024)
|
|
}
|
|
if d.HasChange("public_network_speed") {
|
|
upgradeOptions.NicSpeed = d.Get("public_network_speed").(int)
|
|
}
|
|
|
|
started, err := client.UpgradeObject(id, &upgradeOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't upgrade virtual guest: %s", err)
|
|
}
|
|
|
|
if started {
|
|
// Wait for softlayer to start upgrading...
|
|
_, err = WaitForUpgradeTransactionsToAppear(d, meta)
|
|
|
|
// Wait for upgrade transactions to finish
|
|
_, err = WaitForNoActiveTransactions(d, meta)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func resourceSoftLayerVirtualGuestDelete(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*Client).virtualGuestService
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
|
|
}
|
|
|
|
_, err = WaitForNoActiveTransactions(d, meta)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Error deleting virtual guest, couldn't wait for zero active transactions: %s", err)
|
|
}
|
|
|
|
_, err = client.DeleteObject(id)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Error deleting virtual guest: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func WaitForUpgradeTransactionsToAppear(d *schema.ResourceData, meta interface{}) (interface{}, error) {
|
|
|
|
log.Printf("Waiting for server (%s) to have upgrade transactions", d.Id())
|
|
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("The instance ID %s must be numeric", d.Id())
|
|
}
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: []string{"pending_upgrade"},
|
|
Target: []string{"upgrade_started"},
|
|
Refresh: func() (interface{}, string, error) {
|
|
client := meta.(*Client).virtualGuestService
|
|
transactions, err := client.GetActiveTransactions(id)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Couldn't fetch active transactions: %s", err)
|
|
}
|
|
for _, transaction := range transactions {
|
|
if strings.Contains(transaction.TransactionStatus.Name, "UPGRADE") {
|
|
return transactions, "upgrade_started", nil
|
|
}
|
|
}
|
|
return transactions, "pending_upgrade", nil
|
|
},
|
|
Timeout: 5 * time.Minute,
|
|
Delay: 5 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
return stateConf.WaitForState()
|
|
}
|
|
|
|
func WaitForPublicIpAvailable(d *schema.ResourceData, meta interface{}) (interface{}, error) {
|
|
log.Printf("Waiting for server (%s) to get a public IP", d.Id())
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: []string{"", "unavailable"},
|
|
Target: []string{"available"},
|
|
Refresh: func() (interface{}, string, error) {
|
|
fmt.Println("Refreshing server state...")
|
|
client := meta.(*Client).virtualGuestService
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Not a valid ID, must be an integer: %s", err)
|
|
}
|
|
result, err := client.GetObject(id)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Error retrieving virtual guest: %s", err)
|
|
}
|
|
if result.PrimaryIpAddress == "" {
|
|
return result, "unavailable", nil
|
|
} else {
|
|
return result, "available", nil
|
|
}
|
|
},
|
|
Timeout: 30 * time.Minute,
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
return stateConf.WaitForState()
|
|
}
|
|
|
|
func WaitForNoActiveTransactions(d *schema.ResourceData, meta interface{}) (interface{}, error) {
|
|
log.Printf("Waiting for server (%s) to have zero active transactions", d.Id())
|
|
id, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("The instance ID %s must be numeric", d.Id())
|
|
}
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: []string{"", "active"},
|
|
Target: []string{"idle"},
|
|
Refresh: func() (interface{}, string, error) {
|
|
client := meta.(*Client).virtualGuestService
|
|
transactions, err := client.GetActiveTransactions(id)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Couldn't get active transactions: %s", err)
|
|
}
|
|
if len(transactions) == 0 {
|
|
return transactions, "idle", nil
|
|
} else {
|
|
return transactions, "active", nil
|
|
}
|
|
},
|
|
Timeout: 10 * time.Minute,
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
return stateConf.WaitForState()
|
|
}
|
|
|
|
func resourceSoftLayerVirtualGuestExists(d *schema.ResourceData, meta interface{}) (bool, error) {
|
|
client := meta.(*Client).virtualGuestService
|
|
|
|
if client == nil {
|
|
return false, fmt.Errorf("The client was nil.")
|
|
}
|
|
|
|
guestId, err := strconv.Atoi(d.Id())
|
|
if err != nil {
|
|
return false, fmt.Errorf("Not a valid ID, must be an integer: %s", err)
|
|
}
|
|
|
|
result, err := client.GetObject(guestId)
|
|
return result.Id == guestId && err == nil, nil
|
|
}
|