github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 }