github.com/richardbowden/terraform@v0.6.12-0.20160901200758-30ea22c25211/builtin/providers/triton/resource_machine.go (about) 1 package triton 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/hashcode" 10 "github.com/hashicorp/terraform/helper/schema" 11 "github.com/joyent/gosdc/cloudapi" 12 ) 13 14 var ( 15 machineStateRunning = "running" 16 machineStateStopped = "stopped" 17 machineStateDeleted = "deleted" 18 19 machineStateChangeTimeout = 10 * time.Minute 20 machineStateChangeCheckInterval = 10 * time.Second 21 22 resourceMachineMetadataKeys = map[string]string{ 23 // semantics: "schema_name": "metadata_name" 24 "root_authorized_keys": "root_authorized_keys", 25 "user_script": "user-script", 26 "user_data": "user-data", 27 "administrator_pw": "administrator-pw", 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 Importer: &schema.ResourceImporter{ 39 State: resourceMachineImporter, 40 }, 41 42 Schema: map[string]*schema.Schema{ 43 "name": { 44 Description: "friendly name", 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 "state": { 56 Description: "current state of the machine", 57 Type: schema.TypeString, 58 Computed: true, 59 }, 60 "dataset": { 61 Description: "dataset URN the machine was provisioned with", 62 Type: schema.TypeString, 63 Computed: true, 64 }, 65 "memory": { 66 Description: "amount of memory the machine has (in Mb)", 67 Type: schema.TypeInt, 68 Computed: true, 69 }, 70 "disk": { 71 Description: "amount of disk the machine has (in Gb)", 72 Type: schema.TypeInt, 73 Computed: true, 74 }, 75 "ips": { 76 Description: "IP addresses the machine has", 77 Type: schema.TypeList, 78 Computed: true, 79 Elem: &schema.Schema{ 80 Type: schema.TypeString, 81 }, 82 }, 83 "tags": { 84 Description: "machine tags", 85 Type: schema.TypeMap, 86 Optional: true, 87 }, 88 "created": { 89 Description: "when the machine was created", 90 Type: schema.TypeString, 91 Computed: true, 92 }, 93 "updated": { 94 Description: "when the machine was update", 95 Type: schema.TypeString, 96 Computed: true, 97 }, 98 "package": { 99 Description: "name of the package to use on provisioning", 100 Type: schema.TypeString, 101 Required: true, 102 }, 103 "image": { 104 Description: "image UUID", 105 Type: schema.TypeString, 106 Required: true, 107 ForceNew: true, 108 // TODO: validate that the UUID is valid 109 }, 110 "primaryip": { 111 Description: "the primary (public) IP address for the machine", 112 Type: schema.TypeString, 113 Computed: true, 114 }, 115 "nic": { 116 Description: "network interface", 117 Type: schema.TypeSet, 118 Computed: true, 119 Optional: true, 120 Set: func(v interface{}) int { 121 m := v.(map[string]interface{}) 122 return hashcode.String(m["network"].(string)) 123 }, 124 Elem: &schema.Resource{ 125 Schema: map[string]*schema.Schema{ 126 "ip": { 127 Description: "NIC's IPv4 address", 128 Computed: true, 129 Type: schema.TypeString, 130 }, 131 "mac": { 132 Description: "NIC's MAC address", 133 Computed: true, 134 Type: schema.TypeString, 135 }, 136 "primary": { 137 Description: "Whether this is the machine's primary NIC", 138 Computed: true, 139 Type: schema.TypeBool, 140 }, 141 "netmask": { 142 Description: "IPv4 netmask", 143 Computed: true, 144 Type: schema.TypeString, 145 }, 146 "gateway": { 147 Description: "IPv4 gateway", 148 Computed: true, 149 Type: schema.TypeString, 150 }, 151 "state": { 152 Description: "describes the state of the NIC (e.g. provisioning, running, or stopped)", 153 Computed: true, 154 Type: schema.TypeString, 155 }, 156 "network": { 157 Description: "Network ID this NIC is attached to", 158 Required: true, 159 Type: schema.TypeString, 160 }, 161 }, 162 }, 163 }, 164 "firewall_enabled": { 165 Description: "enable firewall for this machine", 166 Type: schema.TypeBool, 167 Optional: true, 168 Default: false, 169 }, 170 "domain_names": { 171 Description: "list of domain names from Triton's CNS", 172 Type: schema.TypeList, 173 Computed: true, 174 Elem: &schema.Schema{ 175 Type: schema.TypeString, 176 }, 177 }, 178 179 // computed resources from metadata 180 "root_authorized_keys": { 181 Description: "authorized keys for the root user on this machine", 182 Type: schema.TypeString, 183 Optional: true, 184 Computed: true, 185 }, 186 "user_script": { 187 Description: "user script to run on boot (every boot on SmartMachines)", 188 Type: schema.TypeString, 189 Optional: true, 190 Computed: true, 191 }, 192 "user_data": { 193 Description: "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.(*cloudapi.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.CreateMachine(cloudapi.CreateMachineOpts{ 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 err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout) 259 if err != nil { 260 return err 261 } 262 263 // refresh state after it provisions 264 d.SetId(machine.Id) 265 err = resourceMachineRead(d, meta) 266 if err != nil { 267 return err 268 } 269 270 return nil 271 } 272 273 func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) { 274 client := meta.(*cloudapi.Client) 275 276 machine, err := client.GetMachine(d.Id()) 277 278 return machine != nil && err == nil, err 279 } 280 281 func resourceMachineRead(d *schema.ResourceData, meta interface{}) error { 282 client := meta.(*cloudapi.Client) 283 284 machine, err := client.GetMachine(d.Id()) 285 if err != nil { 286 return err 287 } 288 289 nics, err := client.ListNICs(d.Id()) 290 if err != nil { 291 return err 292 } 293 294 d.SetId(machine.Id) 295 d.Set("name", machine.Name) 296 d.Set("type", machine.Type) 297 d.Set("state", machine.State) 298 d.Set("dataset", machine.Dataset) 299 d.Set("memory", machine.Memory) 300 d.Set("disk", machine.Disk) 301 d.Set("ips", machine.IPs) 302 d.Set("tags", machine.Tags) 303 d.Set("created", machine.Created) 304 d.Set("updated", machine.Updated) 305 d.Set("package", machine.Package) 306 d.Set("image", machine.Image) 307 d.Set("primaryip", machine.PrimaryIP) 308 d.Set("firewall_enabled", machine.FirewallEnabled) 309 d.Set("domain_names", machine.DomainNames) 310 311 // create and update NICs 312 var ( 313 machineNICs []map[string]interface{} 314 networks []string 315 ) 316 for _, nic := range nics { 317 machineNICs = append( 318 machineNICs, 319 map[string]interface{}{ 320 "ip": nic.IP, 321 "mac": nic.MAC, 322 "primary": nic.Primary, 323 "netmask": nic.Netmask, 324 "gateway": nic.Gateway, 325 "state": nic.State, 326 "network": nic.Network, 327 }, 328 ) 329 networks = append(networks, nic.Network) 330 } 331 d.Set("nic", machineNICs) 332 d.Set("networks", networks) 333 334 // computed attributes from metadata 335 for schemaName, metadataKey := range resourceMachineMetadataKeys { 336 d.Set(schemaName, machine.Metadata[metadataKey]) 337 } 338 339 return nil 340 } 341 342 func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error { 343 client := meta.(*cloudapi.Client) 344 345 d.Partial(true) 346 347 if d.HasChange("name") { 348 if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil { 349 return err 350 } 351 352 err := waitFor( 353 func() (bool, error) { 354 machine, err := client.GetMachine(d.Id()) 355 return machine.Name == d.Get("name").(string), err 356 }, 357 machineStateChangeCheckInterval, 358 1*time.Minute, 359 ) 360 if err != nil { 361 return err 362 } 363 364 d.SetPartial("name") 365 } 366 367 if d.HasChange("tags") { 368 tags := map[string]string{} 369 for k, v := range d.Get("tags").(map[string]interface{}) { 370 tags[k] = v.(string) 371 } 372 373 var err error 374 if len(tags) == 0 { 375 err = client.DeleteMachineTags(d.Id()) 376 } else { 377 _, err = client.ReplaceMachineTags(d.Id(), tags) 378 } 379 if err != nil { 380 return err 381 } 382 383 err = waitFor( 384 func() (bool, error) { 385 machine, err := client.GetMachine(d.Id()) 386 return reflect.DeepEqual(machine.Tags, tags), err 387 }, 388 machineStateChangeCheckInterval, 389 1*time.Minute, 390 ) 391 if err != nil { 392 return err 393 } 394 395 d.SetPartial("tags") 396 } 397 398 if d.HasChange("package") { 399 if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil { 400 return err 401 } 402 403 err := waitFor( 404 func() (bool, error) { 405 machine, err := client.GetMachine(d.Id()) 406 return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err 407 }, 408 machineStateChangeCheckInterval, 409 machineStateChangeTimeout, 410 ) 411 if err != nil { 412 return err 413 } 414 415 d.SetPartial("package") 416 } 417 418 if d.HasChange("firewall_enabled") { 419 var err error 420 if d.Get("firewall_enabled").(bool) { 421 err = client.EnableFirewallMachine(d.Id()) 422 } else { 423 err = client.DisableFirewallMachine(d.Id()) 424 } 425 if err != nil { 426 return err 427 } 428 429 err = waitFor( 430 func() (bool, error) { 431 machine, err := client.GetMachine(d.Id()) 432 return machine.FirewallEnabled == d.Get("firewall_enabled").(bool), err 433 }, 434 machineStateChangeCheckInterval, 435 machineStateChangeTimeout, 436 ) 437 438 if err != nil { 439 return err 440 } 441 442 d.SetPartial("firewall_enabled") 443 } 444 445 if d.HasChange("nic") { 446 o, n := d.GetChange("nic") 447 if o == nil { 448 o = new(schema.Set) 449 } 450 if n == nil { 451 n = new(schema.Set) 452 } 453 454 oldNICs := o.(*schema.Set) 455 newNICs := o.(*schema.Set) 456 457 // add new NICs that are not in old NICs 458 for _, nicI := range newNICs.Difference(oldNICs).List() { 459 nic := nicI.(map[string]interface{}) 460 fmt.Printf("adding %+v\n", nic) 461 _, err := client.AddNIC(d.Id(), nic["network"].(string)) 462 if err != nil { 463 return err 464 } 465 } 466 467 // remove old NICs that are not in new NICs 468 for _, nicI := range oldNICs.Difference(newNICs).List() { 469 nic := nicI.(map[string]interface{}) 470 fmt.Printf("removing %+v\n", nic) 471 err := client.RemoveNIC(d.Id(), nic["mac"].(string)) 472 if err != nil { 473 return err 474 } 475 } 476 477 d.SetPartial("nic") 478 } 479 480 // metadata stuff 481 metadata := map[string]string{} 482 for schemaName, metadataKey := range resourceMachineMetadataKeys { 483 if d.HasChange(schemaName) { 484 metadata[metadataKey] = d.Get(schemaName).(string) 485 } 486 } 487 if len(metadata) > 0 { 488 _, err := client.UpdateMachineMetadata(d.Id(), metadata) 489 if err != nil { 490 return err 491 } 492 493 err = waitFor( 494 func() (bool, error) { 495 machine, err := client.GetMachine(d.Id()) 496 for k, v := range metadata { 497 if provider_v, ok := machine.Metadata[k]; !ok || v != provider_v { 498 return false, err 499 } 500 } 501 return true, err 502 }, 503 machineStateChangeCheckInterval, 504 1*time.Minute, 505 ) 506 if err != nil { 507 return err 508 } 509 510 for schemaName := range resourceMachineMetadataKeys { 511 if d.HasChange(schemaName) { 512 d.SetPartial(schemaName) 513 } 514 } 515 } 516 517 d.Partial(false) 518 519 err := resourceMachineRead(d, meta) 520 if err != nil { 521 return err 522 } 523 524 return nil 525 } 526 527 func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error { 528 client := meta.(*cloudapi.Client) 529 530 state, err := readMachineState(client, d.Id()) 531 if state != machineStateStopped { 532 err = client.StopMachine(d.Id()) 533 if err != nil { 534 return err 535 } 536 537 waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout) 538 } 539 540 err = client.DeleteMachine(d.Id()) 541 if err != nil { 542 return err 543 } 544 545 waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout) 546 return nil 547 } 548 549 func readMachineState(api *cloudapi.Client, id string) (string, error) { 550 machine, err := api.GetMachine(id) 551 if err != nil { 552 return "", err 553 } 554 555 return machine.State, nil 556 } 557 558 // waitForMachineState waits for a machine to be in the desired state (waiting 559 // some seconds between each poll). If it doesn't reach the state within the 560 // duration specified in `timeout`, it returns ErrMachineStateTimeout. 561 func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error { 562 return waitFor( 563 func() (bool, error) { 564 currentState, err := readMachineState(api, id) 565 return currentState == state, err 566 }, 567 machineStateChangeCheckInterval, 568 machineStateChangeTimeout, 569 ) 570 } 571 572 func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) { 573 warnings = []string{} 574 errors = []error{} 575 576 r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`) 577 if !r.Match([]byte(value.(string))) { 578 errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name)) 579 } 580 581 return warnings, errors 582 } 583 584 func resourceMachineImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 585 return []*schema.ResourceData{d}, nil 586 }