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