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