github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/triton/resource_machine.go (about) 1 package triton 2 3 import ( 4 "fmt" 5 "regexp" 6 "time" 7 8 "github.com/hashicorp/terraform/helper/hashcode" 9 "github.com/hashicorp/terraform/helper/resource" 10 "github.com/hashicorp/terraform/helper/schema" 11 "github.com/joyent/triton-go" 12 ) 13 14 var ( 15 machineStateRunning = "running" 16 machineStateDeleted = "deleted" 17 18 machineStateChangeTimeout = 10 * time.Minute 19 20 resourceMachineMetadataKeys = map[string]string{ 21 // semantics: "schema_name": "metadata_name" 22 "root_authorized_keys": "root_authorized_keys", 23 "user_script": "user-script", 24 "user_data": "user-data", 25 "administrator_pw": "administrator-pw", 26 "cloud_config": "cloud-init:user-data", 27 } 28 ) 29 30 func resourceMachine() *schema.Resource { 31 return &schema.Resource{ 32 Create: resourceMachineCreate, 33 Exists: resourceMachineExists, 34 Read: resourceMachineRead, 35 Update: resourceMachineUpdate, 36 Delete: resourceMachineDelete, 37 Timeouts: slowResourceTimeout, 38 Importer: &schema.ResourceImporter{ 39 State: schema.ImportStatePassthrough, 40 }, 41 42 Schema: map[string]*schema.Schema{ 43 "name": { 44 Description: "Friendly name for machine", 45 Type: schema.TypeString, 46 Optional: true, 47 Computed: true, 48 ValidateFunc: resourceMachineValidateName, 49 }, 50 "type": { 51 Description: "Machine type (smartmachine or virtualmachine)", 52 Type: schema.TypeString, 53 Computed: true, 54 }, 55 "dataset": { 56 Description: "Dataset URN with which the machine was provisioned", 57 Type: schema.TypeString, 58 Computed: true, 59 }, 60 "memory": { 61 Description: "Amount of memory allocated to the machine (in Mb)", 62 Type: schema.TypeInt, 63 Computed: true, 64 }, 65 "disk": { 66 Description: "Amount of disk allocated to the machine (in Gb)", 67 Type: schema.TypeInt, 68 Computed: true, 69 }, 70 "ips": { 71 Description: "IP addresses assigned to the machine", 72 Type: schema.TypeList, 73 Computed: true, 74 Elem: &schema.Schema{ 75 Type: schema.TypeString, 76 }, 77 }, 78 "tags": { 79 Description: "Machine tags", 80 Type: schema.TypeMap, 81 Optional: true, 82 }, 83 "created": { 84 Description: "When the machine was created", 85 Type: schema.TypeString, 86 Computed: true, 87 }, 88 "updated": { 89 Description: "When the machine was updated", 90 Type: schema.TypeString, 91 Computed: true, 92 }, 93 "package": { 94 Description: "The package for use for provisioning", 95 Type: schema.TypeString, 96 Required: true, 97 }, 98 "image": { 99 Description: "UUID of the image", 100 Type: schema.TypeString, 101 Required: true, 102 ForceNew: true, 103 }, 104 "primaryip": { 105 Description: "Primary (public) IP address for the machine", 106 Type: schema.TypeString, 107 Computed: true, 108 }, 109 "nic": { 110 Description: "Network interface", 111 Type: schema.TypeSet, 112 Computed: true, 113 Optional: true, 114 Set: func(v interface{}) int { 115 m := v.(map[string]interface{}) 116 return hashcode.String(m["network"].(string)) 117 }, 118 Elem: &schema.Resource{ 119 Schema: map[string]*schema.Schema{ 120 "ip": { 121 Description: "NIC's IPv4 address", 122 Computed: true, 123 Type: schema.TypeString, 124 }, 125 "mac": { 126 Description: "NIC's MAC address", 127 Computed: true, 128 Type: schema.TypeString, 129 }, 130 "primary": { 131 Description: "Whether this is the machine's primary NIC", 132 Computed: true, 133 Type: schema.TypeBool, 134 }, 135 "netmask": { 136 Description: "IPv4 netmask", 137 Computed: true, 138 Type: schema.TypeString, 139 }, 140 "gateway": { 141 Description: "IPv4 gateway", 142 Computed: true, 143 Type: schema.TypeString, 144 }, 145 "network": { 146 Description: "ID of the network to which the NIC is attached", 147 Required: true, 148 Type: schema.TypeString, 149 }, 150 "state": { 151 Description: "Provisioning state of the NIC", 152 Computed: true, 153 Type: schema.TypeString, 154 }, 155 }, 156 }, 157 }, 158 "firewall_enabled": { 159 Description: "Whether to enable the firewall for this machine", 160 Type: schema.TypeBool, 161 Optional: true, 162 Default: false, 163 }, 164 "domain_names": { 165 Description: "List of domain names from Triton CNS", 166 Type: schema.TypeList, 167 Computed: true, 168 Elem: &schema.Schema{ 169 Type: schema.TypeString, 170 }, 171 }, 172 173 // computed resources from metadata 174 "root_authorized_keys": { 175 Description: "Authorized keys for the root user on this machine", 176 Type: schema.TypeString, 177 Optional: true, 178 Computed: true, 179 }, 180 "user_script": { 181 Description: "User script to run on boot (every boot on SmartMachines)", 182 Type: schema.TypeString, 183 Optional: true, 184 Computed: true, 185 }, 186 "cloud_config": { 187 Description: "copied to machine on boot", 188 Type: schema.TypeString, 189 Optional: true, 190 Computed: true, 191 }, 192 "user_data": { 193 Description: "Data copied to machine on boot", 194 Type: schema.TypeString, 195 Optional: true, 196 Computed: true, 197 }, 198 "administrator_pw": { 199 Description: "Administrator's initial password (Windows only)", 200 Type: schema.TypeString, 201 Optional: true, 202 Computed: true, 203 }, 204 205 // deprecated fields 206 "networks": { 207 Description: "Desired network IDs", 208 Type: schema.TypeList, 209 Optional: true, 210 Computed: true, 211 Deprecated: "Networks is deprecated, please use `nic`", 212 Elem: &schema.Schema{ 213 Type: schema.TypeString, 214 }, 215 }, 216 }, 217 } 218 } 219 220 func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error { 221 client := meta.(*triton.Client) 222 223 var networks []string 224 for _, network := range d.Get("networks").([]interface{}) { 225 networks = append(networks, network.(string)) 226 } 227 nics := d.Get("nic").(*schema.Set) 228 for _, nicI := range nics.List() { 229 nic := nicI.(map[string]interface{}) 230 networks = append(networks, nic["network"].(string)) 231 } 232 233 metadata := map[string]string{} 234 for schemaName, metadataKey := range resourceMachineMetadataKeys { 235 if v, ok := d.GetOk(schemaName); ok { 236 metadata[metadataKey] = v.(string) 237 } 238 } 239 240 tags := map[string]string{} 241 for k, v := range d.Get("tags").(map[string]interface{}) { 242 tags[k] = v.(string) 243 } 244 245 machine, err := client.Machines().CreateMachine(&triton.CreateMachineInput{ 246 Name: d.Get("name").(string), 247 Package: d.Get("package").(string), 248 Image: d.Get("image").(string), 249 Networks: networks, 250 Metadata: metadata, 251 Tags: tags, 252 FirewallEnabled: d.Get("firewall_enabled").(bool), 253 }) 254 if err != nil { 255 return err 256 } 257 258 d.SetId(machine.ID) 259 stateConf := &resource.StateChangeConf{ 260 Target: []string{fmt.Sprintf(machineStateRunning)}, 261 Refresh: func() (interface{}, string, error) { 262 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 263 ID: d.Id(), 264 }) 265 if err != nil { 266 return nil, "", err 267 } 268 269 return getResp, getResp.State, nil 270 }, 271 Timeout: machineStateChangeTimeout, 272 MinTimeout: 3 * time.Second, 273 } 274 _, err = stateConf.WaitForState() 275 if err != nil { 276 return err 277 } 278 if err != nil { 279 return err 280 } 281 282 // refresh state after it provisions 283 return resourceMachineRead(d, meta) 284 } 285 286 func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) { 287 client := meta.(*triton.Client) 288 289 return resourceExists(client.Machines().GetMachine(&triton.GetMachineInput{ 290 ID: d.Id(), 291 })) 292 } 293 294 func resourceMachineRead(d *schema.ResourceData, meta interface{}) error { 295 client := meta.(*triton.Client) 296 297 machine, err := client.Machines().GetMachine(&triton.GetMachineInput{ 298 ID: d.Id(), 299 }) 300 if err != nil { 301 return err 302 } 303 304 nics, err := client.Machines().ListNICs(&triton.ListNICsInput{ 305 MachineID: d.Id(), 306 }) 307 if err != nil { 308 return err 309 } 310 311 d.Set("name", machine.Name) 312 d.Set("type", machine.Type) 313 d.Set("state", machine.State) 314 d.Set("dataset", machine.Image) 315 d.Set("image", machine.Image) 316 d.Set("memory", machine.Memory) 317 d.Set("disk", machine.Disk) 318 d.Set("ips", machine.IPs) 319 d.Set("tags", machine.Tags) 320 d.Set("created", machine.Created) 321 d.Set("updated", machine.Updated) 322 d.Set("package", machine.Package) 323 d.Set("image", machine.Image) 324 d.Set("primaryip", machine.PrimaryIP) 325 d.Set("firewall_enabled", machine.FirewallEnabled) 326 d.Set("domain_names", machine.DomainNames) 327 328 // create and update NICs 329 var ( 330 machineNICs []map[string]interface{} 331 networks []string 332 ) 333 for _, nic := range nics { 334 machineNICs = append( 335 machineNICs, 336 map[string]interface{}{ 337 "ip": nic.IP, 338 "mac": nic.MAC, 339 "primary": nic.Primary, 340 "netmask": nic.Netmask, 341 "gateway": nic.Gateway, 342 "state": nic.State, 343 "network": nic.Network, 344 }, 345 ) 346 networks = append(networks, nic.Network) 347 } 348 d.Set("nic", machineNICs) 349 d.Set("networks", networks) 350 351 // computed attributes from metadata 352 for schemaName, metadataKey := range resourceMachineMetadataKeys { 353 d.Set(schemaName, machine.Metadata[metadataKey]) 354 } 355 356 return nil 357 } 358 359 func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error { 360 client := meta.(*triton.Client) 361 362 d.Partial(true) 363 364 if d.HasChange("name") { 365 oldNameInterface, newNameInterface := d.GetChange("name") 366 oldName := oldNameInterface.(string) 367 newName := newNameInterface.(string) 368 369 err := client.Machines().RenameMachine(&triton.RenameMachineInput{ 370 ID: d.Id(), 371 Name: newName, 372 }) 373 if err != nil { 374 return err 375 } 376 377 stateConf := &resource.StateChangeConf{ 378 Pending: []string{oldName}, 379 Target: []string{newName}, 380 Refresh: func() (interface{}, string, error) { 381 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 382 ID: d.Id(), 383 }) 384 if err != nil { 385 return nil, "", err 386 } 387 388 return getResp, getResp.Name, nil 389 }, 390 Timeout: machineStateChangeTimeout, 391 MinTimeout: 3 * time.Second, 392 } 393 _, err = stateConf.WaitForState() 394 if err != nil { 395 return err 396 } 397 398 d.SetPartial("name") 399 } 400 401 if d.HasChange("tags") { 402 tags := map[string]string{} 403 for k, v := range d.Get("tags").(map[string]interface{}) { 404 tags[k] = v.(string) 405 } 406 407 var err error 408 if len(tags) == 0 { 409 err = client.Machines().DeleteMachineTags(&triton.DeleteMachineTagsInput{ 410 ID: d.Id(), 411 }) 412 } else { 413 err = client.Machines().ReplaceMachineTags(&triton.ReplaceMachineTagsInput{ 414 ID: d.Id(), 415 Tags: tags, 416 }) 417 } 418 if err != nil { 419 return err 420 } 421 422 expectedTagsMD5 := stableMapHash(tags) 423 stateConf := &resource.StateChangeConf{ 424 Target: []string{expectedTagsMD5}, 425 Refresh: func() (interface{}, string, error) { 426 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 427 ID: d.Id(), 428 }) 429 if err != nil { 430 return nil, "", err 431 } 432 433 return getResp, stableMapHash(getResp.Tags), nil 434 }, 435 Timeout: machineStateChangeTimeout, 436 MinTimeout: 3 * time.Second, 437 } 438 _, err = stateConf.WaitForState() 439 if err != nil { 440 return err 441 } 442 443 d.SetPartial("tags") 444 } 445 446 if d.HasChange("package") { 447 newPackage := d.Get("package").(string) 448 449 err := client.Machines().ResizeMachine(&triton.ResizeMachineInput{ 450 ID: d.Id(), 451 Package: newPackage, 452 }) 453 if err != nil { 454 return err 455 } 456 457 stateConf := &resource.StateChangeConf{ 458 Target: []string{fmt.Sprintf("%s@%s", newPackage, "running")}, 459 Refresh: func() (interface{}, string, error) { 460 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 461 ID: d.Id(), 462 }) 463 if err != nil { 464 return nil, "", err 465 } 466 467 return getResp, fmt.Sprintf("%s@%s", getResp.Package, getResp.State), nil 468 }, 469 Timeout: machineStateChangeTimeout, 470 MinTimeout: 3 * time.Second, 471 } 472 _, err = stateConf.WaitForState() 473 if err != nil { 474 return err 475 } 476 477 d.SetPartial("package") 478 } 479 480 if d.HasChange("firewall_enabled") { 481 enable := d.Get("firewall_enabled").(bool) 482 483 var err error 484 if enable { 485 err = client.Machines().EnableMachineFirewall(&triton.EnableMachineFirewallInput{ 486 ID: d.Id(), 487 }) 488 } else { 489 err = client.Machines().DisableMachineFirewall(&triton.DisableMachineFirewallInput{ 490 ID: d.Id(), 491 }) 492 } 493 if err != nil { 494 return err 495 } 496 497 stateConf := &resource.StateChangeConf{ 498 Target: []string{fmt.Sprintf("%t", enable)}, 499 Refresh: func() (interface{}, string, error) { 500 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 501 ID: d.Id(), 502 }) 503 if err != nil { 504 return nil, "", err 505 } 506 507 return getResp, fmt.Sprintf("%t", getResp.FirewallEnabled), nil 508 }, 509 Timeout: machineStateChangeTimeout, 510 MinTimeout: 3 * time.Second, 511 } 512 _, err = stateConf.WaitForState() 513 if err != nil { 514 return err 515 } 516 517 d.SetPartial("firewall_enabled") 518 } 519 520 if d.HasChange("nic") { 521 o, n := d.GetChange("nic") 522 if o == nil { 523 o = new(schema.Set) 524 } 525 if n == nil { 526 n = new(schema.Set) 527 } 528 529 oldNICs := o.(*schema.Set) 530 newNICs := n.(*schema.Set) 531 532 for _, nicI := range newNICs.Difference(oldNICs).List() { 533 nic := nicI.(map[string]interface{}) 534 if _, err := client.Machines().AddNIC(&triton.AddNICInput{ 535 MachineID: d.Id(), 536 Network: nic["network"].(string), 537 }); err != nil { 538 return err 539 } 540 } 541 542 for _, nicI := range oldNICs.Difference(newNICs).List() { 543 nic := nicI.(map[string]interface{}) 544 if err := client.Machines().RemoveNIC(&triton.RemoveNICInput{ 545 MachineID: d.Id(), 546 MAC: nic["mac"].(string), 547 }); err != nil { 548 return err 549 } 550 } 551 552 d.SetPartial("nic") 553 } 554 555 metadata := map[string]string{} 556 for schemaName, metadataKey := range resourceMachineMetadataKeys { 557 if d.HasChange(schemaName) { 558 metadata[metadataKey] = d.Get(schemaName).(string) 559 } 560 } 561 if len(metadata) > 0 { 562 if _, err := client.Machines().UpdateMachineMetadata(&triton.UpdateMachineMetadataInput{ 563 ID: d.Id(), 564 Metadata: metadata, 565 }); err != nil { 566 return err 567 } 568 569 stateConf := &resource.StateChangeConf{ 570 Target: []string{"converged"}, 571 Refresh: func() (interface{}, string, error) { 572 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 573 ID: d.Id(), 574 }) 575 if err != nil { 576 return nil, "", err 577 } 578 579 for k, v := range metadata { 580 if upstream, ok := getResp.Metadata[k]; !ok || v != upstream { 581 return getResp, "converging", nil 582 } 583 } 584 585 return getResp, "converged", nil 586 }, 587 Timeout: machineStateChangeTimeout, 588 MinTimeout: 3 * time.Second, 589 } 590 _, err := stateConf.WaitForState() 591 if err != nil { 592 return err 593 } 594 595 for schemaName := range resourceMachineMetadataKeys { 596 if d.HasChange(schemaName) { 597 d.SetPartial(schemaName) 598 } 599 } 600 } 601 602 d.Partial(false) 603 604 return resourceMachineRead(d, meta) 605 } 606 607 func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error { 608 client := meta.(*triton.Client) 609 610 err := client.Machines().DeleteMachine(&triton.DeleteMachineInput{ 611 ID: d.Id(), 612 }) 613 if err != nil { 614 return err 615 } 616 617 stateConf := &resource.StateChangeConf{ 618 Target: []string{machineStateDeleted}, 619 Refresh: func() (interface{}, string, error) { 620 getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{ 621 ID: d.Id(), 622 }) 623 if err != nil { 624 if triton.IsResourceNotFound(err) { 625 return nil, "deleted", nil 626 } 627 return nil, "", err 628 } 629 630 return getResp, getResp.State, nil 631 }, 632 Timeout: machineStateChangeTimeout, 633 MinTimeout: 3 * time.Second, 634 } 635 _, err = stateConf.WaitForState() 636 if err != nil { 637 return err 638 } 639 640 return nil 641 } 642 643 func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) { 644 warnings = []string{} 645 errors = []error{} 646 647 r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`) 648 if !r.Match([]byte(value.(string))) { 649 errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name)) 650 } 651 652 return warnings, errors 653 }