github.com/ottenhoff/terraform@v0.7.0-rc1.0.20160607213102-ac2d195cc560/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 171 // computed resources from metadata 172 "root_authorized_keys": { 173 Description: "authorized keys for the root user on this machine", 174 Type: schema.TypeString, 175 Optional: true, 176 Computed: true, 177 }, 178 "user_script": { 179 Description: "user script to run on boot (every boot on SmartMachines)", 180 Type: schema.TypeString, 181 Optional: true, 182 Computed: true, 183 }, 184 "user_data": { 185 Description: "copied to machine on boot", 186 Type: schema.TypeString, 187 Optional: true, 188 Computed: true, 189 }, 190 "administrator_pw": { 191 Description: "administrator's initial password (Windows only)", 192 Type: schema.TypeString, 193 Optional: true, 194 Computed: true, 195 }, 196 197 // deprecated fields 198 "networks": { 199 Description: "desired network IDs", 200 Type: schema.TypeList, 201 Optional: true, 202 Computed: true, 203 Deprecated: "Networks is deprecated, please use `nic`", 204 Elem: &schema.Schema{ 205 Type: schema.TypeString, 206 }, 207 }, 208 }, 209 } 210 } 211 212 func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error { 213 client := meta.(*cloudapi.Client) 214 215 var networks []string 216 for _, network := range d.Get("networks").([]interface{}) { 217 networks = append(networks, network.(string)) 218 } 219 nics := d.Get("nic").(*schema.Set) 220 for _, nicI := range nics.List() { 221 nic := nicI.(map[string]interface{}) 222 networks = append(networks, nic["network"].(string)) 223 } 224 225 metadata := map[string]string{} 226 for schemaName, metadataKey := range resourceMachineMetadataKeys { 227 if v, ok := d.GetOk(schemaName); ok { 228 metadata[metadataKey] = v.(string) 229 } 230 } 231 232 tags := map[string]string{} 233 for k, v := range d.Get("tags").(map[string]interface{}) { 234 tags[k] = v.(string) 235 } 236 237 machine, err := client.CreateMachine(cloudapi.CreateMachineOpts{ 238 Name: d.Get("name").(string), 239 Package: d.Get("package").(string), 240 Image: d.Get("image").(string), 241 Networks: networks, 242 Metadata: metadata, 243 Tags: tags, 244 FirewallEnabled: d.Get("firewall_enabled").(bool), 245 }) 246 if err != nil { 247 return err 248 } 249 250 err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout) 251 if err != nil { 252 return err 253 } 254 255 // refresh state after it provisions 256 d.SetId(machine.Id) 257 err = resourceMachineRead(d, meta) 258 if err != nil { 259 return err 260 } 261 262 return nil 263 } 264 265 func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) { 266 client := meta.(*cloudapi.Client) 267 268 machine, err := client.GetMachine(d.Id()) 269 270 return machine != nil && err == nil, err 271 } 272 273 func resourceMachineRead(d *schema.ResourceData, meta interface{}) error { 274 client := meta.(*cloudapi.Client) 275 276 machine, err := client.GetMachine(d.Id()) 277 if err != nil { 278 return err 279 } 280 281 nics, err := client.ListNICs(d.Id()) 282 if err != nil { 283 return err 284 } 285 286 d.SetId(machine.Id) 287 d.Set("name", machine.Name) 288 d.Set("type", machine.Type) 289 d.Set("state", machine.State) 290 d.Set("dataset", machine.Dataset) 291 d.Set("memory", machine.Memory) 292 d.Set("disk", machine.Disk) 293 d.Set("ips", machine.IPs) 294 d.Set("tags", machine.Tags) 295 d.Set("created", machine.Created) 296 d.Set("updated", machine.Updated) 297 d.Set("package", machine.Package) 298 d.Set("image", machine.Image) 299 d.Set("primaryip", machine.PrimaryIP) 300 d.Set("firewall_enabled", machine.FirewallEnabled) 301 302 // create and update NICs 303 var ( 304 machineNICs []map[string]interface{} 305 networks []string 306 ) 307 for _, nic := range nics { 308 machineNICs = append( 309 machineNICs, 310 map[string]interface{}{ 311 "ip": nic.IP, 312 "mac": nic.MAC, 313 "primary": nic.Primary, 314 "netmask": nic.Netmask, 315 "gateway": nic.Gateway, 316 "state": nic.State, 317 "network": nic.Network, 318 }, 319 ) 320 networks = append(networks, nic.Network) 321 } 322 d.Set("nic", machineNICs) 323 d.Set("networks", networks) 324 325 // computed attributes from metadata 326 for schemaName, metadataKey := range resourceMachineMetadataKeys { 327 d.Set(schemaName, machine.Metadata[metadataKey]) 328 } 329 330 return nil 331 } 332 333 func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error { 334 client := meta.(*cloudapi.Client) 335 336 d.Partial(true) 337 338 if d.HasChange("name") { 339 if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil { 340 return err 341 } 342 343 err := waitFor( 344 func() (bool, error) { 345 machine, err := client.GetMachine(d.Id()) 346 return machine.Name == d.Get("name").(string), err 347 }, 348 machineStateChangeCheckInterval, 349 1*time.Minute, 350 ) 351 if err != nil { 352 return err 353 } 354 355 d.SetPartial("name") 356 } 357 358 if d.HasChange("tags") { 359 tags := map[string]string{} 360 for k, v := range d.Get("tags").(map[string]interface{}) { 361 tags[k] = v.(string) 362 } 363 364 var err error 365 if len(tags) == 0 { 366 err = client.DeleteMachineTags(d.Id()) 367 } else { 368 _, err = client.ReplaceMachineTags(d.Id(), tags) 369 } 370 if err != nil { 371 return err 372 } 373 374 err = waitFor( 375 func() (bool, error) { 376 machine, err := client.GetMachine(d.Id()) 377 return reflect.DeepEqual(machine.Tags, tags), err 378 }, 379 machineStateChangeCheckInterval, 380 1*time.Minute, 381 ) 382 if err != nil { 383 return err 384 } 385 386 d.SetPartial("tags") 387 } 388 389 if d.HasChange("package") { 390 if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil { 391 return err 392 } 393 394 err := waitFor( 395 func() (bool, error) { 396 machine, err := client.GetMachine(d.Id()) 397 return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err 398 }, 399 machineStateChangeCheckInterval, 400 machineStateChangeTimeout, 401 ) 402 if err != nil { 403 return err 404 } 405 406 d.SetPartial("package") 407 } 408 409 if d.HasChange("firewall_enabled") { 410 var err error 411 if d.Get("firewall_enabled").(bool) { 412 err = client.EnableFirewallMachine(d.Id()) 413 } else { 414 err = client.DisableFirewallMachine(d.Id()) 415 } 416 if err != nil { 417 return err 418 } 419 420 err = waitFor( 421 func() (bool, error) { 422 machine, err := client.GetMachine(d.Id()) 423 return machine.FirewallEnabled == d.Get("firewall_enabled").(bool), err 424 }, 425 machineStateChangeCheckInterval, 426 machineStateChangeTimeout, 427 ) 428 429 if err != nil { 430 return err 431 } 432 433 d.SetPartial("firewall_enabled") 434 } 435 436 if d.HasChange("nic") { 437 o, n := d.GetChange("nic") 438 if o == nil { 439 o = new(schema.Set) 440 } 441 if n == nil { 442 n = new(schema.Set) 443 } 444 445 oldNICs := o.(*schema.Set) 446 newNICs := o.(*schema.Set) 447 448 // add new NICs that are not in old NICs 449 for _, nicI := range newNICs.Difference(oldNICs).List() { 450 nic := nicI.(map[string]interface{}) 451 fmt.Printf("adding %+v\n", nic) 452 _, err := client.AddNIC(d.Id(), nic["network"].(string)) 453 if err != nil { 454 return err 455 } 456 } 457 458 // remove old NICs that are not in new NICs 459 for _, nicI := range oldNICs.Difference(newNICs).List() { 460 nic := nicI.(map[string]interface{}) 461 fmt.Printf("removing %+v\n", nic) 462 err := client.RemoveNIC(d.Id(), nic["mac"].(string)) 463 if err != nil { 464 return err 465 } 466 } 467 468 d.SetPartial("nic") 469 } 470 471 // metadata stuff 472 metadata := map[string]string{} 473 for schemaName, metadataKey := range resourceMachineMetadataKeys { 474 if d.HasChange(schemaName) { 475 metadata[metadataKey] = d.Get(schemaName).(string) 476 } 477 } 478 if len(metadata) > 0 { 479 _, err := client.UpdateMachineMetadata(d.Id(), metadata) 480 if err != nil { 481 return err 482 } 483 484 err = waitFor( 485 func() (bool, error) { 486 machine, err := client.GetMachine(d.Id()) 487 for k, v := range metadata { 488 if provider_v, ok := machine.Metadata[k]; !ok || v != provider_v { 489 return false, err 490 } 491 } 492 return true, err 493 }, 494 machineStateChangeCheckInterval, 495 1*time.Minute, 496 ) 497 if err != nil { 498 return err 499 } 500 501 for schemaName := range resourceMachineMetadataKeys { 502 if d.HasChange(schemaName) { 503 d.SetPartial(schemaName) 504 } 505 } 506 } 507 508 d.Partial(false) 509 510 err := resourceMachineRead(d, meta) 511 if err != nil { 512 return err 513 } 514 515 return nil 516 } 517 518 func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error { 519 client := meta.(*cloudapi.Client) 520 521 state, err := readMachineState(client, d.Id()) 522 if state != machineStateStopped { 523 err = client.StopMachine(d.Id()) 524 if err != nil { 525 return err 526 } 527 528 waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout) 529 } 530 531 err = client.DeleteMachine(d.Id()) 532 if err != nil { 533 return err 534 } 535 536 waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout) 537 return nil 538 } 539 540 func readMachineState(api *cloudapi.Client, id string) (string, error) { 541 machine, err := api.GetMachine(id) 542 if err != nil { 543 return "", err 544 } 545 546 return machine.State, nil 547 } 548 549 // waitForMachineState waits for a machine to be in the desired state (waiting 550 // some seconds between each poll). If it doesn't reach the state within the 551 // duration specified in `timeout`, it returns ErrMachineStateTimeout. 552 func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error { 553 return waitFor( 554 func() (bool, error) { 555 currentState, err := readMachineState(api, id) 556 return currentState == state, err 557 }, 558 machineStateChangeCheckInterval, 559 machineStateChangeTimeout, 560 ) 561 } 562 563 func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) { 564 warnings = []string{} 565 errors = []error{} 566 567 r := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\_\.\-]*$`) 568 if !r.Match([]byte(value.(string))) { 569 errors = append(errors, fmt.Errorf(`"%s" is not a valid %s`, value.(string), name)) 570 } 571 572 return warnings, errors 573 } 574 575 func resourceMachineImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 576 return []*schema.ResourceData{d}, nil 577 }