github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/softlayer/resource_softlayer_virtual_guest.go (about)

     1  package softlayer
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strconv"
     7  	"time"
     8  
     9  	"encoding/base64"
    10  	"github.com/hashicorp/terraform/helper/resource"
    11  	"github.com/hashicorp/terraform/helper/schema"
    12  	datatypes "github.com/maximilien/softlayer-go/data_types"
    13  	"github.com/maximilien/softlayer-go/softlayer"
    14  	"math"
    15  	"strings"
    16  )
    17  
    18  func resourceSoftLayerVirtualGuest() *schema.Resource {
    19  	return &schema.Resource{
    20  		Create: resourceSoftLayerVirtualGuestCreate,
    21  		Read:   resourceSoftLayerVirtualGuestRead,
    22  		Update: resourceSoftLayerVirtualGuestUpdate,
    23  		Delete: resourceSoftLayerVirtualGuestDelete,
    24  		Exists: resourceSoftLayerVirtualGuestExists,
    25  		Schema: map[string]*schema.Schema{
    26  			"name": &schema.Schema{
    27  				Type:     schema.TypeString,
    28  				Required: true,
    29  			},
    30  
    31  			"domain": &schema.Schema{
    32  				Type:     schema.TypeString,
    33  				Required: true,
    34  			},
    35  
    36  			"image": &schema.Schema{
    37  				Type:     schema.TypeString,
    38  				Optional: true,
    39  				ForceNew: true,
    40  			},
    41  
    42  			"hourly_billing": &schema.Schema{
    43  				Type:     schema.TypeBool,
    44  				Required: true,
    45  				ForceNew: true,
    46  			},
    47  
    48  			"private_network_only": &schema.Schema{
    49  				Type:     schema.TypeBool,
    50  				Optional: true,
    51  				Default:  false,
    52  				ForceNew: true,
    53  			},
    54  
    55  			"region": &schema.Schema{
    56  				Type:     schema.TypeString,
    57  				Required: true,
    58  				ForceNew: true,
    59  			},
    60  
    61  			"cpu": &schema.Schema{
    62  				Type:     schema.TypeInt,
    63  				Required: true,
    64  				// TODO: This fields for now requires recreation, because currently for some reason SoftLayer resets "dedicated_acct_host_only"
    65  				// TODO: flag to false, while upgrading CPUs. That problem is reported to SoftLayer team. "ForceNew" can be set back
    66  				// TODO: to false as soon as it is fixed at their side. Also corresponding test for virtual guest upgrade will be uncommented.
    67  				ForceNew: true,
    68  			},
    69  
    70  			"ram": &schema.Schema{
    71  				Type:     schema.TypeInt,
    72  				Required: true,
    73  				ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
    74  					memoryInMB := float64(v.(int))
    75  
    76  					// Validate memory to match gigs format
    77  					remaining := math.Mod(memoryInMB, 1024)
    78  					if remaining > 0 {
    79  						suggested := math.Ceil(memoryInMB/1024) * 1024
    80  						errors = append(errors, fmt.Errorf(
    81  							"Invalid 'ram' value %d megabytes, must be a multiple of 1024 (e.g. use %d)", int(memoryInMB), int(suggested)))
    82  					}
    83  
    84  					return
    85  				},
    86  			},
    87  
    88  			"dedicated_acct_host_only": &schema.Schema{
    89  				Type:     schema.TypeBool,
    90  				Optional: true,
    91  				ForceNew: true,
    92  			},
    93  
    94  			"frontend_vlan_id": &schema.Schema{
    95  				Type:     schema.TypeString,
    96  				Optional: true,
    97  				ForceNew: true,
    98  			},
    99  
   100  			"backend_vlan_id": &schema.Schema{
   101  				Type:     schema.TypeString,
   102  				Optional: true,
   103  				ForceNew: true,
   104  			},
   105  
   106  			"disks": &schema.Schema{
   107  				Type:     schema.TypeList,
   108  				Optional: true,
   109  				Elem:     &schema.Schema{Type: schema.TypeInt},
   110  			},
   111  
   112  			"public_network_speed": &schema.Schema{
   113  				Type:     schema.TypeInt,
   114  				Optional: true,
   115  				Default:  1000,
   116  			},
   117  
   118  			"ipv4_address": &schema.Schema{
   119  				Type:     schema.TypeString,
   120  				Computed: true,
   121  			},
   122  
   123  			"ipv4_address_private": &schema.Schema{
   124  				Type:     schema.TypeString,
   125  				Computed: true,
   126  			},
   127  
   128  			"ssh_keys": &schema.Schema{
   129  				Type:     schema.TypeList,
   130  				Optional: true,
   131  				Elem:     &schema.Schema{Type: schema.TypeInt},
   132  			},
   133  
   134  			"user_data": &schema.Schema{
   135  				Type:     schema.TypeString,
   136  				Optional: true,
   137  			},
   138  
   139  			"local_disk": &schema.Schema{
   140  				Type:     schema.TypeBool,
   141  				Required: true,
   142  				ForceNew: true,
   143  			},
   144  
   145  			"post_install_script_uri": &schema.Schema{
   146  				Type:     schema.TypeString,
   147  				Optional: true,
   148  				Default:  nil,
   149  				ForceNew: true,
   150  			},
   151  
   152  			"block_device_template_group_gid": &schema.Schema{
   153  				Type:     schema.TypeString,
   154  				Optional: true,
   155  				ForceNew: true,
   156  			},
   157  		},
   158  	}
   159  }
   160  
   161  func getNameForBlockDevice(i int) string {
   162  	// skip 1, which is reserved for the swap disk.
   163  	// so we get 0, 2, 3, 4, 5 ...
   164  	if i == 0 {
   165  		return "0"
   166  	} else {
   167  		return strconv.Itoa(i + 1)
   168  	}
   169  }
   170  
   171  func getBlockDevices(d *schema.ResourceData) []datatypes.BlockDevice {
   172  	numBlocks := d.Get("disks.#").(int)
   173  	if numBlocks == 0 {
   174  		return nil
   175  	} else {
   176  		blocks := make([]datatypes.BlockDevice, 0, numBlocks)
   177  		for i := 0; i < numBlocks; i++ {
   178  			blockRef := fmt.Sprintf("disks.%d", i)
   179  			name := getNameForBlockDevice(i)
   180  			capacity := d.Get(blockRef).(int)
   181  			block := datatypes.BlockDevice{
   182  				Device: name,
   183  				DiskImage: datatypes.DiskImage{
   184  					Capacity: capacity,
   185  				},
   186  			}
   187  			blocks = append(blocks, block)
   188  		}
   189  		return blocks
   190  	}
   191  }
   192  
   193  func resourceSoftLayerVirtualGuestCreate(d *schema.ResourceData, meta interface{}) error {
   194  	client := meta.(*Client).virtualGuestService
   195  	if client == nil {
   196  		return fmt.Errorf("The client was nil.")
   197  	}
   198  
   199  	dc := datatypes.Datacenter{
   200  		Name: d.Get("region").(string),
   201  	}
   202  
   203  	networkComponent := datatypes.NetworkComponents{
   204  		MaxSpeed: d.Get("public_network_speed").(int),
   205  	}
   206  
   207  	privateNetworkOnly := d.Get("private_network_only").(bool)
   208  	opts := datatypes.SoftLayer_Virtual_Guest_Template{
   209  		Hostname:               d.Get("name").(string),
   210  		Domain:                 d.Get("domain").(string),
   211  		HourlyBillingFlag:      d.Get("hourly_billing").(bool),
   212  		PrivateNetworkOnlyFlag: privateNetworkOnly,
   213  		Datacenter:             dc,
   214  		StartCpus:              d.Get("cpu").(int),
   215  		MaxMemory:              d.Get("ram").(int),
   216  		NetworkComponents:      []datatypes.NetworkComponents{networkComponent},
   217  		BlockDevices:           getBlockDevices(d),
   218  		LocalDiskFlag:          d.Get("local_disk").(bool),
   219  		PostInstallScriptUri:   d.Get("post_install_script_uri").(string),
   220  	}
   221  
   222  	if dedicatedAcctHostOnly, ok := d.GetOk("dedicated_acct_host_only"); ok {
   223  		opts.DedicatedAccountHostOnlyFlag = dedicatedAcctHostOnly.(bool)
   224  	}
   225  
   226  	if globalIdentifier, ok := d.GetOk("block_device_template_group_gid"); ok {
   227  		opts.BlockDeviceTemplateGroup = &datatypes.BlockDeviceTemplateGroup{
   228  			GlobalIdentifier: globalIdentifier.(string),
   229  		}
   230  	}
   231  
   232  	if operatingSystemReferenceCode, ok := d.GetOk("image"); ok {
   233  		opts.OperatingSystemReferenceCode = operatingSystemReferenceCode.(string)
   234  	}
   235  
   236  	// Apply frontend VLAN if provided
   237  	if param, ok := d.GetOk("frontend_vlan_id"); ok {
   238  		frontendVlanId, err := strconv.Atoi(param.(string))
   239  		if err != nil {
   240  			return fmt.Errorf("Not a valid frontend ID, must be an integer: %s", err)
   241  		}
   242  		opts.PrimaryNetworkComponent = &datatypes.PrimaryNetworkComponent{
   243  			NetworkVlan: datatypes.NetworkVlan{Id: (frontendVlanId)},
   244  		}
   245  	}
   246  
   247  	// Apply backend VLAN if provided
   248  	if param, ok := d.GetOk("backend_vlan_id"); ok {
   249  		backendVlanId, err := strconv.Atoi(param.(string))
   250  		if err != nil {
   251  			return fmt.Errorf("Not a valid backend ID, must be an integer: %s", err)
   252  		}
   253  		opts.PrimaryBackendNetworkComponent = &datatypes.PrimaryBackendNetworkComponent{
   254  			NetworkVlan: datatypes.NetworkVlan{Id: (backendVlanId)},
   255  		}
   256  	}
   257  
   258  	if userData, ok := d.GetOk("user_data"); ok {
   259  		opts.UserData = []datatypes.UserData{
   260  			datatypes.UserData{
   261  				Value: userData.(string),
   262  			},
   263  		}
   264  	}
   265  
   266  	// Get configured ssh_keys
   267  	ssh_keys := d.Get("ssh_keys.#").(int)
   268  	if ssh_keys > 0 {
   269  		opts.SshKeys = make([]datatypes.SshKey, 0, ssh_keys)
   270  		for i := 0; i < ssh_keys; i++ {
   271  			key := fmt.Sprintf("ssh_keys.%d", i)
   272  			id := d.Get(key).(int)
   273  			sshKey := datatypes.SshKey{
   274  				Id: id,
   275  			}
   276  			opts.SshKeys = append(opts.SshKeys, sshKey)
   277  		}
   278  	}
   279  
   280  	log.Printf("[INFO] Creating virtual machine")
   281  
   282  	guest, err := client.CreateObject(opts)
   283  
   284  	if err != nil {
   285  		return fmt.Errorf("Error creating virtual guest: %s", err)
   286  	}
   287  
   288  	d.SetId(fmt.Sprintf("%d", guest.Id))
   289  
   290  	log.Printf("[INFO] Virtual Machine ID: %s", d.Id())
   291  
   292  	// wait for machine availability
   293  	_, err = WaitForNoActiveTransactions(d, meta)
   294  
   295  	if err != nil {
   296  		return fmt.Errorf(
   297  			"Error waiting for virtual machine (%s) to become ready: %s", d.Id(), err)
   298  	}
   299  
   300  	if !privateNetworkOnly {
   301  		_, err = WaitForPublicIpAvailable(d, meta)
   302  		if err != nil {
   303  			return fmt.Errorf(
   304  				"Error waiting for virtual machine (%s) public ip to become ready: %s", d.Id(), err)
   305  		}
   306  	}
   307  
   308  	return resourceSoftLayerVirtualGuestRead(d, meta)
   309  }
   310  
   311  func resourceSoftLayerVirtualGuestRead(d *schema.ResourceData, meta interface{}) error {
   312  	client := meta.(*Client).virtualGuestService
   313  	id, err := strconv.Atoi(d.Id())
   314  	if err != nil {
   315  		return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
   316  	}
   317  	result, err := client.GetObject(id)
   318  	if err != nil {
   319  		return fmt.Errorf("Error retrieving virtual guest: %s", err)
   320  	}
   321  
   322  	d.Set("name", result.Hostname)
   323  	d.Set("domain", result.Domain)
   324  	if result.Datacenter != nil {
   325  		d.Set("region", result.Datacenter.Name)
   326  	}
   327  	d.Set("public_network_speed", result.NetworkComponents[0].MaxSpeed)
   328  	d.Set("cpu", result.StartCpus)
   329  	d.Set("ram", result.MaxMemory)
   330  	d.Set("dedicated_acct_host_only", result.DedicatedAccountHostOnlyFlag)
   331  	d.Set("has_public_ip", result.PrimaryIpAddress != "")
   332  	d.Set("ipv4_address", result.PrimaryIpAddress)
   333  	d.Set("ipv4_address_private", result.PrimaryBackendIpAddress)
   334  	d.Set("private_network_only", result.PrivateNetworkOnlyFlag)
   335  	d.Set("hourly_billing", result.HourlyBillingFlag)
   336  	d.Set("local_disk", result.LocalDiskFlag)
   337  	d.Set("frontend_vlan_id", result.PrimaryNetworkComponent.NetworkVlan.Id)
   338  	d.Set("backend_vlan_id", result.PrimaryBackendNetworkComponent.NetworkVlan.Id)
   339  
   340  	userData := result.UserData
   341  	if userData != nil && len(userData) > 0 {
   342  		data, err := base64.StdEncoding.DecodeString(userData[0].Value)
   343  		if err != nil {
   344  			log.Printf("Can't base64 decode user data %s. error: %s", userData, err)
   345  			d.Set("user_data", userData)
   346  		} else {
   347  			d.Set("user_data", string(data))
   348  		}
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  func resourceSoftLayerVirtualGuestUpdate(d *schema.ResourceData, meta interface{}) error {
   355  	client := meta.(*Client).virtualGuestService
   356  	id, err := strconv.Atoi(d.Id())
   357  	if err != nil {
   358  		return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
   359  	}
   360  	result, err := client.GetObject(id)
   361  	if err != nil {
   362  		return fmt.Errorf("Error retrieving virtual guest: %s", err)
   363  	}
   364  
   365  	// Update "name" and "domain" fields if present and changed
   366  	// Those are the only fields, which could be updated
   367  	if d.HasChange("name") || d.HasChange("domain") {
   368  		result.Hostname = d.Get("name").(string)
   369  		result.Domain = d.Get("domain").(string)
   370  
   371  		_, err = client.EditObject(id, result)
   372  
   373  		if err != nil {
   374  			return fmt.Errorf("Couldn't update virtual guest: %s", err)
   375  		}
   376  	}
   377  
   378  	// Set user data if provided and not empty
   379  	if d.HasChange("user_data") {
   380  		client.SetMetadata(id, d.Get("user_data").(string))
   381  	}
   382  
   383  	// Upgrade "cpu", "ram" and "nic_speed" if provided and changed
   384  	upgradeOptions := softlayer.UpgradeOptions{}
   385  	if d.HasChange("cpu") {
   386  		upgradeOptions.Cpus = d.Get("cpu").(int)
   387  	}
   388  	if d.HasChange("ram") {
   389  		memoryInMB := float64(d.Get("ram").(int))
   390  
   391  		// Convert memory to GB, as softlayer only allows to upgrade RAM in Gigs
   392  		// Must be already validated at this step
   393  		upgradeOptions.MemoryInGB = int(memoryInMB / 1024)
   394  	}
   395  	if d.HasChange("public_network_speed") {
   396  		upgradeOptions.NicSpeed = d.Get("public_network_speed").(int)
   397  	}
   398  
   399  	started, err := client.UpgradeObject(id, &upgradeOptions)
   400  	if err != nil {
   401  		return fmt.Errorf("Couldn't upgrade virtual guest: %s", err)
   402  	}
   403  
   404  	if started {
   405  		// Wait for softlayer to start upgrading...
   406  		_, err = WaitForUpgradeTransactionsToAppear(d, meta)
   407  
   408  		// Wait for upgrade transactions to finish
   409  		_, err = WaitForNoActiveTransactions(d, meta)
   410  	}
   411  
   412  	return err
   413  }
   414  
   415  func resourceSoftLayerVirtualGuestDelete(d *schema.ResourceData, meta interface{}) error {
   416  	client := meta.(*Client).virtualGuestService
   417  	id, err := strconv.Atoi(d.Id())
   418  	if err != nil {
   419  		return fmt.Errorf("Not a valid ID, must be an integer: %s", err)
   420  	}
   421  
   422  	_, err = WaitForNoActiveTransactions(d, meta)
   423  
   424  	if err != nil {
   425  		return fmt.Errorf("Error deleting virtual guest, couldn't wait for zero active transactions: %s", err)
   426  	}
   427  
   428  	_, err = client.DeleteObject(id)
   429  
   430  	if err != nil {
   431  		return fmt.Errorf("Error deleting virtual guest: %s", err)
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  func WaitForUpgradeTransactionsToAppear(d *schema.ResourceData, meta interface{}) (interface{}, error) {
   438  
   439  	log.Printf("Waiting for server (%s) to have upgrade transactions", d.Id())
   440  
   441  	id, err := strconv.Atoi(d.Id())
   442  	if err != nil {
   443  		return nil, fmt.Errorf("The instance ID %s must be numeric", d.Id())
   444  	}
   445  
   446  	stateConf := &resource.StateChangeConf{
   447  		Pending: []string{"pending_upgrade"},
   448  		Target:  []string{"upgrade_started"},
   449  		Refresh: func() (interface{}, string, error) {
   450  			client := meta.(*Client).virtualGuestService
   451  			transactions, err := client.GetActiveTransactions(id)
   452  			if err != nil {
   453  				return nil, "", fmt.Errorf("Couldn't fetch active transactions: %s", err)
   454  			}
   455  			for _, transaction := range transactions {
   456  				if strings.Contains(transaction.TransactionStatus.Name, "UPGRADE") {
   457  					return transactions, "upgrade_started", nil
   458  				}
   459  			}
   460  			return transactions, "pending_upgrade", nil
   461  		},
   462  		Timeout:    5 * time.Minute,
   463  		Delay:      5 * time.Second,
   464  		MinTimeout: 3 * time.Second,
   465  	}
   466  
   467  	return stateConf.WaitForState()
   468  }
   469  
   470  func WaitForPublicIpAvailable(d *schema.ResourceData, meta interface{}) (interface{}, error) {
   471  	log.Printf("Waiting for server (%s) to get a public IP", d.Id())
   472  
   473  	stateConf := &resource.StateChangeConf{
   474  		Pending: []string{"", "unavailable"},
   475  		Target:  []string{"available"},
   476  		Refresh: func() (interface{}, string, error) {
   477  			fmt.Println("Refreshing server state...")
   478  			client := meta.(*Client).virtualGuestService
   479  			id, err := strconv.Atoi(d.Id())
   480  			if err != nil {
   481  				return nil, "", fmt.Errorf("Not a valid ID, must be an integer: %s", err)
   482  			}
   483  			result, err := client.GetObject(id)
   484  			if err != nil {
   485  				return nil, "", fmt.Errorf("Error retrieving virtual guest: %s", err)
   486  			}
   487  			if result.PrimaryIpAddress == "" {
   488  				return result, "unavailable", nil
   489  			} else {
   490  				return result, "available", nil
   491  			}
   492  		},
   493  		Timeout:    30 * time.Minute,
   494  		Delay:      10 * time.Second,
   495  		MinTimeout: 3 * time.Second,
   496  	}
   497  
   498  	return stateConf.WaitForState()
   499  }
   500  
   501  func WaitForNoActiveTransactions(d *schema.ResourceData, meta interface{}) (interface{}, error) {
   502  	log.Printf("Waiting for server (%s) to have zero active transactions", d.Id())
   503  	id, err := strconv.Atoi(d.Id())
   504  	if err != nil {
   505  		return nil, fmt.Errorf("The instance ID %s must be numeric", d.Id())
   506  	}
   507  
   508  	stateConf := &resource.StateChangeConf{
   509  		Pending: []string{"", "active"},
   510  		Target:  []string{"idle"},
   511  		Refresh: func() (interface{}, string, error) {
   512  			client := meta.(*Client).virtualGuestService
   513  			transactions, err := client.GetActiveTransactions(id)
   514  			if err != nil {
   515  				return nil, "", fmt.Errorf("Couldn't get active transactions: %s", err)
   516  			}
   517  			if len(transactions) == 0 {
   518  				return transactions, "idle", nil
   519  			} else {
   520  				return transactions, "active", nil
   521  			}
   522  		},
   523  		Timeout:    10 * time.Minute,
   524  		Delay:      10 * time.Second,
   525  		MinTimeout: 3 * time.Second,
   526  	}
   527  
   528  	return stateConf.WaitForState()
   529  }
   530  
   531  func resourceSoftLayerVirtualGuestExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   532  	client := meta.(*Client).virtualGuestService
   533  
   534  	if client == nil {
   535  		return false, fmt.Errorf("The client was nil.")
   536  	}
   537  
   538  	guestId, err := strconv.Atoi(d.Id())
   539  	if err != nil {
   540  		return false, fmt.Errorf("Not a valid ID, must be an integer: %s", err)
   541  	}
   542  
   543  	result, err := client.GetObject(guestId)
   544  	return result.Id == guestId && err == nil, nil
   545  }