Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builder/tencentcloud/cvm/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion builder/tencentcloud/cvm/builder.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 14 additions & 4 deletions builder/tencentcloud/cvm/run_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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."))
Expand Down
12 changes: 9 additions & 3 deletions builder/tencentcloud/cvm/run_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
Expand Down
3 changes: 1 addition & 2 deletions builder/tencentcloud/cvm/step_check_source_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {}
131 changes: 40 additions & 91 deletions builder/tencentcloud/cvm/step_config_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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) {
Expand Down
67 changes: 36 additions & 31 deletions builder/tencentcloud/cvm/step_run_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// 移除了zoneid,由subnet step生成的subnet信息提供
type stepRunInstance struct {
InstanceType string
InstanceTypeCandidates []string
InstanceChargeType string
UserData string
UserDataFile string
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()))
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading