diff --git a/builder/tencentcloud/cvm/builder.go b/builder/tencentcloud/cvm/builder.go index 6a2a2cd1..f8b30a23 100644 --- a/builder/tencentcloud/cvm/builder.go +++ b/builder/tencentcloud/cvm/builder.go @@ -123,7 +123,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) }, // 遍历 subnet 列表, 尝试创建机器,直到创建成功或最终失败 &stepRunInstance{ - InstanceType: b.config.InstanceType, + InstanceTypeCandidates: b.config.InstanceTypeCandidates, InstanceChargeType: b.config.InstanceChargeType, UserData: b.config.UserData, UserDataFile: b.config.UserDataFile, diff --git a/builder/tencentcloud/cvm/builder.hcl2spec.go b/builder/tencentcloud/cvm/builder.hcl2spec.go index a0f900bb..687c2c9c 100644 --- a/builder/tencentcloud/cvm/builder.hcl2spec.go +++ b/builder/tencentcloud/cvm/builder.hcl2spec.go @@ -39,7 +39,8 @@ type FlatConfig struct { SourceImageId *string `mapstructure:"source_image_id" required:"false" cty:"source_image_id" hcl:"source_image_id"` SourceImageName *string `mapstructure:"source_image_name" required:"false" cty:"source_image_name" hcl:"source_image_name"` InstanceChargeType *string `mapstructure:"instance_charge_type" required:"false" cty:"instance_charge_type" hcl:"instance_charge_type"` - InstanceType *string `mapstructure:"instance_type" required:"true" cty:"instance_type" hcl:"instance_type"` + InstanceTypeCandidates []string `mapstructure:"instance_type_candidates" required:"false" cty:"instance_type_candidates" hcl:"instance_type_candidates"` + InstanceType *string `mapstructure:"instance_type" required:"false" cty:"instance_type" hcl:"instance_type"` InstanceName *string `mapstructure:"instance_name" required:"false" cty:"instance_name" hcl:"instance_name"` DiskType *string `mapstructure:"disk_type" required:"false" cty:"disk_type" hcl:"disk_type"` DiskSize *int64 `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"` @@ -158,6 +159,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "source_image_id": &hcldec.AttrSpec{Name: "source_image_id", Type: cty.String, Required: false}, "source_image_name": &hcldec.AttrSpec{Name: "source_image_name", Type: cty.String, Required: false}, "instance_charge_type": &hcldec.AttrSpec{Name: "instance_charge_type", Type: cty.String, Required: false}, + "instance_type_candidates": &hcldec.AttrSpec{Name: "instance_type_candidates", Type: cty.List(cty.String), Required: false}, "instance_type": &hcldec.AttrSpec{Name: "instance_type", Type: cty.String, Required: false}, "instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false}, "disk_type": &hcldec.AttrSpec{Name: "disk_type", Type: cty.String, Required: false}, diff --git a/builder/tencentcloud/cvm/run_config.go b/builder/tencentcloud/cvm/run_config.go index 4d29d3ce..cc923f99 100644 --- a/builder/tencentcloud/cvm/run_config.go +++ b/builder/tencentcloud/cvm/run_config.go @@ -36,10 +36,15 @@ type TencentCloudRunConfig struct { SourceImageName string `mapstructure:"source_image_name" required:"false"` // Charge type of cvm, values can be `POSTPAID_BY_HOUR` (default) `SPOTPAID` InstanceChargeType string `mapstructure:"instance_charge_type" required:"false"` + // The instance type candidate list your cvm will be launched by. + // Will try to launch instance type from this list in order. + // You should reference Instace Type + // for parameter taking. + InstanceTypeCandidates []string `mapstructure:"instance_type_candidates" required:"false"` // The instance type your cvm will be launched by. // You should reference Instace Type // for parameter taking. - InstanceType string `mapstructure:"instance_type" required:"true"` + InstanceType string `mapstructure:"instance_type" required:"false"` // Instance name. InstanceName string `mapstructure:"instance_name" required:"false"` // Root disk type your cvm will be launched by, default is `CLOUD_PREMIUM`. you could @@ -133,8 +138,13 @@ func (cf *TencentCloudRunConfig) Prepare(ctx *interpolate.Context) []error { errs = append(errs, errors.New("source_image_id wrong format")) } - if cf.InstanceType == "" { - errs = append(errs, errors.New("instance_type must be specified")) + if cf.InstanceType != "" && len(cf.InstanceTypeCandidates) != 0 { + errs = append(errs, errors.New("only one of instance_type or instance_type_candidates can be specified")) + } else if cf.InstanceType == "" && len(cf.InstanceTypeCandidates) == 0 { + errs = append(errs, errors.New("instance_type or instance_type_candidates must be specified")) + } else if len(cf.InstanceTypeCandidates) == 0 { + // normalize + cf.InstanceTypeCandidates = []string{cf.InstanceType} } if cf.UserData != "" && cf.UserDataFile != "" { @@ -145,7 +155,7 @@ func (cf *TencentCloudRunConfig) Prepare(ctx *interpolate.Context) []error { } } - // 添加SubnetName的判断,制定了SubnetName会自动搜索SubnetId + // 添加SubnetName的判断,指定了SubnetName会自动搜索SubnetId if (cf.VpcId != "" || cf.CidrBlock != "") && cf.SubnetId == "" && cf.SubnetName == "" && cf.SubnectCidrBlock == "" { errs = append(errs, errors.New("if vpc cidr_block is specified, then "+ "subnet_cidr_block must also be specified.")) diff --git a/builder/tencentcloud/cvm/run_config_test.go b/builder/tencentcloud/cvm/run_config_test.go index 97536ea6..a7f86226 100644 --- a/builder/tencentcloud/cvm/run_config_test.go +++ b/builder/tencentcloud/cvm/run_config_test.go @@ -13,8 +13,8 @@ import ( func testConfig() *TencentCloudRunConfig { return &TencentCloudRunConfig{ - SourceImageId: "img-qwer1234", - InstanceType: "S3.SMALL2", + SourceImageId: "img-qwer1234", + InstanceTypeCandidates: []string{"S3.SMALL2"}, Comm: communicator.Config{ SSH: communicator.SSH{ SSHUsername: "tencentcloud", @@ -30,12 +30,18 @@ func TestTencentCloudRunConfig_Prepare(t *testing.T) { t.Fatalf("shouldn't have err: %v", err) } + cf.InstanceType = "S3.SMALL2" + if err := cf.Prepare(nil); err == nil { + t.Fatal("should have err") + } + cf.InstanceType = "" + cf.InstanceTypeCandidates = []string{} if err := cf.Prepare(nil); err == nil { t.Fatal("should have err") } - cf.InstanceType = "S3.SMALL2" + cf.InstanceTypeCandidates = []string{"S3.SMALL2"} cf.SourceImageId = "" if err := cf.Prepare(nil); err == nil { t.Fatal("should have err") diff --git a/builder/tencentcloud/cvm/step_check_source_image.go b/builder/tencentcloud/cvm/step_check_source_image.go index 616c4734..5d1a851d 100644 --- a/builder/tencentcloud/cvm/step_check_source_image.go +++ b/builder/tencentcloud/cvm/step_check_source_image.go @@ -27,7 +27,6 @@ func (s *stepCheckSourceImage) Run(ctx context.Context, state multistep.StateBag Say(state, config.SourceImageId, "Trying to check source image") req := cvm.NewDescribeImagesRequest() - req.InstanceType = &config.InstanceType if config.SourceImageId != "" { req.ImageIds = []*string{&config.SourceImageId} } else { @@ -63,7 +62,7 @@ func (s *stepCheckSourceImage) Run(ctx context.Context, state multistep.StateBag } } - return Halt(state, fmt.Errorf("No image found under current instance_type(%s) restriction", config.InstanceType), "") + return Halt(state, fmt.Errorf("No image found"), "") } func (s *stepCheckSourceImage) Cleanup(bag multistep.StateBag) {} diff --git a/builder/tencentcloud/cvm/step_config_subnet.go b/builder/tencentcloud/cvm/step_config_subnet.go index e4d8af4c..ff59b6d5 100644 --- a/builder/tencentcloud/cvm/step_config_subnet.go +++ b/builder/tencentcloud/cvm/step_config_subnet.go @@ -6,12 +6,11 @@ package cvm import ( "context" "fmt" - "strings" "github.com/hashicorp/packer-plugin-sdk/multistep" "github.com/hashicorp/packer-plugin-sdk/uuid" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" ) @@ -25,74 +24,35 @@ type stepConfigSubnet struct { func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { vpcClient := state.Get("vpc_client").(*vpc.Client) - cvmClient := state.Get("cvm_client").(*cvm.Client) vpcId := state.Get("vpc_id").(string) - instanceType := state.Get("config").(*Config).InstanceType - - zones := []string{s.Zone} - // 根据机型自动选择可用区 - if len(s.Zone) == 0 { - Say(state, fmt.Sprintf("Try to get available zones for instance: %s", instanceType), "") - req := cvm.NewDescribeZoneInstanceConfigInfosRequest() - req.Filters = []*cvm.Filter{ - { - Name: common.StringPtr("instance-type"), - Values: common.StringPtrs([]string{instanceType}), - }, - { - Name: common.StringPtr("instance-charge-type"), - Values: common.StringPtrs([]string{"POSTPAID_BY_HOUR"}), - }, - } - var resp *cvm.DescribeZoneInstanceConfigInfosResponse - err := Retry(ctx, func(ctx context.Context) error { - var e error - resp, e = cvmClient.DescribeZoneInstanceConfigInfos(req) - return e - }) - if err != nil { - return Halt(state, err, "Failed to get available zones instance config") - } - if len(resp.Response.InstanceTypeQuotaSet) > 0 { - zones = make([]string, 0) - Say(state, fmt.Sprintf("length:%d", len(resp.Response.InstanceTypeQuotaSet)), "") - for _, z := range resp.Response.InstanceTypeQuotaSet { - zones = append(zones, *z.Zone) - } - Say(state, fmt.Sprintf("Found zones: %s", strings.Join(zones, ",")), "") - } else { - Say(state, fmt.Sprintf("The instance type %s isn't available in this region."+ - "\n You can change to other regions.", instanceType), "") - state.Put("error", fmt.Errorf("The instance type %s isn't available in this region."+ - "\n You can change to other regions.", instanceType)) - return multistep.ActionHalt - } - } // 如果指定了子网ID或子网名称,则尝试使用已有子网 if len(s.SubnetId) != 0 || len(s.SubnetName) != 0 { Say(state, fmt.Sprintf("Trying to use existing subnet id: %s, name: %s", s.SubnetId, s.SubnetName), "") req := vpc.NewDescribeSubnetsRequest() + req.Filters = []*vpc.Filter{ + { + Name: common.StringPtr("vpc-id"), + Values: common.StringPtrs([]string{vpcId}), + }, + } + // 搜索指定所有可用区或所有可用区中符合条件的subnet + if s.Zone != "" { + req.Filters = append(req.Filters, + &vpc.Filter{ + Name: common.StringPtr("zone"), + Values: common.StringPtrs([]string{s.Zone}), + }) + } // 空字符串作为参数会报错 if s.SubnetId != "" { req.SubnetIds = []*string{&s.SubnetId} - } - if len(s.SubnetName) != 0 { - // s.zones列表长度不能超过5,取最后五个 - if len(zones) > 5 { - zones = zones[len(zones)-5:] - } - // 搜索机型在售所有可用区内符合subnet名称的subnet - req.Filters = []*vpc.Filter{ - { + } else if len(s.SubnetName) != 0 { + req.Filters = append(req.Filters, + &vpc.Filter{ Name: common.StringPtr("subnet-name"), Values: common.StringPtrs([]string{s.SubnetName}), - }, - { - Name: common.StringPtr("zone"), - Values: common.StringPtrs(zones), - }, - } + }) } var resp *vpc.DescribeSubnetsResponse err := Retry(ctx, func(ctx context.Context) error { @@ -104,12 +64,6 @@ func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) mu return Halt(state, err, "Failed to get subnet info") } if *resp.Response.TotalCount > 0 { - for _, subnet := range resp.Response.SubnetSet { - if *subnet.VpcId != vpcId { - return Halt(state, fmt.Errorf("the specified subnet(%s) does not belong to the specified vpc(%s)", - *subnet.SubnetId, vpcId), "") - } - } state.Put("subnets", resp.Response.SubnetSet) Message(state, fmt.Sprintf("%d subnets in total.", *resp.Response.TotalCount), "Subnet found") return multistep.ActionContinue @@ -120,34 +74,29 @@ func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) mu // 遍历候选可用区,在对应可用区内创建subnet并将subnet收集起来便于后续销毁 // 此时subnetname一定为空,使用随机生成的名称 s.SubnetName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()[:8]) - for _, zone := range zones { - Say(state, s.SubnetName, "Trying to create a new subnet") - req := vpc.NewCreateSubnetRequest() - req.VpcId = &vpcId - req.SubnetName = &s.SubnetName - req.CidrBlock = &s.SubnetCidrBlock - req.Zone = &zone - var resp *vpc.CreateSubnetResponse - err := Retry(ctx, func(ctx context.Context) error { - var e error - resp, e = vpcClient.CreateSubnet(req) - return e - }) - if err != nil { - Say(state, s.SubnetName, "Failed to create subnet") - continue - } - - // 创建成功后都将subnet收集起来,便于后续销毁 - s.createdSubnet = resp.Response.Subnet - Message(state, fmt.Sprintf("subnet created: %s in zone: %s", *s.createdSubnet.SubnetId, *s.createdSubnet.Zone), "Subnet created") - - // 由于cidr冲突,不能用同一个cidr创建多个subnet,所以创建成功后直接继续 - state.Put("subnets", []*vpc.Subnet{s.createdSubnet}) - return multistep.ActionContinue + Say(state, s.SubnetName, "Trying to create a new subnet") + req := vpc.NewCreateSubnetRequest() + req.VpcId = &vpcId + req.SubnetName = &s.SubnetName + req.CidrBlock = &s.SubnetCidrBlock + req.Zone = &s.Zone + var resp *vpc.CreateSubnetResponse + err := Retry(ctx, func(ctx context.Context) error { + var e error + resp, e = vpcClient.CreateSubnet(req) + return e + }) + if err != nil { + return Halt(state, err, "Failed to create subnet") } - return Halt(state, fmt.Errorf("cannot create subnet"), "no available subnet") + // 创建成功后都将subnet收集起来,便于后续销毁 + s.createdSubnet = resp.Response.Subnet + Message(state, fmt.Sprintf("subnet created: %s in zone: %s", *s.createdSubnet.SubnetId, *s.createdSubnet.Zone), "Subnet created") + + // 由于cidr冲突,不能用同一个cidr创建多个subnet,所以创建成功后直接继续 + state.Put("subnets", []*vpc.Subnet{s.createdSubnet}) + return multistep.ActionContinue } func (s *stepConfigSubnet) Cleanup(state multistep.StateBag) { diff --git a/builder/tencentcloud/cvm/step_run_instance.go b/builder/tencentcloud/cvm/step_run_instance.go index 61653b23..e5b41cab 100644 --- a/builder/tencentcloud/cvm/step_run_instance.go +++ b/builder/tencentcloud/cvm/step_run_instance.go @@ -19,7 +19,7 @@ import ( // 移除了zoneid,由subnet step生成的subnet信息提供 type stepRunInstance struct { - InstanceType string + InstanceTypeCandidates []string InstanceChargeType string UserData string UserDataFile string @@ -78,7 +78,7 @@ func (s *stepRunInstance) Run(ctx context.Context, state multistep.StateBag) mul } req.InstanceChargeType = &instanceChargeType req.ImageId = source_image.ImageId - req.InstanceType = &s.InstanceType + // Instance type will be set later // TODO: Add check for system disk size, it should be larger than image system disk size. req.SystemDisk = &cvm.SystemDisk{ DiskType: &s.DiskType, @@ -168,35 +168,40 @@ func (s *stepRunInstance) Run(ctx context.Context, state multistep.StateBag) mul return Halt(state, fmt.Errorf("no subnets in state"), "Cannot get subnets info when starting instance") } err = fmt.Errorf("No subnet found") - // 腾讯云开机时返回instanceid后还需要等待实例状态为running才可认为开机成功。 - for _, subnet := range subnets.([]*vpc.Subnet) { - var instanceIds []*string - instanceIds, err = s.CreateCvmInstance(ctx, state, subnet, req) - if err == nil { - // 此时 WaitForInstance 已经确认了instance状态为RUNNING,可以认为开机成功,且id不可能为空 - s.instanceId = *instanceIds[0] - break - } - // InstanceIdSet不为空,代表已经创建了instance,但是开机不成功,此时需要删除instance - if len(instanceIds) > 0 { - // 尝试删除已有的instanceId,避免资源泄露 - terminateReq := cvm.NewTerminateInstancesRequest() - terminateReq.InstanceIds = instanceIds - terminateErr := Retry(ctx, func(ctx context.Context) error { - _, e := client.TerminateInstances(terminateReq) - return e - }) - // 如果删除失败,且不是因为instanceId不存在,则报错 - // instanceId不存在代表之前开机不成功,此处不需要再次删除。若是LAUNCH_FAILED会预到Code=InvalidInstanceId.NotFound,跳过尝试下一个subnet继续尝试开机即可 - if terminateErr != nil && terminateErr.(*errors.TencentCloudSDKError).Code != "InvalidInstanceId.NotFound" { - // undefined behavior, just halt - // halt use put to store error in state, it cannot append - var builder strings.Builder - for _, instanceId := range instanceIds { - builder.WriteString(*instanceId) - builder.WriteString(",") + // 根据instance_type_candidates顺序尝试创建instance +loop: + for _, instanceType := range s.InstanceTypeCandidates { + req.InstanceType = &instanceType + // 腾讯云开机时返回instanceid后还需要等待实例状态为running才可认为开机成功。 + for _, subnet := range subnets.([]*vpc.Subnet) { + var instanceIds []*string + instanceIds, err = s.CreateCvmInstance(ctx, state, subnet, req) + if err == nil { + // 此时 WaitForInstance 已经确认了instance状态为RUNNING,可以认为开机成功,且id不可能为空 + s.instanceId = *instanceIds[0] + break loop + } + // InstanceIdSet不为空,代表已经创建了instance,但是开机不成功,此时需要删除instance + if len(instanceIds) > 0 { + // 尝试删除已有的instanceId,避免资源泄露 + terminateReq := cvm.NewTerminateInstancesRequest() + terminateReq.InstanceIds = instanceIds + terminateErr := Retry(ctx, func(ctx context.Context) error { + _, e := client.TerminateInstances(terminateReq) + return e + }) + // 如果删除失败,且不是因为instanceId不存在,则报错 + // instanceId不存在代表之前开机不成功,此处不需要再次删除。若是LAUNCH_FAILED会预到Code=InvalidInstanceId.NotFound,跳过尝试下一个subnet继续尝试开机即可 + if terminateErr != nil && terminateErr.(*errors.TencentCloudSDKError).Code != "InvalidInstanceId.NotFound" { + // undefined behavior, just halt + // halt use put to store error in state, it cannot append + var builder strings.Builder + for _, instanceId := range instanceIds { + builder.WriteString(*instanceId) + builder.WriteString(",") + } + return Halt(state, terminateErr, fmt.Sprintf("Failed to terminate instance %s may need to delete it manually", builder.String())) } - return Halt(state, terminateErr, fmt.Sprintf("Failed to terminate instance %s may need to delete it manually", builder.String())) } } } @@ -268,7 +273,7 @@ func (s *stepRunInstance) CreateCvmInstance(ctx context.Context, state multistep vpcId := state.Get("vpc_id").(string) Say(state, fmt.Sprintf("instance-type: %s, subnet-id: %s, zone: %s", - s.InstanceType, *subnet.SubnetId, *subnet.Zone, + *req.InstanceType, *subnet.SubnetId, *subnet.Zone, ), "Try to create instance") req.VirtualPrivateCloud = &cvm.VirtualPrivateCloud{ VpcId: &vpcId, diff --git a/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-not-required.mdx b/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-not-required.mdx index 16770d8c..dc790b10 100644 --- a/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-not-required.mdx +++ b/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-not-required.mdx @@ -11,6 +11,15 @@ - `instance_charge_type` (string) - Charge type of cvm, values can be `POSTPAID_BY_HOUR` (default) `SPOTPAID` +- `instance_type_candidates` ([]string) - The instance type candidate list your cvm will be launched by. + Will try to launch instance type from this list in order. + You should reference Instace Type + for parameter taking. + +- `instance_type` (string) - The instance type your cvm will be launched by. + You should reference Instace Type + for parameter taking. + - `instance_name` (string) - Instance name. - `disk_type` (string) - Root disk type your cvm will be launched by, default is `CLOUD_PREMIUM`. you could