Skip to content

Commit ce0821e

Browse files
feat(instance): 添加候选机型规格列表支持
1 parent 0f052fc commit ce0821e

File tree

8 files changed

+113
-133
lines changed

8 files changed

+113
-133
lines changed

builder/tencentcloud/cvm/builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
123123
},
124124
// 遍历 subnet 列表, 尝试创建机器,直到创建成功或最终失败
125125
&stepRunInstance{
126-
InstanceType: b.config.InstanceType,
126+
InstanceTypeCandidates: b.config.InstanceTypeCandidates,
127127
InstanceChargeType: b.config.InstanceChargeType,
128128
UserData: b.config.UserData,
129129
UserDataFile: b.config.UserDataFile,

builder/tencentcloud/cvm/builder.hcl2spec.go

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/tencentcloud/cvm/run_config.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ type TencentCloudRunConfig struct {
3636
SourceImageName string `mapstructure:"source_image_name" required:"false"`
3737
// Charge type of cvm, values can be `POSTPAID_BY_HOUR` (default) `SPOTPAID`
3838
InstanceChargeType string `mapstructure:"instance_charge_type" required:"false"`
39+
// The instance type candidate list your cvm will be launched by.
40+
// Will try to launch instance type from this list in order.
41+
// You should reference Instace Type
42+
// for parameter taking.
43+
InstanceTypeCandidates []string `mapstructure:"instance_type_candidates" required:"false"`
3944
// The instance type your cvm will be launched by.
4045
// You should reference Instace Type
4146
// for parameter taking.
42-
InstanceType string `mapstructure:"instance_type" required:"true"`
47+
InstanceType string `mapstructure:"instance_type" required:"false"`
4348
// Instance name.
4449
InstanceName string `mapstructure:"instance_name" required:"false"`
4550
// 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 {
133138
errs = append(errs, errors.New("source_image_id wrong format"))
134139
}
135140

136-
if cf.InstanceType == "" {
137-
errs = append(errs, errors.New("instance_type must be specified"))
141+
if cf.InstanceType != "" && len(cf.InstanceTypeCandidates) != 0 {
142+
errs = append(errs, errors.New("only one of instance_type or instance_type_candidates can be specified"))
143+
} else if cf.InstanceType == "" && len(cf.InstanceTypeCandidates) == 0 {
144+
errs = append(errs, errors.New("instance_type or instance_type_candidates must be specified"))
145+
} else if len(cf.InstanceTypeCandidates) == 0 {
146+
// normalize
147+
cf.InstanceTypeCandidates = []string{cf.InstanceType}
138148
}
139149

140150
if cf.UserData != "" && cf.UserDataFile != "" {
@@ -145,7 +155,7 @@ func (cf *TencentCloudRunConfig) Prepare(ctx *interpolate.Context) []error {
145155
}
146156
}
147157

148-
// 添加SubnetName的判断,制定了SubnetName会自动搜索SubnetId
158+
// 添加SubnetName的判断,指定了SubnetName会自动搜索SubnetId
149159
if (cf.VpcId != "" || cf.CidrBlock != "") && cf.SubnetId == "" && cf.SubnetName == "" && cf.SubnectCidrBlock == "" {
150160
errs = append(errs, errors.New("if vpc cidr_block is specified, then "+
151161
"subnet_cidr_block must also be specified."))

builder/tencentcloud/cvm/run_config_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313

1414
func testConfig() *TencentCloudRunConfig {
1515
return &TencentCloudRunConfig{
16-
SourceImageId: "img-qwer1234",
17-
InstanceType: "S3.SMALL2",
16+
SourceImageId: "img-qwer1234",
17+
InstanceTypeCandidates: []string{"S3.SMALL2"},
1818
Comm: communicator.Config{
1919
SSH: communicator.SSH{
2020
SSHUsername: "tencentcloud",
@@ -30,12 +30,18 @@ func TestTencentCloudRunConfig_Prepare(t *testing.T) {
3030
t.Fatalf("shouldn't have err: %v", err)
3131
}
3232

33+
cf.InstanceType = "S3.SMALL2"
34+
if err := cf.Prepare(nil); err == nil {
35+
t.Fatal("should have err")
36+
}
37+
3338
cf.InstanceType = ""
39+
cf.InstanceTypeCandidates = []string{}
3440
if err := cf.Prepare(nil); err == nil {
3541
t.Fatal("should have err")
3642
}
3743

38-
cf.InstanceType = "S3.SMALL2"
44+
cf.InstanceTypeCandidates = []string{"S3.SMALL2"}
3945
cf.SourceImageId = ""
4046
if err := cf.Prepare(nil); err == nil {
4147
t.Fatal("should have err")

builder/tencentcloud/cvm/step_check_source_image.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ func (s *stepCheckSourceImage) Run(ctx context.Context, state multistep.StateBag
2727
Say(state, config.SourceImageId, "Trying to check source image")
2828

2929
req := cvm.NewDescribeImagesRequest()
30-
req.InstanceType = &config.InstanceType
3130
if config.SourceImageId != "" {
3231
req.ImageIds = []*string{&config.SourceImageId}
3332
} else {
@@ -63,7 +62,7 @@ func (s *stepCheckSourceImage) Run(ctx context.Context, state multistep.StateBag
6362
}
6463
}
6564

66-
return Halt(state, fmt.Errorf("No image found under current instance_type(%s) restriction", config.InstanceType), "")
65+
return Halt(state, fmt.Errorf("No image found"), "")
6766
}
6867

6968
func (s *stepCheckSourceImage) Cleanup(bag multistep.StateBag) {}

builder/tencentcloud/cvm/step_config_subnet.go

Lines changed: 40 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ package cvm
66
import (
77
"context"
88
"fmt"
9-
"strings"
109

1110
"github.com/hashicorp/packer-plugin-sdk/multistep"
1211
"github.com/hashicorp/packer-plugin-sdk/uuid"
12+
1313
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
14-
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
1514
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
1615
)
1716

@@ -25,74 +24,35 @@ type stepConfigSubnet struct {
2524

2625
func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
2726
vpcClient := state.Get("vpc_client").(*vpc.Client)
28-
cvmClient := state.Get("cvm_client").(*cvm.Client)
2927
vpcId := state.Get("vpc_id").(string)
30-
instanceType := state.Get("config").(*Config).InstanceType
31-
32-
zones := []string{s.Zone}
33-
// 根据机型自动选择可用区
34-
if len(s.Zone) == 0 {
35-
Say(state, fmt.Sprintf("Try to get available zones for instance: %s", instanceType), "")
36-
req := cvm.NewDescribeZoneInstanceConfigInfosRequest()
37-
req.Filters = []*cvm.Filter{
38-
{
39-
Name: common.StringPtr("instance-type"),
40-
Values: common.StringPtrs([]string{instanceType}),
41-
},
42-
{
43-
Name: common.StringPtr("instance-charge-type"),
44-
Values: common.StringPtrs([]string{"POSTPAID_BY_HOUR"}),
45-
},
46-
}
47-
var resp *cvm.DescribeZoneInstanceConfigInfosResponse
48-
err := Retry(ctx, func(ctx context.Context) error {
49-
var e error
50-
resp, e = cvmClient.DescribeZoneInstanceConfigInfos(req)
51-
return e
52-
})
53-
if err != nil {
54-
return Halt(state, err, "Failed to get available zones instance config")
55-
}
56-
if len(resp.Response.InstanceTypeQuotaSet) > 0 {
57-
zones = make([]string, 0)
58-
Say(state, fmt.Sprintf("length:%d", len(resp.Response.InstanceTypeQuotaSet)), "")
59-
for _, z := range resp.Response.InstanceTypeQuotaSet {
60-
zones = append(zones, *z.Zone)
61-
}
62-
Say(state, fmt.Sprintf("Found zones: %s", strings.Join(zones, ",")), "")
63-
} else {
64-
Say(state, fmt.Sprintf("The instance type %s isn't available in this region."+
65-
"\n You can change to other regions.", instanceType), "")
66-
state.Put("error", fmt.Errorf("The instance type %s isn't available in this region."+
67-
"\n You can change to other regions.", instanceType))
68-
return multistep.ActionHalt
69-
}
70-
}
7128

7229
// 如果指定了子网ID或子网名称,则尝试使用已有子网
7330
if len(s.SubnetId) != 0 || len(s.SubnetName) != 0 {
7431
Say(state, fmt.Sprintf("Trying to use existing subnet id: %s, name: %s", s.SubnetId, s.SubnetName), "")
7532
req := vpc.NewDescribeSubnetsRequest()
33+
req.Filters = []*vpc.Filter{
34+
{
35+
Name: common.StringPtr("vpc-id"),
36+
Values: common.StringPtrs([]string{vpcId}),
37+
},
38+
}
39+
// 搜索指定所有可用区或所有可用区中符合条件的subnet
40+
if s.Zone != "" {
41+
req.Filters = append(req.Filters,
42+
&vpc.Filter{
43+
Name: common.StringPtr("zone"),
44+
Values: common.StringPtrs([]string{s.Zone}),
45+
})
46+
}
7647
// 空字符串作为参数会报错
7748
if s.SubnetId != "" {
7849
req.SubnetIds = []*string{&s.SubnetId}
79-
}
80-
if len(s.SubnetName) != 0 {
81-
// s.zones列表长度不能超过5,取最后五个
82-
if len(zones) > 5 {
83-
zones = zones[len(zones)-5:]
84-
}
85-
// 搜索机型在售所有可用区内符合subnet名称的subnet
86-
req.Filters = []*vpc.Filter{
87-
{
50+
} else if len(s.SubnetName) != 0 {
51+
req.Filters = append(req.Filters,
52+
&vpc.Filter{
8853
Name: common.StringPtr("subnet-name"),
8954
Values: common.StringPtrs([]string{s.SubnetName}),
90-
},
91-
{
92-
Name: common.StringPtr("zone"),
93-
Values: common.StringPtrs(zones),
94-
},
95-
}
55+
})
9656
}
9757
var resp *vpc.DescribeSubnetsResponse
9858
err := Retry(ctx, func(ctx context.Context) error {
@@ -104,12 +64,6 @@ func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) mu
10464
return Halt(state, err, "Failed to get subnet info")
10565
}
10666
if *resp.Response.TotalCount > 0 {
107-
for _, subnet := range resp.Response.SubnetSet {
108-
if *subnet.VpcId != vpcId {
109-
return Halt(state, fmt.Errorf("the specified subnet(%s) does not belong to the specified vpc(%s)",
110-
*subnet.SubnetId, vpcId), "")
111-
}
112-
}
11367
state.Put("subnets", resp.Response.SubnetSet)
11468
Message(state, fmt.Sprintf("%d subnets in total.", *resp.Response.TotalCount), "Subnet found")
11569
return multistep.ActionContinue
@@ -120,34 +74,29 @@ func (s *stepConfigSubnet) Run(ctx context.Context, state multistep.StateBag) mu
12074
// 遍历候选可用区,在对应可用区内创建subnet并将subnet收集起来便于后续销毁
12175
// 此时subnetname一定为空,使用随机生成的名称
12276
s.SubnetName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()[:8])
123-
for _, zone := range zones {
124-
Say(state, s.SubnetName, "Trying to create a new subnet")
125-
req := vpc.NewCreateSubnetRequest()
126-
req.VpcId = &vpcId
127-
req.SubnetName = &s.SubnetName
128-
req.CidrBlock = &s.SubnetCidrBlock
129-
req.Zone = &zone
130-
var resp *vpc.CreateSubnetResponse
131-
err := Retry(ctx, func(ctx context.Context) error {
132-
var e error
133-
resp, e = vpcClient.CreateSubnet(req)
134-
return e
135-
})
136-
if err != nil {
137-
Say(state, s.SubnetName, "Failed to create subnet")
138-
continue
139-
}
140-
141-
// 创建成功后都将subnet收集起来,便于后续销毁
142-
s.createdSubnet = resp.Response.Subnet
143-
Message(state, fmt.Sprintf("subnet created: %s in zone: %s", *s.createdSubnet.SubnetId, *s.createdSubnet.Zone), "Subnet created")
144-
145-
// 由于cidr冲突,不能用同一个cidr创建多个subnet,所以创建成功后直接继续
146-
state.Put("subnets", []*vpc.Subnet{s.createdSubnet})
147-
return multistep.ActionContinue
77+
Say(state, s.SubnetName, "Trying to create a new subnet")
78+
req := vpc.NewCreateSubnetRequest()
79+
req.VpcId = &vpcId
80+
req.SubnetName = &s.SubnetName
81+
req.CidrBlock = &s.SubnetCidrBlock
82+
req.Zone = &s.Zone
83+
var resp *vpc.CreateSubnetResponse
84+
err := Retry(ctx, func(ctx context.Context) error {
85+
var e error
86+
resp, e = vpcClient.CreateSubnet(req)
87+
return e
88+
})
89+
if err != nil {
90+
return Halt(state, err, "Failed to create subnet")
14891
}
14992

150-
return Halt(state, fmt.Errorf("cannot create subnet"), "no available subnet")
93+
// 创建成功后都将subnet收集起来,便于后续销毁
94+
s.createdSubnet = resp.Response.Subnet
95+
Message(state, fmt.Sprintf("subnet created: %s in zone: %s", *s.createdSubnet.SubnetId, *s.createdSubnet.Zone), "Subnet created")
96+
97+
// 由于cidr冲突,不能用同一个cidr创建多个subnet,所以创建成功后直接继续
98+
state.Put("subnets", []*vpc.Subnet{s.createdSubnet})
99+
return multistep.ActionContinue
151100
}
152101

153102
func (s *stepConfigSubnet) Cleanup(state multistep.StateBag) {

builder/tencentcloud/cvm/step_run_instance.go

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
// 移除了zoneid,由subnet step生成的subnet信息提供
2121
type stepRunInstance struct {
22-
InstanceType string
22+
InstanceTypeCandidates []string
2323
InstanceChargeType string
2424
UserData string
2525
UserDataFile string
@@ -78,7 +78,7 @@ func (s *stepRunInstance) Run(ctx context.Context, state multistep.StateBag) mul
7878
}
7979
req.InstanceChargeType = &instanceChargeType
8080
req.ImageId = source_image.ImageId
81-
req.InstanceType = &s.InstanceType
81+
// Instance type will be set later
8282
// TODO: Add check for system disk size, it should be larger than image system disk size.
8383
req.SystemDisk = &cvm.SystemDisk{
8484
DiskType: &s.DiskType,
@@ -168,35 +168,40 @@ func (s *stepRunInstance) Run(ctx context.Context, state multistep.StateBag) mul
168168
return Halt(state, fmt.Errorf("no subnets in state"), "Cannot get subnets info when starting instance")
169169
}
170170
err = fmt.Errorf("No subnet found")
171-
// 腾讯云开机时返回instanceid后还需要等待实例状态为running才可认为开机成功。
172-
for _, subnet := range subnets.([]*vpc.Subnet) {
173-
var instanceIds []*string
174-
instanceIds, err = s.CreateCvmInstance(ctx, state, subnet, req)
175-
if err == nil {
176-
// 此时 WaitForInstance 已经确认了instance状态为RUNNING,可以认为开机成功,且id不可能为空
177-
s.instanceId = *instanceIds[0]
178-
break
179-
}
180-
// InstanceIdSet不为空,代表已经创建了instance,但是开机不成功,此时需要删除instance
181-
if len(instanceIds) > 0 {
182-
// 尝试删除已有的instanceId,避免资源泄露
183-
terminateReq := cvm.NewTerminateInstancesRequest()
184-
terminateReq.InstanceIds = instanceIds
185-
terminateErr := Retry(ctx, func(ctx context.Context) error {
186-
_, e := client.TerminateInstances(terminateReq)
187-
return e
188-
})
189-
// 如果删除失败,且不是因为instanceId不存在,则报错
190-
// instanceId不存在代表之前开机不成功,此处不需要再次删除。若是LAUNCH_FAILED会预到Code=InvalidInstanceId.NotFound,跳过尝试下一个subnet继续尝试开机即可
191-
if terminateErr != nil && terminateErr.(*errors.TencentCloudSDKError).Code != "InvalidInstanceId.NotFound" {
192-
// undefined behavior, just halt
193-
// halt use put to store error in state, it cannot append
194-
var builder strings.Builder
195-
for _, instanceId := range instanceIds {
196-
builder.WriteString(*instanceId)
197-
builder.WriteString(",")
171+
// 根据instance_type_candidates顺序尝试创建instance
172+
loop:
173+
for _, instanceType := range s.InstanceTypeCandidates {
174+
req.InstanceType = &instanceType
175+
// 腾讯云开机时返回instanceid后还需要等待实例状态为running才可认为开机成功。
176+
for _, subnet := range subnets.([]*vpc.Subnet) {
177+
var instanceIds []*string
178+
instanceIds, err = s.CreateCvmInstance(ctx, state, subnet, req)
179+
if err == nil {
180+
// 此时 WaitForInstance 已经确认了instance状态为RUNNING,可以认为开机成功,且id不可能为空
181+
s.instanceId = *instanceIds[0]
182+
break loop
183+
}
184+
// InstanceIdSet不为空,代表已经创建了instance,但是开机不成功,此时需要删除instance
185+
if len(instanceIds) > 0 {
186+
// 尝试删除已有的instanceId,避免资源泄露
187+
terminateReq := cvm.NewTerminateInstancesRequest()
188+
terminateReq.InstanceIds = instanceIds
189+
terminateErr := Retry(ctx, func(ctx context.Context) error {
190+
_, e := client.TerminateInstances(terminateReq)
191+
return e
192+
})
193+
// 如果删除失败,且不是因为instanceId不存在,则报错
194+
// instanceId不存在代表之前开机不成功,此处不需要再次删除。若是LAUNCH_FAILED会预到Code=InvalidInstanceId.NotFound,跳过尝试下一个subnet继续尝试开机即可
195+
if terminateErr != nil && terminateErr.(*errors.TencentCloudSDKError).Code != "InvalidInstanceId.NotFound" {
196+
// undefined behavior, just halt
197+
// halt use put to store error in state, it cannot append
198+
var builder strings.Builder
199+
for _, instanceId := range instanceIds {
200+
builder.WriteString(*instanceId)
201+
builder.WriteString(",")
202+
}
203+
return Halt(state, terminateErr, fmt.Sprintf("Failed to terminate instance %s may need to delete it manually", builder.String()))
198204
}
199-
return Halt(state, terminateErr, fmt.Sprintf("Failed to terminate instance %s may need to delete it manually", builder.String()))
200205
}
201206
}
202207
}
@@ -268,7 +273,7 @@ func (s *stepRunInstance) CreateCvmInstance(ctx context.Context, state multistep
268273
vpcId := state.Get("vpc_id").(string)
269274
Say(state,
270275
fmt.Sprintf("instance-type: %s, subnet-id: %s, zone: %s",
271-
s.InstanceType, *subnet.SubnetId, *subnet.Zone,
276+
*req.InstanceType, *subnet.SubnetId, *subnet.Zone,
272277
), "Try to create instance")
273278
req.VirtualPrivateCloud = &cvm.VirtualPrivateCloud{
274279
VpcId: &vpcId,

0 commit comments

Comments
 (0)