terraform/builtin/providers/softlayer/resource_softlayer_virtual_...

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
}