github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/builtin/providers/alicloud/resource_alicloud_instance.go (about) 1 package alicloud 2 3 import ( 4 "fmt" 5 "log" 6 7 "encoding/base64" 8 "encoding/json" 9 "github.com/denverdino/aliyungo/common" 10 "github.com/denverdino/aliyungo/ecs" 11 "github.com/hashicorp/terraform/helper/resource" 12 "github.com/hashicorp/terraform/helper/schema" 13 "strings" 14 "time" 15 ) 16 17 func resourceAliyunInstance() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAliyunInstanceCreate, 20 Read: resourceAliyunInstanceRead, 21 Update: resourceAliyunInstanceUpdate, 22 Delete: resourceAliyunInstanceDelete, 23 24 Schema: map[string]*schema.Schema{ 25 "availability_zone": &schema.Schema{ 26 Type: schema.TypeString, 27 Optional: true, 28 ForceNew: true, 29 Computed: true, 30 }, 31 32 "image_id": &schema.Schema{ 33 Type: schema.TypeString, 34 Required: true, 35 }, 36 37 "instance_type": &schema.Schema{ 38 Type: schema.TypeString, 39 Required: true, 40 }, 41 42 "security_groups": &schema.Schema{ 43 Type: schema.TypeSet, 44 Elem: &schema.Schema{Type: schema.TypeString}, 45 Required: true, 46 }, 47 48 "allocate_public_ip": &schema.Schema{ 49 Type: schema.TypeBool, 50 Optional: true, 51 Default: false, 52 }, 53 54 "instance_name": &schema.Schema{ 55 Type: schema.TypeString, 56 Optional: true, 57 Default: "ECS-Instance", 58 ValidateFunc: validateInstanceName, 59 }, 60 61 "description": &schema.Schema{ 62 Type: schema.TypeString, 63 Optional: true, 64 ValidateFunc: validateInstanceDescription, 65 }, 66 67 "internet_charge_type": &schema.Schema{ 68 Type: schema.TypeString, 69 Optional: true, 70 ForceNew: true, 71 ValidateFunc: validateInternetChargeType, 72 }, 73 "internet_max_bandwidth_in": &schema.Schema{ 74 Type: schema.TypeString, 75 Optional: true, 76 ForceNew: true, 77 }, 78 "internet_max_bandwidth_out": &schema.Schema{ 79 Type: schema.TypeInt, 80 Optional: true, 81 ForceNew: true, 82 ValidateFunc: validateInternetMaxBandWidthOut, 83 }, 84 "host_name": &schema.Schema{ 85 Type: schema.TypeString, 86 Optional: true, 87 Computed: true, 88 }, 89 "password": &schema.Schema{ 90 Type: schema.TypeString, 91 Optional: true, 92 Sensitive: true, 93 }, 94 "io_optimized": &schema.Schema{ 95 Type: schema.TypeString, 96 Required: true, 97 ForceNew: true, 98 ValidateFunc: validateIoOptimized, 99 }, 100 101 "system_disk_category": &schema.Schema{ 102 Type: schema.TypeString, 103 Default: "cloud", 104 Optional: true, 105 ForceNew: true, 106 ValidateFunc: validateAllowedStringValue([]string{ 107 string(ecs.DiskCategoryCloud), 108 string(ecs.DiskCategoryCloudSSD), 109 string(ecs.DiskCategoryCloudEfficiency), 110 string(ecs.DiskCategoryEphemeralSSD), 111 }), 112 }, 113 "system_disk_size": &schema.Schema{ 114 Type: schema.TypeInt, 115 Optional: true, 116 Computed: true, 117 ValidateFunc: validateIntegerInRange(40, 500), 118 }, 119 120 //subnet_id and vswitch_id both exists, cause compatible old version, and aws habit. 121 "subnet_id": &schema.Schema{ 122 Type: schema.TypeString, 123 Optional: true, 124 ForceNew: true, 125 Computed: true, //add this schema cause subnet_id not used enter parameter, will different, so will be ForceNew 126 }, 127 128 "vswitch_id": &schema.Schema{ 129 Type: schema.TypeString, 130 Optional: true, 131 ForceNew: true, 132 }, 133 134 "instance_charge_type": &schema.Schema{ 135 Type: schema.TypeString, 136 Optional: true, 137 ForceNew: true, 138 ValidateFunc: validateInstanceChargeType, 139 }, 140 "period": &schema.Schema{ 141 Type: schema.TypeInt, 142 Optional: true, 143 ForceNew: true, 144 }, 145 146 "public_ip": &schema.Schema{ 147 Type: schema.TypeString, 148 Optional: true, 149 Computed: true, 150 }, 151 152 "private_ip": &schema.Schema{ 153 Type: schema.TypeString, 154 Computed: true, 155 }, 156 157 "status": &schema.Schema{ 158 Type: schema.TypeString, 159 Computed: true, 160 }, 161 162 "user_data": &schema.Schema{ 163 Type: schema.TypeString, 164 Optional: true, 165 ForceNew: true, 166 }, 167 168 "tags": tagsSchema(), 169 }, 170 } 171 } 172 173 func resourceAliyunInstanceCreate(d *schema.ResourceData, meta interface{}) error { 174 conn := meta.(*AliyunClient).ecsconn 175 176 // create postpaid instance by runInstances API 177 if v := d.Get("instance_charge_type").(string); v != string(common.PrePaid) { 178 return resourceAliyunRunInstance(d, meta) 179 } 180 181 args, err := buildAliyunInstanceArgs(d, meta) 182 if err != nil { 183 return err 184 } 185 186 instanceID, err := conn.CreateInstance(args) 187 if err != nil { 188 return fmt.Errorf("Error creating Aliyun ecs instance: %#v", err) 189 } 190 191 d.SetId(instanceID) 192 193 d.Set("password", d.Get("password")) 194 195 // after instance created, its status is pending, 196 // so we need to wait it become to stopped and then start it 197 if err := conn.WaitForInstanceAsyn(d.Id(), ecs.Stopped, defaultTimeout); err != nil { 198 return fmt.Errorf("[DEBUG] WaitForInstance %s got error: %#v", ecs.Stopped, err) 199 } 200 201 if err := allocateIpAndBandWidthRelative(d, meta); err != nil { 202 return fmt.Errorf("allocateIpAndBandWidthRelative err: %#v", err) 203 } 204 205 if err := conn.StartInstance(d.Id()); err != nil { 206 return fmt.Errorf("Start instance got error: %#v", err) 207 } 208 209 if err := conn.WaitForInstanceAsyn(d.Id(), ecs.Running, defaultTimeout); err != nil { 210 return fmt.Errorf("[DEBUG] WaitForInstance %s got error: %#v", ecs.Running, err) 211 } 212 213 return resourceAliyunInstanceUpdate(d, meta) 214 } 215 216 func resourceAliyunRunInstance(d *schema.ResourceData, meta interface{}) error { 217 conn := meta.(*AliyunClient).ecsconn 218 newConn := meta.(*AliyunClient).ecsNewconn 219 220 args, err := buildAliyunInstanceArgs(d, meta) 221 if err != nil { 222 return err 223 } 224 225 if args.IoOptimized == "optimized" { 226 args.IoOptimized = ecs.IoOptimized("true") 227 } else { 228 args.IoOptimized = ecs.IoOptimized("false") 229 } 230 231 runArgs, err := buildAliyunRunInstancesArgs(d, meta) 232 if err != nil { 233 return err 234 } 235 236 runArgs.CreateInstanceArgs = *args 237 238 // runInstances is support in version 2016-03-14 239 instanceIds, err := newConn.RunInstances(runArgs) 240 241 if err != nil { 242 return fmt.Errorf("Error creating Aliyun ecs instance: %#v", err) 243 } 244 245 d.SetId(instanceIds[0]) 246 247 d.Set("password", d.Get("password")) 248 d.Set("system_disk_category", d.Get("system_disk_category")) 249 d.Set("system_disk_size", d.Get("system_disk_size")) 250 251 // after instance created, its status change from pending, starting to running 252 if err := conn.WaitForInstanceAsyn(d.Id(), ecs.Running, defaultTimeout); err != nil { 253 return fmt.Errorf("[DEBUG] WaitForInstance %s got error: %#v", ecs.Running, err) 254 } 255 256 if err := allocateIpAndBandWidthRelative(d, meta); err != nil { 257 return fmt.Errorf("allocateIpAndBandWidthRelative err: %#v", err) 258 } 259 260 if err := conn.WaitForInstanceAsyn(d.Id(), ecs.Running, defaultTimeout); err != nil { 261 return fmt.Errorf("[DEBUG] WaitForInstance %s got error: %#v", ecs.Running, err) 262 } 263 264 return resourceAliyunInstanceUpdate(d, meta) 265 } 266 267 func resourceAliyunInstanceRead(d *schema.ResourceData, meta interface{}) error { 268 client := meta.(*AliyunClient) 269 conn := client.ecsconn 270 271 instance, err := client.QueryInstancesById(d.Id()) 272 273 if err != nil { 274 if notFoundError(err) { 275 d.SetId("") 276 return nil 277 } 278 return fmt.Errorf("Error DescribeInstanceAttribute: %#v", err) 279 } 280 281 disk, diskErr := client.QueryInstanceSystemDisk(d.Id()) 282 283 if diskErr != nil { 284 if notFoundError(diskErr) { 285 d.SetId("") 286 return nil 287 } 288 return fmt.Errorf("Error DescribeSystemDisk: %#v", err) 289 } 290 291 d.Set("instance_name", instance.InstanceName) 292 d.Set("description", instance.Description) 293 d.Set("status", instance.Status) 294 d.Set("availability_zone", instance.ZoneId) 295 d.Set("host_name", instance.HostName) 296 d.Set("image_id", instance.ImageId) 297 d.Set("instance_type", instance.InstanceType) 298 d.Set("system_disk_category", disk.Category) 299 d.Set("system_disk_size", disk.Size) 300 301 // In Classic network, internet_charge_type is valid in any case, and its default value is 'PayByBanwidth'. 302 // In VPC network, internet_charge_type is valid when instance has public ip, and its default value is 'PayByBanwidth'. 303 d.Set("internet_charge_type", instance.InternetChargeType) 304 305 if d.Get("allocate_public_ip").(bool) { 306 d.Set("public_ip", instance.PublicIpAddress.IpAddress[0]) 307 } 308 309 if ecs.StringOrBool(instance.IoOptimized).Value { 310 d.Set("io_optimized", "optimized") 311 } else { 312 d.Set("io_optimized", "none") 313 } 314 315 if d.Get("subnet_id").(string) != "" || d.Get("vswitch_id").(string) != "" { 316 ipAddress := instance.VpcAttributes.PrivateIpAddress.IpAddress[0] 317 d.Set("private_ip", ipAddress) 318 d.Set("subnet_id", instance.VpcAttributes.VSwitchId) 319 d.Set("vswitch_id", instance.VpcAttributes.VSwitchId) 320 } else { 321 ipAddress := strings.Join(ecs.IpAddressSetType(instance.InnerIpAddress).IpAddress, ",") 322 d.Set("private_ip", ipAddress) 323 } 324 325 if d.Get("user_data").(string) != "" { 326 ud, err := conn.DescribeUserdata(&ecs.DescribeUserdataArgs{ 327 RegionId: getRegion(d, meta), 328 InstanceId: d.Id(), 329 }) 330 331 if err != nil { 332 log.Printf("[ERROR] DescribeUserData for instance got error: %#v", err) 333 } 334 d.Set("user_data", userDataHashSum(ud.UserData)) 335 } 336 337 tags, _, err := conn.DescribeTags(&ecs.DescribeTagsArgs{ 338 RegionId: getRegion(d, meta), 339 ResourceType: ecs.TagResourceInstance, 340 ResourceId: d.Id(), 341 }) 342 343 if err != nil { 344 log.Printf("[ERROR] DescribeTags for instance got error: %#v", err) 345 } 346 d.Set("tags", tagsToMap(tags)) 347 348 return nil 349 } 350 351 func resourceAliyunInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 352 353 client := meta.(*AliyunClient) 354 conn := client.ecsconn 355 356 d.Partial(true) 357 358 if err := setTags(client, ecs.TagResourceInstance, d); err != nil { 359 log.Printf("[DEBUG] Set tags for instance got error: %#v", err) 360 return fmt.Errorf("Set tags for instance got error: %#v", err) 361 } else { 362 d.SetPartial("tags") 363 } 364 365 imageUpdate := false 366 if d.HasChange("image_id") && !d.IsNewResource() { 367 log.Printf("[DEBUG] Replace instance system disk via changing image_id") 368 replaceSystemArgs := &ecs.ReplaceSystemDiskArgs{ 369 InstanceId: d.Id(), 370 ImageId: d.Get("image_id").(string), 371 SystemDisk: ecs.SystemDiskType{ 372 Size: d.Get("system_disk_size").(int), 373 }, 374 } 375 if v, ok := d.GetOk("status"); ok && v.(string) != "" { 376 if ecs.InstanceStatus(d.Get("status").(string)) == ecs.Running { 377 log.Printf("[DEBUG] StopInstance before change system disk") 378 if err := conn.StopInstance(d.Id(), true); err != nil { 379 return fmt.Errorf("Force Stop Instance got an error: %#v", err) 380 } 381 if err := conn.WaitForInstance(d.Id(), ecs.Stopped, 60); err != nil { 382 return fmt.Errorf("WaitForInstance got error: %#v", err) 383 } 384 } 385 } 386 _, err := conn.ReplaceSystemDisk(replaceSystemArgs) 387 if err != nil { 388 return fmt.Errorf("Replace system disk got an error: %#v", err) 389 } 390 // Ensure instance's image has been replaced successfully. 391 timeout := ecs.InstanceDefaultTimeout 392 for { 393 instance, errDesc := conn.DescribeInstanceAttribute(d.Id()) 394 if errDesc != nil { 395 return fmt.Errorf("Describe instance got an error: %#v", errDesc) 396 } 397 if instance.ImageId == d.Get("image_id") { 398 break 399 } 400 time.Sleep(ecs.DefaultWaitForInterval * time.Second) 401 timeout = timeout - ecs.DefaultWaitForInterval 402 if timeout <= 0 { 403 return common.GetClientErrorFromString("Timeout") 404 } 405 } 406 imageUpdate = true 407 d.SetPartial("system_disk_size") 408 d.SetPartial("image_id") 409 } 410 // Provider doesn't support change 'system_disk_size'separately. 411 if d.HasChange("system_disk_size") && !d.HasChange("image_id") { 412 return fmt.Errorf("Update resource failed. 'system_disk_size' isn't allowed to change separately. You can update it via renewing instance or replacing system disk.") 413 } 414 415 attributeUpdate := false 416 args := &ecs.ModifyInstanceAttributeArgs{ 417 InstanceId: d.Id(), 418 } 419 420 if d.HasChange("instance_name") && !d.IsNewResource() { 421 log.Printf("[DEBUG] ModifyInstanceAttribute instance_name") 422 d.SetPartial("instance_name") 423 args.InstanceName = d.Get("instance_name").(string) 424 425 attributeUpdate = true 426 } 427 428 if d.HasChange("description") && !d.IsNewResource() { 429 log.Printf("[DEBUG] ModifyInstanceAttribute description") 430 d.SetPartial("description") 431 args.Description = d.Get("description").(string) 432 433 attributeUpdate = true 434 } 435 436 if d.HasChange("host_name") && !d.IsNewResource() { 437 log.Printf("[DEBUG] ModifyInstanceAttribute host_name") 438 d.SetPartial("host_name") 439 args.HostName = d.Get("host_name").(string) 440 441 attributeUpdate = true 442 } 443 444 passwordUpdate := false 445 if d.HasChange("password") && !d.IsNewResource() { 446 log.Printf("[DEBUG] ModifyInstanceAttribute password") 447 d.SetPartial("password") 448 args.Password = d.Get("password").(string) 449 450 attributeUpdate = true 451 passwordUpdate = true 452 } 453 454 if attributeUpdate { 455 if err := conn.ModifyInstanceAttribute(args); err != nil { 456 return fmt.Errorf("Modify instance attribute got error: %#v", err) 457 } 458 } 459 460 if imageUpdate || passwordUpdate { 461 instance, errDesc := conn.DescribeInstanceAttribute(d.Id()) 462 if errDesc != nil { 463 return fmt.Errorf("Describe instance got an error: %#v", errDesc) 464 } 465 if instance.Status != ecs.Running && instance.Status != ecs.Stopped { 466 return fmt.Errorf("ECS instance's status doesn't support to start or reboot operation after replace image_id or update password. The current instance's status is %#v", instance.Status) 467 } else if instance.Status == ecs.Running { 468 log.Printf("[DEBUG] Reboot instance after change image or password") 469 if err := conn.RebootInstance(d.Id(), false); err != nil { 470 return fmt.Errorf("RebootInstance got error: %#v", err) 471 } 472 } else { 473 log.Printf("[DEBUG] Start instance after change image or password") 474 if err := conn.StartInstance(d.Id()); err != nil { 475 return fmt.Errorf("StartInstance got error: %#v", err) 476 } 477 } 478 // Start instance sometimes costs more than 6 minutes when os type is centos. 479 if err := conn.WaitForInstance(d.Id(), ecs.Running, 400); err != nil { 480 return fmt.Errorf("WaitForInstance got error: %#v", err) 481 } 482 } 483 484 if d.HasChange("security_groups") { 485 o, n := d.GetChange("security_groups") 486 os := o.(*schema.Set) 487 ns := n.(*schema.Set) 488 489 rl := expandStringList(os.Difference(ns).List()) 490 al := expandStringList(ns.Difference(os).List()) 491 492 if len(al) > 0 { 493 err := client.JoinSecurityGroups(d.Id(), al) 494 if err != nil { 495 return err 496 } 497 } 498 if len(rl) > 0 { 499 err := client.LeaveSecurityGroups(d.Id(), rl) 500 if err != nil { 501 return err 502 } 503 } 504 505 d.SetPartial("security_groups") 506 } 507 508 d.Partial(false) 509 return resourceAliyunInstanceRead(d, meta) 510 } 511 512 func resourceAliyunInstanceDelete(d *schema.ResourceData, meta interface{}) error { 513 client := meta.(*AliyunClient) 514 conn := client.ecsconn 515 516 return resource.Retry(5*time.Minute, func() *resource.RetryError { 517 instance, err := client.QueryInstancesById(d.Id()) 518 if err != nil { 519 if notFoundError(err) { 520 return nil 521 } 522 } 523 524 if instance.Status != ecs.Stopped { 525 if err := conn.StopInstance(d.Id(), true); err != nil { 526 return resource.RetryableError(fmt.Errorf("ECS stop error - trying again.")) 527 } 528 529 if err := conn.WaitForInstance(d.Id(), ecs.Stopped, defaultTimeout); err != nil { 530 return resource.RetryableError(fmt.Errorf("Waiting for ecs stopped timeout - trying again.")) 531 } 532 } 533 534 if err := conn.DeleteInstance(d.Id()); err != nil { 535 return resource.RetryableError(fmt.Errorf("ECS Instance in use - trying again while it is deleted.")) 536 } 537 538 return nil 539 }) 540 541 } 542 543 func allocateIpAndBandWidthRelative(d *schema.ResourceData, meta interface{}) error { 544 conn := meta.(*AliyunClient).ecsconn 545 if d.Get("allocate_public_ip").(bool) { 546 if d.Get("internet_max_bandwidth_out") == 0 { 547 return fmt.Errorf("Error: if allocate_public_ip is true than the internet_max_bandwidth_out cannot equal zero.") 548 } 549 _, err := conn.AllocatePublicIpAddress(d.Id()) 550 if err != nil { 551 return fmt.Errorf("[DEBUG] AllocatePublicIpAddress for instance got error: %#v", err) 552 } 553 } 554 return nil 555 } 556 557 func buildAliyunRunInstancesArgs(d *schema.ResourceData, meta interface{}) (*ecs.RunInstanceArgs, error) { 558 args := &ecs.RunInstanceArgs{ 559 MaxAmount: DEFAULT_INSTANCE_COUNT, 560 MinAmount: DEFAULT_INSTANCE_COUNT, 561 } 562 563 bussStr, err := json.Marshal(DefaultBusinessInfo) 564 if err != nil { 565 log.Printf("Failed to translate bussiness info %#v from json to string", DefaultBusinessInfo) 566 } 567 568 args.BusinessInfo = string(bussStr) 569 570 subnetValue := d.Get("subnet_id").(string) 571 vswitchValue := d.Get("vswitch_id").(string) 572 //networkValue := d.Get("instance_network_type").(string) 573 574 // because runInstance is not compatible with createInstance, force NetworkType value to classic 575 if subnetValue == "" && vswitchValue == "" { 576 args.NetworkType = string(ClassicNet) 577 } 578 579 return args, nil 580 } 581 582 func buildAliyunInstanceArgs(d *schema.ResourceData, meta interface{}) (*ecs.CreateInstanceArgs, error) { 583 client := meta.(*AliyunClient) 584 585 args := &ecs.CreateInstanceArgs{ 586 RegionId: getRegion(d, meta), 587 InstanceType: d.Get("instance_type").(string), 588 } 589 590 imageID := d.Get("image_id").(string) 591 592 args.ImageId = imageID 593 594 systemDiskCategory := ecs.DiskCategory(d.Get("system_disk_category").(string)) 595 systemDiskSize := d.Get("system_disk_size").(int) 596 597 zoneID := d.Get("availability_zone").(string) 598 // check instanceType and systemDiskCategory, when zoneID is not empty 599 if zoneID != "" { 600 zone, err := client.DescribeZone(zoneID) 601 if err != nil { 602 return nil, err 603 } 604 605 if err := client.ResourceAvailable(zone, ecs.ResourceTypeInstance); err != nil { 606 return nil, err 607 } 608 609 if err := client.DiskAvailable(zone, systemDiskCategory); err != nil { 610 return nil, err 611 } 612 613 args.ZoneId = zoneID 614 615 } 616 617 args.SystemDisk = ecs.SystemDiskType{ 618 Category: systemDiskCategory, 619 Size: systemDiskSize, 620 } 621 622 sgs, ok := d.GetOk("security_groups") 623 624 if ok { 625 sgList := expandStringList(sgs.(*schema.Set).List()) 626 sg0 := sgList[0] 627 // check security group instance exist 628 _, err := client.DescribeSecurity(sg0) 629 if err == nil { 630 args.SecurityGroupId = sg0 631 } 632 } 633 634 if v := d.Get("instance_name").(string); v != "" { 635 args.InstanceName = v 636 } 637 638 if v := d.Get("description").(string); v != "" { 639 args.Description = v 640 } 641 642 if v := d.Get("internet_charge_type").(string); v != "" { 643 args.InternetChargeType = common.InternetChargeType(v) 644 } 645 646 if v := d.Get("internet_max_bandwidth_out").(int); v != 0 { 647 args.InternetMaxBandwidthOut = v 648 } 649 650 if v := d.Get("host_name").(string); v != "" { 651 args.HostName = v 652 } 653 654 if v := d.Get("password").(string); v != "" { 655 args.Password = v 656 } 657 658 if v := d.Get("io_optimized").(string); v != "" { 659 args.IoOptimized = ecs.IoOptimized(v) 660 } 661 662 vswitchValue := d.Get("subnet_id").(string) 663 if vswitchValue == "" { 664 vswitchValue = d.Get("vswitch_id").(string) 665 } 666 if vswitchValue != "" { 667 args.VSwitchId = vswitchValue 668 if d.Get("allocate_public_ip").(bool) && args.InternetMaxBandwidthOut <= 0 { 669 return nil, fmt.Errorf("Invalid internet_max_bandwidth_out result in allocation public ip failed in the VPC.") 670 } 671 } 672 673 if v := d.Get("instance_charge_type").(string); v != "" { 674 args.InstanceChargeType = common.InstanceChargeType(v) 675 } 676 677 log.Printf("[DEBUG] period is %d", d.Get("period").(int)) 678 if v := d.Get("period").(int); v != 0 { 679 args.Period = v 680 } else if args.InstanceChargeType == common.PrePaid { 681 return nil, fmt.Errorf("period is required for instance_charge_type is PrePaid") 682 } 683 684 if v := d.Get("user_data").(string); v != "" { 685 args.UserData = v 686 } 687 688 return args, nil 689 } 690 691 func userDataHashSum(user_data string) string { 692 // Check whether the user_data is not Base64 encoded. 693 // Always calculate hash of base64 decoded value since we 694 // check against double-encoding when setting it 695 v, base64DecodeError := base64.StdEncoding.DecodeString(user_data) 696 if base64DecodeError != nil { 697 v = []byte(user_data) 698 } 699 return string(v) 700 }