Add tencent cloud builder (#7135 & #6839)

This commit is contained in:
Adrien Delorme 2018-12-20 17:09:44 +01:00 committed by GitHub
parent e666b60d16
commit 7655d2efb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 15438 additions and 0 deletions

View File

@ -0,0 +1,127 @@
package cvm
import (
"fmt"
"os"
"github.com/hashicorp/packer/template/interpolate"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
)
type Region string
// below would be moved to tencentcloud sdk git repo
const (
Bangkok = Region("ap-bangkok")
Beijing = Region("ap-beijing")
Chengdu = Region("ap-chengdu")
Chongqing = Region("ap-chongqing")
Guangzhou = Region("ap-guangzhou")
GuangzhouOpen = Region("ap-guangzhou-open")
Hongkong = Region("ap-hongkong")
Mumbai = Region("ap-mumbai")
Seoul = Region("ap-seoul")
Shanghai = Region("ap-shanghai")
ShanghaiFsi = Region("ap-shanghai-fsi")
ShenzhenFsi = Region("ap-shenzhen-fsi")
Singapore = Region("ap-singapore")
Tokyo = Region("ap-tokyo")
Frankfurt = Region("eu-frankfurt")
Moscow = Region("eu-moscow")
Ashburn = Region("na-ashburn")
Siliconvalley = Region("na-siliconvalley")
Toronto = Region("na-toronto")
)
var ValidRegions = []Region{
Bangkok, Beijing, Chengdu, Chongqing, Guangzhou, GuangzhouOpen, Hongkong, Shanghai,
ShanghaiFsi, ShenzhenFsi,
Mumbai, Seoul, Singapore, Tokyo, Moscow,
Frankfurt, Ashburn, Siliconvalley, Toronto,
}
type TencentCloudAccessConfig struct {
SecretId string `mapstructure:"secret_id"`
SecretKey string `mapstructure:"secret_key"`
Region string `mapstructure:"region"`
Zone string `mapstructure:"zone"`
SkipValidation bool `mapstructure:"skip_region_validation"`
}
func (cf *TencentCloudAccessConfig) Client() (*cvm.Client, *vpc.Client, error) {
var (
err error
cvm_client *cvm.Client
vpc_client *vpc.Client
resp *cvm.DescribeZonesResponse
)
if err = cf.validateRegion(); err != nil {
return nil, nil, err
}
credential := common.NewCredential(
cf.SecretId, cf.SecretKey)
cpf := profile.NewClientProfile()
if cvm_client, err = cvm.NewClient(credential, cf.Region, cpf); err != nil {
return nil, nil, err
}
if vpc_client, err = vpc.NewClient(credential, cf.Region, cpf); err != nil {
return nil, nil, err
}
if resp, err = cvm_client.DescribeZones(nil); err != nil {
return nil, nil, err
}
if cf.Zone != "" {
for _, zone := range resp.Response.ZoneSet {
if cf.Zone == *zone.Zone {
return cvm_client, vpc_client, nil
}
}
return nil, nil, fmt.Errorf("unknown zone: %s", cf.Zone)
} else {
return nil, nil, fmt.Errorf("zone must be set")
}
}
func (cf *TencentCloudAccessConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
if err := cf.Config(); err != nil {
errs = append(errs, err)
}
if cf.Region == "" {
errs = append(errs, fmt.Errorf("region must be set"))
} else if !cf.SkipValidation {
if err := cf.validateRegion(); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errs
}
return nil
}
func (cf *TencentCloudAccessConfig) Config() error {
if cf.SecretId == "" {
cf.SecretId = os.Getenv("TENCENTCLOUD_SECRET_ID")
}
if cf.SecretKey == "" {
cf.SecretKey = os.Getenv("TENCENTCLOUD_SECRET_KEY")
}
if cf.SecretId == "" || cf.SecretKey == "" {
return fmt.Errorf("TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY must be set")
}
return nil
}
func (cf *TencentCloudAccessConfig) validateRegion() error {
for _, valid := range ValidRegions {
if valid == Region(cf.Region) {
return nil
}
}
return fmt.Errorf("unknown region: %s", cf.Region)
}

View File

@ -0,0 +1,31 @@
package cvm
import (
"testing"
)
func TestTencentCloudAccessConfig_Prepare(t *testing.T) {
cf := TencentCloudAccessConfig{
SecretId: "secret-id",
SecretKey: "secret-key",
}
if err := cf.Prepare(nil); err == nil {
t.Fatal("should raise error: region not set")
}
cf.Region = "ap-guangzhou"
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't raise error: %v", err)
}
cf.Region = "unknown-region"
if err := cf.Prepare(nil); err == nil {
t.Fatal("should raise error: unknown region")
}
cf.SkipValidation = true
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't raise error: %v", err)
}
}

View File

@ -0,0 +1,124 @@
package cvm
import (
"fmt"
"log"
"sort"
"strings"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type Artifact struct {
TencentCloudImages map[string]string
BuilderIdValue string
Client *cvm.Client
}
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*Artifact) Files() []string {
return nil
}
func (a *Artifact) Id() string {
parts := make([]string, 0, len(a.TencentCloudImages))
for region, imageId := range a.TencentCloudImages {
parts = append(parts, fmt.Sprintf("%s:%s", region, imageId))
}
sort.Strings(parts)
return strings.Join(parts, ",")
}
func (a *Artifact) String() string {
parts := make([]string, 0, len(a.TencentCloudImages))
for region, imageId := range a.TencentCloudImages {
parts = append(parts, fmt.Sprintf("%s: %s", region, imageId))
}
sort.Strings(parts)
return fmt.Sprintf("Tencentcloud images(%s) were created:\n\n", strings.Join(parts, "\n"))
}
func (a *Artifact) State(name string) interface{} {
switch name {
case "atlas.artifact.metadata":
return a.stateAtlasMetadata()
default:
return nil
}
}
func (a *Artifact) Destroy() error {
errors := make([]error, 0)
for region, imageId := range a.TencentCloudImages {
log.Printf("Delete tencentcloud image ID(%s) from region(%s)", imageId, region)
describeReq := cvm.NewDescribeImagesRequest()
describeReq.ImageIds = []*string{&imageId}
describeResp, err := a.Client.DescribeImages(describeReq)
if err != nil {
errors = append(errors, err)
continue
}
if *describeResp.Response.TotalCount == 0 {
errors = append(errors, fmt.Errorf(
"describe images failed, region(%s) ImageId(%s)", region, imageId))
}
describeShareReq := cvm.NewDescribeImageSharePermissionRequest()
describeShareReq.ImageId = &imageId
describeShareResp, err := a.Client.DescribeImageSharePermission(describeShareReq)
var shareAccountIds []*string = nil
if err != nil {
errors = append(errors, err)
} else {
for _, sharePermission := range describeShareResp.Response.SharePermissionSet {
shareAccountIds = append(shareAccountIds, sharePermission.AccountId)
}
}
if shareAccountIds != nil && len(shareAccountIds) != 0 {
cancelShareReq := cvm.NewModifyImageSharePermissionRequest()
cancelShareReq.ImageId = &imageId
cancelShareReq.AccountIds = shareAccountIds
CANCEL := "CANCEL"
cancelShareReq.Permission = &CANCEL
_, err := a.Client.ModifyImageSharePermission(cancelShareReq)
if err != nil {
errors = append(errors, err)
}
}
deleteReq := cvm.NewDeleteImagesRequest()
deleteReq.ImageIds = []*string{&imageId}
_, err = a.Client.DeleteImages(deleteReq)
if err != nil {
errors = append(errors, err)
}
}
if len(errors) == 1 {
return errors[0]
} else if len(errors) > 1 {
return &packer.MultiError{Errors: errors}
} else {
return nil
}
}
func (a *Artifact) stateAtlasMetadata() interface{} {
metadata := make(map[string]string)
for region, imageId := range a.TencentCloudImages {
k := fmt.Sprintf("region.%s", region)
metadata[k] = imageId
}
return metadata
}

View File

@ -0,0 +1,150 @@
package cvm
import (
"fmt"
"log"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
const BuilderId = "tencent.cloud"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
TencentCloudAccessConfig `mapstructure:",squash"`
TencentCloudImageConfig `mapstructure:",squash"`
TencentCloudRunConfig `mapstructure:",squash"`
ctx interpolate.Context
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...)
b.config.ctx.EnableEnv = true
if err != nil {
return nil, err
}
// Accumulate any errors
var errs *packer.MultiError
errs = packer.MultiErrorAppend(errs, b.config.TencentCloudAccessConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.TencentCloudImageConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.TencentCloudRunConfig.Prepare(&b.config.ctx)...)
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
packer.LogSecretFilter.Set(b.config.SecretId, b.config.SecretKey)
log.Println(b.config)
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
cvmClient, vpcClient, err := b.config.Client()
if err != nil {
return nil, err
}
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("cvm_client", cvmClient)
state.Put("vpc_client", vpcClient)
state.Put("hook", hook)
state.Put("ui", ui)
var steps []multistep.Step
// Build the steps
steps = []multistep.Step{
&stepCheckSourceImage{b.config.SourceImageId},
&stepConfigKeyPair{
Debug: b.config.PackerDebug,
Comm: &b.config.Comm,
DebugKeyPath: fmt.Sprintf("cvm_%s.pem", b.config.PackerBuildName),
},
&stepConfigVPC{
VpcId: b.config.VpcId,
CidrBlock: b.config.CidrBlock,
VpcName: b.config.VpcName,
},
&stepConfigSubnet{
SubnetId: b.config.SubnetId,
SubnetCidrBlock: b.config.SubnectCidrBlock,
SubnetName: b.config.SubnetName,
Zone: b.config.Zone,
},
&stepConfigSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SecurityGroupName: b.config.SecurityGroupName,
Description: "a simple security group",
},
&stepRunInstance{
InstanceType: b.config.InstanceType,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
ZoneId: b.config.Zone,
InstanceName: b.config.InstanceName,
DiskType: b.config.DiskType,
DiskSize: b.config.DiskSize,
HostName: b.config.HostName,
InternetMaxBandwidthOut: b.config.InternetMaxBandwidthOut,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
},
&communicator.StepConnect{
Config: &b.config.TencentCloudRunConfig.Comm,
SSHConfig: b.config.TencentCloudRunConfig.Comm.SSHConfigFunc(),
Host: SSHHost(b.config.AssociatePublicIpAddress),
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.TencentCloudRunConfig.Comm},
&stepCreateImage{},
&stepShareImage{b.config.ImageShareAccounts},
&stepCopyImage{
DesinationRegions: b.config.ImageCopyRegions,
SourceRegion: b.config.Region,
},
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
if _, ok := state.GetOk("image"); !ok {
return nil, nil
}
artifact := &Artifact{
TencentCloudImages: state.Get("tencentcloudimages").(map[string]string),
BuilderIdValue: BuilderId,
Client: cvmClient,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}

View File

@ -0,0 +1,103 @@
package cvm
import (
"fmt"
"regexp"
"time"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
func CheckResourceIdFormat(resource string, id string) bool {
regex := regexp.MustCompile(fmt.Sprintf("%s-[0-9a-z]{8}$", resource))
if !regex.MatchString(id) {
return false
}
return true
}
func MessageClean(state multistep.StateBag, module string) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
ui := state.Get("ui").(packer.Ui)
if cancelled || halted {
ui.Say(fmt.Sprintf("Deleting %s because of cancellation or error...", module))
} else {
ui.Say(fmt.Sprintf("Cleaning up '%s'", module))
}
}
const DefaultWaitForInterval = 5
func WaitForInstance(client *cvm.Client, instanceId string, status string, timeout int) error {
req := cvm.NewDescribeInstancesRequest()
req.InstanceIds = []*string{&instanceId}
for {
resp, err := client.DescribeInstances(req)
if err != nil {
return err
}
if *resp.Response.TotalCount == 0 {
return fmt.Errorf("instance(%s) not exist", instanceId)
}
if *resp.Response.InstanceSet[0].InstanceState == status {
break
}
time.Sleep(DefaultWaitForInterval * time.Second)
timeout = timeout - DefaultWaitForInterval
if timeout <= 0 {
return fmt.Errorf("wait instance(%s) status(%s) timeout", instanceId, status)
}
}
return nil
}
func WaitForImageReady(client *cvm.Client, imageName string, status string, timeout int) error {
req := cvm.NewDescribeImagesRequest()
FILTER_IMAGE_NAME := "image-name"
req.Filters = []*cvm.Filter{
{
Name: &FILTER_IMAGE_NAME,
Values: []*string{&imageName},
},
}
for {
resp, err := client.DescribeImages(req)
if err != nil {
return err
}
find := false
for _, image := range resp.Response.ImageSet {
if *image.ImageName == imageName && *image.ImageState == status {
find = true
break
}
}
if find {
break
}
time.Sleep(DefaultWaitForInterval * time.Second)
timeout = timeout - DefaultWaitForInterval
if timeout <= 0 {
return fmt.Errorf("wait image(%s) ready timeout", imageName)
}
}
return nil
}
// SSHHost returns a function that can be given to the SSH communicator
func SSHHost(pubilcIp bool) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
instance := state.Get("instance").(*cvm.Instance)
if pubilcIp {
return *instance.PublicIpAddresses[0], nil
} else {
return *instance.PrivateIpAddresses[0], nil
}
}
}

View File

@ -0,0 +1,74 @@
package cvm
import (
"fmt"
"regexp"
"github.com/hashicorp/packer/template/interpolate"
)
type TencentCloudImageConfig struct {
ImageName string `mapstructure:"image_name"`
ImageDescription string `mapstructure:"image_description"`
Reboot bool `mapstructure:"reboot"`
ForcePoweroff bool `mapstructure:"force_poweroff"`
Sysprep bool `mapstructure:"sysprep"`
ImageForceDelete bool `mapstructure:"image_force_delete"`
ImageCopyRegions []string `mapstructure:"image_copy_regions"`
ImageShareAccounts []string `mapstructure:"image_share_accounts"`
SkipValidation bool `mapstructure:"skip_region_validation"`
}
func (cf *TencentCloudImageConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
cf.ForcePoweroff = true
if cf.ImageName == "" {
errs = append(errs, fmt.Errorf("image_name must be set"))
} else if len(cf.ImageName) > 20 {
errs = append(errs, fmt.Errorf("image_num length should not exceed 20 characters"))
} else {
regex := regexp.MustCompile("^[0-9a-zA-Z\\-]+$")
if !regex.MatchString(cf.ImageName) {
errs = append(errs, fmt.Errorf("image_name can only be composed of letters, numbers and minus sign"))
}
}
if len(cf.ImageDescription) > 60 {
errs = append(errs, fmt.Errorf("image_description length should not exceed 60 characters"))
}
if len(cf.ImageCopyRegions) > 0 {
regionSet := make(map[string]struct{})
regions := make([]string, 0, len(cf.ImageCopyRegions))
for _, region := range cf.ImageCopyRegions {
if _, ok := regionSet[region]; ok {
continue
}
regionSet[region] = struct{}{}
if !cf.SkipValidation {
if err := validRegion(region); err != nil {
errs = append(errs, err)
continue
}
}
regions = append(regions, region)
}
cf.ImageCopyRegions = regions
}
if len(errs) > 0 {
return errs
}
return nil
}
func validRegion(region string) error {
for _, valid := range ValidRegions {
if Region(region) == valid {
return nil
}
}
return fmt.Errorf("unknown region: %s", region)
}

View File

@ -0,0 +1,35 @@
package cvm
import "testing"
func TestTencentCloudImageConfig_Prepare(t *testing.T) {
cf := &TencentCloudImageConfig{
ImageName: "foo",
}
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %v", err)
}
cf.ImageName = "foo:"
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have error")
}
cf.ImageName = "foo"
cf.ImageCopyRegions = []string{"ap-guangzhou", "ap-hongkong"}
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %v", err)
}
cf.ImageCopyRegions = []string{"unknown"}
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have err")
}
cf.SkipValidation = true
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err:%v", err)
}
}

View File

@ -0,0 +1,133 @@
package cvm
import (
"fmt"
"os"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/template/interpolate"
"github.com/pkg/errors"
)
type TencentCloudRunConfig struct {
AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"`
SourceImageId string `mapstructure:"source_image_id"`
InstanceType string `mapstructure:"instance_type"`
InstanceName string `mapstructure:"instance_name"`
DiskType string `mapstructure:"disk_type"`
DiskSize int64 `mapstructure:"disk_size"`
VpcId string `mapstructure:"vpc_id"`
VpcName string `mapstructure:"vpc_name"`
VpcIp string `mapstructure:"vpc_ip"`
SubnetId string `mapstructure:"subnet_id"`
SubnetName string `mapstructure:"subnet_name"`
CidrBlock string `mapstructure:"cidr_block"` // 10.0.0.0/16(default), 172.16.0.0/12, 192.168.0.0/16
SubnectCidrBlock string `mapstructure:"subnect_cidr_block"`
InternetChargeType string `mapstructure:"internet_charge_type"`
InternetMaxBandwidthOut int64 `mapstructure:"internet_max_bandwidth_out"`
SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupName string `mapstructure:"security_group_name"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
HostName string `mapstructure:"host_name"`
// Communicator settings
Comm communicator.Config `mapstructure:",squash"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
}
var ValidCBSType = []string{
"LOCAL_BASIC", "LOCAL_SSD", "CLOUD_BASIC", "CLOUD_SSD", "CLOUD_PREMIUM",
}
func (cf *TencentCloudRunConfig) Prepare(ctx *interpolate.Context) []error {
if cf.Comm.SSHKeyPairName == "" && cf.Comm.SSHTemporaryKeyPairName == "" &&
cf.Comm.SSHPrivateKeyFile == "" && cf.Comm.SSHPassword == "" && cf.Comm.WinRMPassword == "" {
//tencentcloud support key pair name length max to 25
cf.Comm.SSHTemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()[:8])
}
errs := cf.Comm.Prepare(ctx)
if cf.SourceImageId == "" {
errs = append(errs, errors.New("source_image_id must be specified"))
}
if !CheckResourceIdFormat("img", cf.SourceImageId) {
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.UserData != "" && cf.UserDataFile != "" {
errs = append(errs, errors.New("only one of user_data or user_data_file can be specified"))
} else if cf.UserDataFile != "" {
if _, err := os.Stat(cf.UserDataFile); err != nil {
errs = append(errs, errors.New("user_data_file not exist"))
}
}
if (cf.VpcId != "" || cf.CidrBlock != "") && cf.SubnetId == "" && cf.SubnectCidrBlock == "" {
errs = append(errs, errors.New("if vpc cidr_block is specified, then "+
"subnet_cidr_block must also be specified."))
}
if cf.VpcId == "" {
if cf.VpcName == "" {
cf.VpcName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
if cf.CidrBlock == "" {
cf.CidrBlock = "10.0.0.0/16"
}
if cf.SubnetId != "" {
errs = append(errs, errors.New("can't set subnet_id without set vpc_id"))
}
}
if cf.SubnetId == "" {
if cf.SubnetName == "" {
cf.SubnetName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
if cf.SubnectCidrBlock == "" {
cf.SubnectCidrBlock = "10.0.8.0/24"
}
}
if cf.SecurityGroupId == "" && cf.SecurityGroupName == "" {
cf.SecurityGroupName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
if cf.DiskType != "" && !checkDiskType(cf.DiskType) {
errs = append(errs, errors.New(fmt.Sprintf("specified disk_type(%s) is invalid", cf.DiskType)))
} else if cf.DiskType == "" {
cf.DiskType = "CLOUD_BASIC"
}
if cf.DiskSize <= 0 {
cf.DiskSize = 50
}
if cf.AssociatePublicIpAddress && cf.InternetMaxBandwidthOut <= 0 {
cf.InternetMaxBandwidthOut = 1
}
if cf.InstanceName == "" {
cf.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if cf.HostName == "" {
cf.HostName = cf.InstanceName[:15]
}
return errs
}
func checkDiskType(diskType string) bool {
for _, valid := range ValidCBSType {
if valid == diskType {
return true
}
}
return false
}

View File

@ -0,0 +1,127 @@
package cvm
import (
"github.com/hashicorp/packer/helper/communicator"
"io/ioutil"
"os"
"testing"
)
func testConfig() *TencentCloudRunConfig {
return &TencentCloudRunConfig{
SourceImageId: "img-qwer1234",
InstanceType: "S3.SMALL2",
Comm: communicator.Config{
SSHUsername: "tencentcloud",
},
}
}
func TestTencentCloudRunConfig_Prepare(t *testing.T) {
cf := testConfig()
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %v", err)
}
cf.InstanceType = ""
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have err")
}
cf.InstanceType = "S3.SMALL2"
cf.SourceImageId = ""
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have err")
}
cf.SourceImageId = "img-qwer1234"
cf.Comm.SSHPort = 0
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %v", err)
}
if cf.Comm.SSHPort != 22 {
t.Fatalf("invalid ssh port value: %v", cf.Comm.SSHPort)
}
cf.Comm.SSHPort = 44
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %v", err)
}
if cf.Comm.SSHPort != 44 {
t.Fatalf("invalid ssh port value: %v", cf.Comm.SSHPort)
}
}
func TestTencentCloudRunConfigPrepare_UserData(t *testing.T) {
cf := testConfig()
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("new temp file failed: %v", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
cf.UserData = "text user_data"
cf.UserDataFile = tf.Name()
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have error")
}
}
func TestTencentCloudRunConfigPrepare_UserDataFile(t *testing.T) {
cf := testConfig()
cf.UserDataFile = "not-exist-file"
if err := cf.Prepare(nil); err == nil {
t.Fatal("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("new temp file failed: %v", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
cf.UserDataFile = tf.Name()
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have error: %v", err)
}
}
func TestTencentCloudRunConfigPrepare_TemporaryKeyPairName(t *testing.T) {
cf := testConfig()
cf.Comm.SSHTemporaryKeyPairName = ""
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have error: %v", err)
}
if cf.Comm.SSHTemporaryKeyPairName == "" {
t.Fatal("invalid ssh key pair value")
}
cf.Comm.SSHTemporaryKeyPairName = "ssh-key-123"
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have error: %v", err)
}
if cf.Comm.SSHTemporaryKeyPairName != "ssh-key-123" {
t.Fatalf("invalid ssh key pair value: %v", cf.Comm.SSHTemporaryKeyPairName)
}
}
func TestTencentCloudRunConfigPrepare_SSHPrivateIp(t *testing.T) {
cf := testConfig()
if cf.SSHPrivateIp != false {
t.Fatalf("invalid ssh_private_ip value: %v", cf.SSHPrivateIp)
}
cf.SSHPrivateIp = true
if err := cf.Prepare(nil); err != nil {
t.Fatalf("shouldn't have error: %v", err)
}
if cf.SSHPrivateIp != true {
t.Fatalf("invalud ssh_private_ip value: %v", cf.SSHPrivateIp)
}
}

View File

@ -0,0 +1,45 @@
package cvm
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepCheckSourceImage struct {
sourceImageId string
}
func (s *stepCheckSourceImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("cvm_client").(*cvm.Client)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
req := cvm.NewDescribeImagesRequest()
req.ImageIds = []*string{&config.SourceImageId}
req.InstanceType = &config.InstanceType
resp, err := client.DescribeImages(req)
if err != nil {
err := fmt.Errorf("querying image info failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if *resp.Response.TotalCount > 0 { // public image or private image.
state.Put("source_image", resp.Response.ImageSet[0])
ui.Message(fmt.Sprintf("Image found: %s", *resp.Response.ImageSet[0].ImageId))
return multistep.ActionContinue
}
// later market image will be included.
err = fmt.Errorf("no image founded under current instance_type(%s) restriction", config.InstanceType)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
func (s *stepCheckSourceImage) Cleanup(bag multistep.StateBag) {}

View File

@ -0,0 +1,132 @@
package cvm
import (
"context"
"fmt"
"io/ioutil"
"os"
"runtime"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepConfigKeyPair struct {
Debug bool
Comm *communicator.Config
DebugKeyPath string
keyID string
}
func (s *stepConfigKeyPair) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.Comm.SSHPrivateKeyFile != "" {
ui.Say("Using existing SSH private key")
privateKeyBytes, err := ioutil.ReadFile(s.Comm.SSHPrivateKeyFile)
if err != nil {
state.Put("error", fmt.Errorf(
"loading configured private key file failed: %s", err))
return multistep.ActionHalt
}
s.Comm.SSHPrivateKey = privateKeyBytes
return multistep.ActionContinue
}
if s.Comm.SSHAgentAuth && s.Comm.SSHKeyPairName == "" {
ui.Say("Using SSH Agent with key pair in source image")
return multistep.ActionContinue
}
if s.Comm.SSHAgentAuth && s.Comm.SSHKeyPairName != "" {
ui.Say(fmt.Sprintf("Using SSH Agent for existing key pair %s", s.Comm.SSHKeyPairName))
return multistep.ActionContinue
}
if s.Comm.SSHTemporaryKeyPairName == "" {
ui.Say("Not using temporary keypair")
s.Comm.SSHKeyPairName = ""
return multistep.ActionContinue
}
client := state.Get("cvm_client").(*cvm.Client)
ui.Say(fmt.Sprintf("Creating temporary keypair: %s", s.Comm.SSHTemporaryKeyPairName))
req := cvm.NewCreateKeyPairRequest()
req.KeyName = &s.Comm.SSHTemporaryKeyPairName
defaultProjectId := int64(0)
req.ProjectId = &defaultProjectId
resp, err := client.CreateKeyPair(req)
if err != nil {
state.Put("error", fmt.Errorf("creating temporary keypair failed: %s", err.Error()))
return multistep.ActionHalt
}
// set keyId to delete when Cleanup
s.keyID = *resp.Response.KeyPair.KeyId
s.Comm.SSHKeyPairName = *resp.Response.KeyPair.KeyId
s.Comm.SSHPrivateKey = []byte(*resp.Response.KeyPair.PrivateKey)
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("creating debug key file failed:%s", err.Error()))
return multistep.ActionHalt
}
defer f.Close()
if _, err := f.Write([]byte(*resp.Response.KeyPair.PrivateKey)); err != nil {
state.Put("error", fmt.Errorf("writing debug key file failed:%s", err.Error()))
return multistep.ActionHalt
}
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
state.Put("error", fmt.Errorf("setting debug key file's permission failed:%s", err.Error()))
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepConfigKeyPair) Cleanup(state multistep.StateBag) {
if s.Comm.SSHPrivateKeyFile != "" || (s.Comm.SSHKeyPairName == "" && s.keyID == "") {
return
}
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting temporary keypair...")
req := cvm.NewDeleteKeyPairsRequest()
req.KeyIds = []*string{&s.keyID}
err := common.Retry(5, 5, 60, func(u uint) (bool, error) {
_, err := client.DeleteKeyPairs(req)
if err == nil {
return true, nil
}
if strings.Index(err.Error(), "NotSupported") != -1 {
return false, nil
} else {
return false, err
}
})
if err != nil {
ui.Error(fmt.Sprintf(
"delete keypair failed, please delete it manually, keyId: %s, err: %s", s.keyID, err.Error()))
}
if s.Debug {
if err := os.Remove(s.DebugKeyPath); err != nil {
ui.Error(fmt.Sprintf("delete debug key file %s failed: %s", s.DebugKeyPath, err.Error()))
}
}
}

View File

@ -0,0 +1,127 @@
package cvm
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/pkg/errors"
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
)
type stepConfigSecurityGroup struct {
SecurityGroupId string
SecurityGroupName string
Description string
isCreate bool
}
func (s *stepConfigSecurityGroup) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
if len(s.SecurityGroupId) != 0 { // use existing security group
req := vpc.NewDescribeSecurityGroupsRequest()
req.SecurityGroupIds = []*string{&s.SecurityGroupId}
resp, err := vpcClient.DescribeSecurityGroups(req)
if err != nil {
ui.Error(fmt.Sprintf("query security group failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
if *resp.Response.TotalCount > 0 {
state.Put("security_group_id", s.SecurityGroupId)
s.isCreate = false
return multistep.ActionContinue
}
message := fmt.Sprintf("the specified security group(%s) does not exist", s.SecurityGroupId)
ui.Error(message)
state.Put("error", errors.New(message))
return multistep.ActionHalt
}
// create a new security group
req := vpc.NewCreateSecurityGroupRequest()
req.GroupName = &s.SecurityGroupName
req.GroupDescription = &s.Description
resp, err := vpcClient.CreateSecurityGroup(req)
if err != nil {
ui.Error(fmt.Sprintf("create security group failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
s.SecurityGroupId = *resp.Response.SecurityGroup.SecurityGroupId
state.Put("security_group_id", s.SecurityGroupId)
s.isCreate = true
// bind security group ingress police
pReq := vpc.NewCreateSecurityGroupPoliciesRequest()
ACCEPT := "ACCEPT"
DEFAULT_CIDR := "0.0.0.0/0"
pReq.SecurityGroupId = &s.SecurityGroupId
pReq.SecurityGroupPolicySet = &vpc.SecurityGroupPolicySet{
Ingress: []*vpc.SecurityGroupPolicy{
{
CidrBlock: &DEFAULT_CIDR,
Action: &ACCEPT,
},
},
}
_, err = vpcClient.CreateSecurityGroupPolicies(pReq)
if err != nil {
ui.Error(fmt.Sprintf("bind security group police failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
// bind security group engress police
pReq = vpc.NewCreateSecurityGroupPoliciesRequest()
pReq.SecurityGroupId = &s.SecurityGroupId
pReq.SecurityGroupPolicySet = &vpc.SecurityGroupPolicySet{
Egress: []*vpc.SecurityGroupPolicy{
{
CidrBlock: &DEFAULT_CIDR,
Action: &ACCEPT,
},
},
}
_, err = vpcClient.CreateSecurityGroupPolicies(pReq)
if err != nil {
ui.Error(fmt.Sprintf("bind security group police failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepConfigSecurityGroup) Cleanup(state multistep.StateBag) {
if !s.isCreate {
return
}
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
MessageClean(state, "VPC")
req := vpc.NewDeleteSecurityGroupRequest()
req.SecurityGroupId = &s.SecurityGroupId
err := common.Retry(5, 5, 60, func(u uint) (bool, error) {
_, err := vpcClient.DeleteSecurityGroup(req)
if err == nil {
return true, nil
}
if strings.Index(err.Error(), "ResourceInUse") != -1 {
return false, nil
} else {
return false, err
}
})
if err != nil {
ui.Error(fmt.Sprintf("delete security group(%s) failed: %s, you need to delete it by hand",
s.SecurityGroupId, err.Error()))
return
}
}

View File

@ -0,0 +1,103 @@
package cvm
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/pkg/errors"
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
)
type stepConfigSubnet struct {
SubnetId string
SubnetCidrBlock string
SubnetName string
Zone string
isCreate bool
}
func (s *stepConfigSubnet) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
vpcId := state.Get("vpc_id").(string)
if len(s.SubnetId) != 0 { // exist subnet
ui.Say(fmt.Sprintf("Trying to use existing subnet(%s)", s.SubnetId))
req := vpc.NewDescribeSubnetsRequest()
req.SubnetIds = []*string{&s.SubnetId}
resp, err := vpcClient.DescribeSubnets(req)
if err != nil {
ui.Error(fmt.Sprintf("query subnet failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
if *resp.Response.TotalCount > 0 {
subnet0 := *resp.Response.SubnetSet[0]
if *subnet0.VpcId != vpcId {
message := fmt.Sprintf("the specified subnet(%s) does not belong to "+
"the specified vpc(%s)", s.SubnetId, vpcId)
ui.Error(message)
state.Put("error", errors.New(message))
return multistep.ActionHalt
}
state.Put("subnet_id", *subnet0.SubnetId)
s.isCreate = false
return multistep.ActionContinue
}
message := fmt.Sprintf("the specified subnet(%s) does not exist", s.SubnetId)
state.Put("error", errors.New(message))
ui.Error(message)
return multistep.ActionHalt
} else { // create a new subnet, tencentcloud create subnet api is synchronous, no need to wait for create.
ui.Say(fmt.Sprintf("Trying to create a new subnet"))
req := vpc.NewCreateSubnetRequest()
req.VpcId = &vpcId
req.SubnetName = &s.SubnetName
req.CidrBlock = &s.SubnetCidrBlock
req.Zone = &s.Zone
resp, err := vpcClient.CreateSubnet(req)
if err != nil {
ui.Error(fmt.Sprintf("create subnet failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
subnet0 := *resp.Response.Subnet
state.Put("subnet_id", *subnet0.SubnetId)
s.SubnetId = *subnet0.SubnetId
s.isCreate = true
return multistep.ActionContinue
}
}
func (s *stepConfigSubnet) Cleanup(state multistep.StateBag) {
if !s.isCreate {
return
}
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
MessageClean(state, "SUBNET")
req := vpc.NewDeleteSubnetRequest()
req.SubnetId = &s.SubnetId
err := common.Retry(5, 5, 60, func(u uint) (bool, error) {
_, err := vpcClient.DeleteSubnet(req)
if err == nil {
return true, nil
}
if strings.Index(err.Error(), "ResourceInUse") != -1 {
return false, nil
} else {
return false, err
}
})
if err != nil {
ui.Error(fmt.Sprintf("delete subnet(%s) failed: %s, you need to delete it by hand",
s.SubnetId, err.Error()))
return
}
}

View File

@ -0,0 +1,92 @@
package cvm
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/pkg/errors"
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
)
type stepConfigVPC struct {
VpcId string
CidrBlock string
VpcName string
isCreate bool
}
func (s *stepConfigVPC) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
if len(s.VpcId) != 0 { // exist vpc
ui.Say(fmt.Sprintf("Trying to use existing vpc(%s)", s.VpcId))
req := vpc.NewDescribeVpcsRequest()
req.VpcIds = []*string{&s.VpcId}
resp, err := vpcClient.DescribeVpcs(req)
if err != nil {
ui.Error(fmt.Sprintf("query vpc failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
if *resp.Response.TotalCount > 0 {
vpc0 := *resp.Response.VpcSet[0]
state.Put("vpc_id", *vpc0.VpcId)
s.isCreate = false
return multistep.ActionContinue
}
message := fmt.Sprintf("the specified vpc(%s) does not exist", s.VpcId)
state.Put("error", errors.New(message))
ui.Error(message)
return multistep.ActionHalt
} else { // create a new vpc, tencentcloud create vpc api is synchronous, no need to wait for create.
ui.Say(fmt.Sprintf("Trying to create a new vpc"))
req := vpc.NewCreateVpcRequest()
req.VpcName = &s.VpcName
req.CidrBlock = &s.CidrBlock
resp, err := vpcClient.CreateVpc(req)
if err != nil {
ui.Error(fmt.Sprintf("create vpc failed: %s", err.Error()))
state.Put("error", err)
return multistep.ActionHalt
}
vpc0 := *resp.Response.Vpc
state.Put("vpc_id", *vpc0.VpcId)
s.VpcId = *vpc0.VpcId
s.isCreate = true
return multistep.ActionContinue
}
}
func (s *stepConfigVPC) Cleanup(state multistep.StateBag) {
if !s.isCreate {
return
}
vpcClient := state.Get("vpc_client").(*vpc.Client)
ui := state.Get("ui").(packer.Ui)
MessageClean(state, "VPC")
req := vpc.NewDeleteVpcRequest()
req.VpcId = &s.VpcId
err := common.Retry(5, 5, 60, func(u uint) (bool, error) {
_, err := vpcClient.DeleteVpc(req)
if err == nil {
return true, nil
}
if strings.Index(err.Error(), "ResourceInUse") != -1 {
return false, nil
} else {
return false, err
}
})
if err != nil {
ui.Error(fmt.Sprintf("delete vpc(%s) failed: %s, you need to delete it by hand",
s.VpcId, err.Error()))
return
}
}

View File

@ -0,0 +1,47 @@
package cvm
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepCopyImage struct {
DesinationRegions []string
SourceRegion string
}
func (s *stepCopyImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
if len(s.DesinationRegions) == 0 || (len(s.DesinationRegions) == 1 && s.DesinationRegions[0] == s.SourceRegion) {
return multistep.ActionContinue
}
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
imageId := state.Get("image").(*cvm.Image).ImageId
req := cvm.NewSyncImagesRequest()
req.ImageIds = []*string{imageId}
copyRegions := make([]*string, 0, len(s.DesinationRegions))
for _, region := range s.DesinationRegions {
if region != s.SourceRegion {
copyRegions = append(copyRegions, &region)
}
}
req.DestinationRegions = copyRegions
_, err := client.SyncImages(req)
if err != nil {
state.Put("error", err)
ui.Error(fmt.Sprintf("copy image failed: %s", err.Error()))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCopyImage) Cleanup(state multistep.StateBag) {
// just do nothing
}

View File

@ -0,0 +1,116 @@
package cvm
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepCreateImage struct {
imageId string
}
func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*cvm.Instance)
ui.Say(fmt.Sprintf("Creating image %s", config.ImageName))
req := cvm.NewCreateImageRequest()
req.ImageName = &config.ImageName
req.ImageDescription = &config.ImageDescription
req.InstanceId = instance.InstanceId
True := "True"
False := "False"
if config.ForcePoweroff {
req.ForcePoweroff = &True
} else {
req.ForcePoweroff = &False
}
if config.Reboot {
req.Reboot = &True
} else {
req.Reboot = &False
}
if config.Sysprep {
req.Sysprep = &True
} else {
req.Sysprep = &False
}
_, err := client.CreateImage(req)
if err != nil {
err := fmt.Errorf("create image failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
err = WaitForImageReady(client, config.ImageName, "NORMAL", 3600)
if err != nil {
err := fmt.Errorf("create image failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
describeReq := cvm.NewDescribeImagesRequest()
FILTER_IMAGE_NAME := "image-name"
describeReq.Filters = []*cvm.Filter{
{
Name: &FILTER_IMAGE_NAME,
Values: []*string{&config.ImageName},
},
}
describeResp, err := client.DescribeImages(describeReq)
if err != nil {
err := fmt.Errorf("wait image ready failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if *describeResp.Response.TotalCount == 0 {
err := fmt.Errorf("create image(%s) failed", config.ImageName)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.imageId = *describeResp.Response.ImageSet[0].ImageId
state.Put("image", describeResp.Response.ImageSet[0])
tencentCloudImages := make(map[string]string)
tencentCloudImages[config.Region] = s.imageId
state.Put("tencentcloudimages", tencentCloudImages)
return multistep.ActionContinue
}
func (s *stepCreateImage) Cleanup(state multistep.StateBag) {
if s.imageId == "" {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if !cancelled && !halted {
return
}
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
ui.Say("Delete image because of cancellation or error...")
req := cvm.NewDeleteImagesRequest()
req.ImageIds = []*string{&s.imageId}
_, err := client.DeleteImages(req)
if err != nil {
ui.Error(fmt.Sprintf("delete image(%s) failed", s.imageId))
}
}

View File

@ -0,0 +1,21 @@
package cvm
import (
"context"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepPreValidate struct {
DestImageName string
ForceDelete bool
}
func (s *stepPreValidate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("TencentCloud support images with same name, image_name check is not required.")
return multistep.ActionContinue
}
func (s *stepPreValidate) Cleanup(multistep.StateBag) {}

View File

@ -0,0 +1,158 @@
package cvm
import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"time"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepRunInstance struct {
InstanceType string
UserData string
UserDataFile string
instanceId string
ZoneId string
InstanceName string
DiskType string
DiskSize int64
HostName string
InternetMaxBandwidthOut int64
AssociatePublicIpAddress bool
}
func (s *stepRunInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("cvm_client").(*cvm.Client)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
source_image := state.Get("source_image").(*cvm.Image)
vpc_id := state.Get("vpc_id").(string)
subnet_id := state.Get("subnet_id").(string)
security_group_id := state.Get("security_group_id").(string)
password := config.Comm.SSHPassword
if password == "" && config.Comm.WinRMPassword != "" {
password = config.Comm.WinRMPassword
}
userData, err := s.getUserData(state)
if err != nil {
err := fmt.Errorf("get user_data failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating Instance.")
// config RunInstances parameters
POSTPAID_BY_HOUR := "POSTPAID_BY_HOUR"
req := cvm.NewRunInstancesRequest()
if s.ZoneId != "" {
req.Placement = &cvm.Placement{
Zone: &s.ZoneId,
}
}
req.ImageId = source_image.ImageId
req.InstanceChargeType = &POSTPAID_BY_HOUR
req.InstanceType = &s.InstanceType
req.SystemDisk = &cvm.SystemDisk{
DiskType: &s.DiskType,
DiskSize: &s.DiskSize,
}
req.VirtualPrivateCloud = &cvm.VirtualPrivateCloud{
VpcId: &vpc_id,
SubnetId: &subnet_id,
}
TRAFFIC_POSTPAID_BY_HOUR := "TRAFFIC_POSTPAID_BY_HOUR"
if s.AssociatePublicIpAddress {
req.InternetAccessible = &cvm.InternetAccessible{
InternetChargeType: &TRAFFIC_POSTPAID_BY_HOUR,
InternetMaxBandwidthOut: &s.InternetMaxBandwidthOut,
}
}
req.InstanceName = &s.InstanceName
loginSettings := cvm.LoginSettings{}
if password != "" {
loginSettings.Password = &password
}
if config.Comm.SSHKeyPairName != "" {
loginSettings.KeyIds = []*string{&config.Comm.SSHKeyPairName}
}
req.LoginSettings = &loginSettings
req.SecurityGroupIds = []*string{&security_group_id}
req.ClientToken = &s.InstanceName
req.HostName = &s.HostName
req.UserData = &userData
resp, err := client.RunInstances(req)
if err != nil {
err := fmt.Errorf("create instance failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(resp.Response.InstanceIdSet) != 1 {
err := fmt.Errorf("create instance failed: %d instance(s) created", len(resp.Response.InstanceIdSet))
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instanceId = *resp.Response.InstanceIdSet[0]
err = WaitForInstance(client, s.instanceId, "RUNNING", 1800)
if err != nil {
err := fmt.Errorf("wait instance launch failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
describeReq := cvm.NewDescribeInstancesRequest()
describeReq.InstanceIds = []*string{&s.instanceId}
describeResp, err := client.DescribeInstances(describeReq)
if err != nil {
err := fmt.Errorf("wait instance launch failed: %s", err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("instance", describeResp.Response.InstanceSet[0])
return multistep.ActionContinue
}
func (s *stepRunInstance) getUserData(state multistep.StateBag) (string, error) {
userData := s.UserData
if userData == "" && s.UserDataFile != "" {
data, err := ioutil.ReadFile(s.UserDataFile)
if err != nil {
return "", err
}
userData = string(data)
}
userData = base64.StdEncoding.EncodeToString([]byte(userData))
log.Printf(fmt.Sprintf("user_data: %s", userData))
return userData, nil
}
func (s *stepRunInstance) Cleanup(state multistep.StateBag) {
if s.instanceId == "" {
return
}
MessageClean(state, "instance")
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
req := cvm.NewTerminateInstancesRequest()
req.InstanceIds = []*string{&s.instanceId}
_, err := client.TerminateInstances(req)
// The binding relation between instance and vpc would last few minutes after
// instance terminate, we sleep here to give more time
time.Sleep(2 * time.Minute)
if err != nil {
ui.Error(fmt.Sprintf("terminate instance(%s) failed: %s", s.instanceId, err.Error()))
}
}

View File

@ -0,0 +1,68 @@
package cvm
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
)
type stepShareImage struct {
ShareAccounts []string
}
func (s *stepShareImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
if len(s.ShareAccounts) == 0 {
return multistep.ActionContinue
}
client := state.Get("cvm_client").(*cvm.Client)
ui := state.Get("ui").(packer.Ui)
imageId := state.Get("image").(*cvm.Image).ImageId
req := cvm.NewModifyImageSharePermissionRequest()
req.ImageId = imageId
SHARE := "SHARE"
req.Permission = &SHARE
accounts := make([]*string, 0, len(s.ShareAccounts))
for _, account := range s.ShareAccounts {
accounts = append(accounts, &account)
}
req.AccountIds = accounts
_, err := client.ModifyImageSharePermission(req)
if err != nil {
state.Put("error", err)
ui.Error(fmt.Sprintf("share image failed: %s", err.Error()))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepShareImage) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
ui := state.Get("ui").(packer.Ui)
client := state.Get("cvm_client").(*cvm.Client)
imageId := state.Get("image").(*cvm.Image).ImageId
ui.Say("Cancel share image due to action cancelled or halted.")
req := cvm.NewModifyImageSharePermissionRequest()
req.ImageId = imageId
CANCEL := "CANCEL"
req.Permission = &CANCEL
accounts := make([]*string, 0, len(s.ShareAccounts))
for _, account := range s.ShareAccounts {
accounts = append(accounts, &account)
}
req.AccountIds = accounts
_, err := client.ModifyImageSharePermission(req)
if err != nil {
ui.Error(fmt.Sprintf("Cancel share image failed: %s", err.Error()))
}
}
}

View File

@ -41,6 +41,7 @@ import (
profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks"
qemubuilder "github.com/hashicorp/packer/builder/qemu"
scalewaybuilder "github.com/hashicorp/packer/builder/scaleway"
tencentcloudbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
tritonbuilder "github.com/hashicorp/packer/builder/triton"
virtualboxisobuilder "github.com/hashicorp/packer/builder/virtualbox/iso"
virtualboxovfbuilder "github.com/hashicorp/packer/builder/virtualbox/ovf"
@ -113,6 +114,7 @@ var Builders = map[string]packer.Builder{
"profitbricks": new(profitbricksbuilder.Builder),
"qemu": new(qemubuilder.Builder),
"scaleway": new(scalewaybuilder.Builder),
"tencentcloud-cvm": new(tencentcloudbuilder.Builder),
"triton": new(tritonbuilder.Builder),
"virtualbox-iso": new(virtualboxisobuilder.Builder),
"virtualbox-ovf": new(virtualboxovfbuilder.Builder),

View File

@ -0,0 +1,26 @@
{
"variables": {
"secret_id": "{{env `TENCENTCLOUD_ACCESS_KEY`}}",
"secret_key": "{{env `TENCENTCLOUD_SECRET_KEY`}}"
},
"builders": [{
"type": "tencentcloud-cvm",
"secret_id": "{{user `secret_id`}}",
"secret_key": "{{user `secret_key`}}",
"region": "ap-guangzhou",
"zone": "ap-guangzhou-3",
"instance_type": "S3.SMALL1",
"source_image_id": "img-oikl1tzv",
"ssh_username" : "root",
"image_name": "packerTest2",
"packer_debug": true,
"associate_public_ip_address": true
}],
"provisioners": [{
"type": "shell",
"inline": [
"sleep 30",
"yum install redis.x86_64 -y"
]
}]
}

View File

@ -0,0 +1,35 @@
{
"variables": {
"secret_id": "{{env `TENCENTCLOUD_ACCESS_KEY`}}",
"secret_key": "{{env `TENCENTCLOUD_SECRET_KEY`}}"
},
"builders": [{
"type": "tencentcloud-cvm",
"secret_id": "{{user `secret_id`}}",
"secret_key": "{{user `secret_key`}}",
"region": "ap-guangzhou",
"zone": "ap-guangzhou-3",
"instance_type": "S3.SMALL1",
"source_image_id": "img-oikl1tzv",
"vpc_id": "vpc-gjusx3kd",
"subnet_id": "subnet-pfditepm",
"internet_max_bandwidth_out": 2,
"security_group_id": "sg-rypoiksl",
"ssh_username" : "root",
"image_name": "packerTest",
"host_name": "packerTest",
"associate_public_ip_address": true,
"image_description": "centosPacker",
"image_copy_regions": ["ap-beijing"]
}],
"provisioners": [{
"execute_command": "echo '{{user `ssh_pass`}}' | {{ .Vars }} sudo -S -E sh '{{ .Path }}'",
"inline": [
"yum update -y",
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
],
"inline_shebang": "/bin/sh -x",
"type": "shell",
"skip_clean": true
}]
}

1
go.mod
View File

@ -154,6 +154,7 @@ require (
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/testify v1.2.2
github.com/tencentcloud/tencentcloud-sdk-go v0.0.0-20181220135002-f1744d40d346
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
github.com/ugorji/go v0.0.0-20151218193438-646ae4a518c1

2
go.sum
View File

@ -345,6 +345,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpke
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tencentcloud/tencentcloud-sdk-go v0.0.0-20181220135002-f1744d40d346 h1:a014AaXz7AISMePv8xKRffUZZkr5z2XmSDf41gRV3+A=
github.com/tencentcloud/tencentcloud-sdk-go v0.0.0-20181220135002-f1744d40d346/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 h1:/Bsw4C+DEdqPjt8vAqaC9LAqpAQnaCQQqmolqq3S1T4=
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8=

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2017-2018 Tencent Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,88 @@
package common
import (
"log"
"net/http"
"time"
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
)
type Client struct {
region string
httpClient *http.Client
httpProfile *profile.HttpProfile
credential *Credential
signMethod string
debug bool
}
func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) {
if request.GetDomain() == "" {
domain := c.httpProfile.Endpoint
if domain == "" {
domain = tchttp.GetServiceDomain(request.GetService())
}
request.SetDomain(domain)
}
err = tchttp.ConstructParams(request)
if err != nil {
return
}
tchttp.CompleteCommonParams(request, c.GetRegion())
err = signRequest(request, c.credential, c.signMethod)
if err != nil {
return
}
httpRequest, err := http.NewRequest(request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader())
if err != nil {
return
}
if request.GetHttpMethod() == "POST" {
httpRequest.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
}
//log.Printf("[DEBUG] http request=%v", httpRequest)
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return err
}
err = tchttp.ParseFromHttpResponse(httpResponse, response)
return
}
func (c *Client) GetRegion() string {
return c.region
}
func (c *Client) Init(region string) *Client {
c.httpClient = &http.Client{}
c.region = region
c.signMethod = "HmacSHA256"
c.debug = false
log.SetFlags(log.LstdFlags | log.Lshortfile)
return c
}
func (c *Client) WithSecretId(secretId, secretKey string) *Client {
c.credential = NewCredential(secretId, secretKey)
return c
}
func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client {
c.signMethod = clientProfile.SignMethod
c.httpProfile = clientProfile.HttpProfile
c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second
return c
}
func (c *Client) WithSignatureMethod(method string) *Client {
c.signMethod = method
return c
}
func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) {
client = &Client{}
client.Init(region).WithSecretId(secretId, secretKey)
return
}

View File

@ -0,0 +1,40 @@
package common
type Credential struct {
SecretId string
SecretKey string
}
func NewCredential(secretId, secretKey string) *Credential {
return &Credential{
SecretId: secretId,
SecretKey: secretKey,
}
}
func (c *Credential) GetCredentialParams() map[string]string {
return map[string]string{
"SecretId": c.SecretId,
}
}
type TokenCredential struct {
SecretId string
SecretKey string
Token string
}
func NewTokenCredential(secretId, secretKey, token string) *TokenCredential {
return &TokenCredential{
SecretId: secretId,
SecretKey: secretKey,
Token: token,
}
}
func (c *TokenCredential) GetCredentialParams() map[string]string {
return map[string]string{
"SecretId": c.SecretId,
"Token": c.Token,
}
}

View File

@ -0,0 +1,35 @@
package errors
import (
"fmt"
)
type TencentCloudSDKError struct {
Code string
Message string
RequestId string
}
func (e *TencentCloudSDKError) Error() string {
return fmt.Sprintf("[TencentCloudSDKError] Code=%s, Message=%s, RequestId=%s", e.Code, e.Message, e.RequestId)
}
func NewTencentCloudSDKError(code, message, requestId string) error {
return &TencentCloudSDKError{
Code: code,
Message: message,
RequestId: requestId,
}
}
func (e *TencentCloudSDKError) GetCode() string {
return e.Code
}
func (e *TencentCloudSDKError) GetMessage() string {
return e.Message
}
func (e *TencentCloudSDKError) GetRequestId() string {
return e.RequestId
}

View File

@ -0,0 +1,231 @@
package common
import (
"io"
//"log"
"math/rand"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
const (
POST = "POST"
GET = "GET"
RootDomain = "tencentcloudapi.com"
Path = "/"
)
type Request interface {
GetAction() string
GetBodyReader() io.Reader
GetDomain() string
GetHttpMethod() string
GetParams() map[string]string
GetPath() string
GetService() string
GetUrl() string
GetVersion() string
SetDomain(string)
SetHttpMethod(string)
}
type BaseRequest struct {
httpMethod string
domain string
path string
params map[string]string
formParams map[string]string
service string
version string
action string
}
func (r *BaseRequest) GetAction() string {
return r.action
}
func (r *BaseRequest) GetHttpMethod() string {
return r.httpMethod
}
func (r *BaseRequest) GetParams() map[string]string {
return r.params
}
func (r *BaseRequest) GetPath() string {
return r.path
}
func (r *BaseRequest) GetDomain() string {
return r.domain
}
func (r *BaseRequest) SetDomain(domain string) {
r.domain = domain
}
func (r *BaseRequest) SetHttpMethod(method string) {
switch strings.ToUpper(method) {
case POST:
{
r.httpMethod = POST
}
case GET:
{
r.httpMethod = GET
}
default:
{
r.httpMethod = GET
}
}
}
func (r *BaseRequest) GetService() string {
return r.service
}
func (r *BaseRequest) GetUrl() string {
if r.httpMethod == GET {
return "https://" + r.domain + r.path + "?" + getUrlQueriesEncoded(r.params)
} else if r.httpMethod == POST {
return "https://" + r.domain + r.path
} else {
return ""
}
}
func (r *BaseRequest) GetVersion() string {
return r.version
}
func getUrlQueriesEncoded(params map[string]string) string {
values := url.Values{}
for key, value := range params {
if value != "" {
values.Add(key, value)
}
}
return values.Encode()
}
func (r *BaseRequest) GetBodyReader() io.Reader {
if r.httpMethod == POST {
s := getUrlQueriesEncoded(r.params)
//log.Printf("[DEBUG] body: %s", s)
return strings.NewReader(s)
} else {
return strings.NewReader("")
}
}
func (r *BaseRequest) Init() *BaseRequest {
r.httpMethod = GET
r.domain = ""
r.path = Path
r.params = make(map[string]string)
r.formParams = make(map[string]string)
return r
}
func (r *BaseRequest) WithApiInfo(service, version, action string) *BaseRequest {
r.service = service
r.version = version
r.action = action
return r
}
func GetServiceDomain(service string) (domain string) {
domain = service + "." + RootDomain
return
}
func CompleteCommonParams(request Request, region string) {
params := request.GetParams()
params["Region"] = region
if request.GetVersion() != "" {
params["Version"] = request.GetVersion()
}
params["Action"] = request.GetAction()
params["Timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
params["Nonce"] = strconv.Itoa(rand.Int())
params["RequestClient"] = "SDK_GO_3.0.26"
}
func ConstructParams(req Request) (err error) {
value := reflect.ValueOf(req).Elem()
err = flatStructure(value, req, "")
//log.Printf("[DEBUG] params=%s", req.GetParams())
return
}
func flatStructure(value reflect.Value, request Request, prefix string) (err error) {
//log.Printf("[DEBUG] reflect value: %v", value.Type())
valueType := value.Type()
for i := 0; i < valueType.NumField(); i++ {
tag := valueType.Field(i).Tag
nameTag, hasNameTag := tag.Lookup("name")
if !hasNameTag {
continue
}
field := value.Field(i)
kind := field.Kind()
if kind == reflect.Ptr && field.IsNil() {
continue
}
if kind == reflect.Ptr {
field = field.Elem()
kind = field.Kind()
}
key := prefix + nameTag
if kind == reflect.String {
s := field.String()
if s != "" {
request.GetParams()[key] = s
}
} else if kind == reflect.Bool {
request.GetParams()[key] = strconv.FormatBool(field.Bool())
} else if kind == reflect.Int || kind == reflect.Int64 {
request.GetParams()[key] = strconv.FormatInt(field.Int(), 10)
} else if kind == reflect.Uint || kind == reflect.Uint64 {
request.GetParams()[key] = strconv.FormatUint(field.Uint(), 10)
} else if kind == reflect.Float64 {
request.GetParams()[key] = strconv.FormatFloat(field.Float(), 'f', -1, 64)
} else if kind == reflect.Slice {
list := value.Field(i)
for j := 0; j < list.Len(); j++ {
vj := list.Index(j)
key := prefix + nameTag + "." + strconv.Itoa(j)
kind = vj.Kind()
if kind == reflect.Ptr && vj.IsNil() {
continue
}
if kind == reflect.Ptr {
vj = vj.Elem()
kind = vj.Kind()
}
if kind == reflect.String {
request.GetParams()[key] = vj.String()
} else if kind == reflect.Bool {
request.GetParams()[key] = strconv.FormatBool(vj.Bool())
} else if kind == reflect.Int || kind == reflect.Int64 {
request.GetParams()[key] = strconv.FormatInt(vj.Int(), 10)
} else if kind == reflect.Uint || kind == reflect.Uint64 {
request.GetParams()[key] = strconv.FormatUint(vj.Uint(), 10)
} else if kind == reflect.Float64 {
request.GetParams()[key] = strconv.FormatFloat(vj.Float(), 'f', -1, 64)
} else {
flatStructure(vj, request, key+".")
}
}
} else {
flatStructure(reflect.ValueOf(field.Interface()), request, prefix+nameTag+".")
}
}
return
}

View File

@ -0,0 +1,69 @@
package common
import (
"encoding/json"
"io/ioutil"
// "log"
"net/http"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
)
type Response interface {
ParseErrorFromHTTPResponse(body []byte) error
}
type BaseResponse struct {
}
type ErrorResponse struct {
Response struct {
Error struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"Error" omitempty`
RequestId string `json:"RequestId"`
} `json:"Response"`
}
type DeprecatedAPIErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
CodeDesc string `json:"codeDesc"`
}
func (r *BaseResponse) ParseErrorFromHTTPResponse(body []byte) (err error) {
resp := &ErrorResponse{}
err = json.Unmarshal(body, resp)
if err != nil {
return
}
if resp.Response.Error.Code != "" {
return errors.NewTencentCloudSDKError(resp.Response.Error.Code, resp.Response.Error.Message, resp.Response.RequestId)
}
deprecated := &DeprecatedAPIErrorResponse{}
err = json.Unmarshal(body, deprecated)
if err != nil {
return
}
if deprecated.Code != 0 {
return errors.NewTencentCloudSDKError(deprecated.CodeDesc, deprecated.Message, "")
}
return nil
}
func ParseFromHttpResponse(hr *http.Response, response Response) (err error) {
defer hr.Body.Close()
body, err := ioutil.ReadAll(hr.Body)
if err != nil {
return
}
//log.Printf("[DEBUG] Response Body=%s", body)
err = response.ParseErrorFromHTTPResponse(body)
if err != nil {
return
}
err = json.Unmarshal(body, &response)
return
}

View File

@ -0,0 +1,13 @@
package profile
type ClientProfile struct {
HttpProfile *HttpProfile
SignMethod string
}
func NewClientProfile() *ClientProfile {
return &ClientProfile{
HttpProfile: NewHttpProfile(),
SignMethod: "HmacSHA256",
}
}

View File

@ -0,0 +1,17 @@
package profile
type HttpProfile struct {
ReqMethod string
ReqTimeout int
Endpoint string
Protocol string
}
func NewHttpProfile() *HttpProfile {
return &HttpProfile{
ReqMethod: "POST",
ReqTimeout: 60,
Endpoint: "",
Protocol: "HTTPS",
}
}

View File

@ -0,0 +1,75 @@
package common
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"fmt"
"sort"
"strings"
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
)
const (
SHA256 = "HmacSHA256"
SHA1 = "HmacSHA1"
)
func Sign(s, secretKey, method string) string {
hashed := hmac.New(sha1.New, []byte(secretKey))
if method == SHA256 {
hashed = hmac.New(sha256.New, []byte(secretKey))
}
hashed.Write([]byte(s))
return base64.StdEncoding.EncodeToString(hashed.Sum(nil))
}
func signRequest(request tchttp.Request, credential *Credential, method string) (err error) {
if method != SHA256 {
method = SHA1
}
checkAuthParams(request, credential, method)
s := getStringToSign(request)
signature := Sign(s, credential.SecretKey, method)
request.GetParams()["Signature"] = signature
return
}
func checkAuthParams(request tchttp.Request, credential *Credential, method string) {
params := request.GetParams()
credentialParams := credential.GetCredentialParams()
for key, value := range credentialParams {
params[key] = value
}
params["SignatureMethod"] = method
delete(params, "Signature")
}
func getStringToSign(request tchttp.Request) string {
method := request.GetHttpMethod()
domain := request.GetDomain()
path := request.GetPath()
text := method + domain + path + "?"
params := request.GetParams()
// sort params
keys := make([]string, 0, len(params))
for k, _ := range params {
keys = append(keys, k)
}
sort.Strings(keys)
for i := range keys {
k := keys[i]
if params[k] == "" {
continue
}
text += fmt.Sprintf("%v=%v&", strings.Replace(k, "_", ".", -1), params[k])
}
text = text[:len(text)-1]
return text
}

View File

@ -0,0 +1,47 @@
package common
func IntPtr(v int) *int {
return &v
}
func Int64Ptr(v int64) *int64 {
return &v
}
func UintPtr(v uint) *uint {
return &v
}
func Uint64Ptr(v uint64) *uint64 {
return &v
}
func Float64Ptr(v float64) *float64 {
return &v
}
func StringPtr(v string) *string {
return &v
}
func StringValues(ptrs []*string) []string {
values := make([]string, len(ptrs))
for i := 0; i < len(ptrs); i++ {
if ptrs[i] != nil {
values[i] = *ptrs[i]
}
}
return values
}
func StringPtrs(vals []string) []*string {
ptrs := make([]*string, len(vals))
for i := 0; i < len(vals); i++ {
ptrs[i] = &vals[i]
}
return ptrs
}
func BoolPtr(v bool) *bool {
return &v
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

36
vendor/vendor.json vendored
View File

@ -1552,6 +1552,42 @@
"version": "v1.2.1",
"versionExact": "v1.2.1"
},
{
"checksumSHA1": "Tq2BiJUqHH6rryU5xGdD/WQlxSk=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "q+o6CYdmE6zUexIu50LJ8m5V9M8=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "0ZnDtNVslrK0P9j01yquQ0qg4rs=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "C2Ft9SlvvWAuKTymaJxyphqlTY0=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "HtMaGZy24OmXN85TYpsLpfWFNr0=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "Maw/I5DN4rRUO4WEenrg4hgPDJA=",
"path": "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312",
"revision": "649583d76d09eff104d18aae17490d2cbf01b373",
"revisionTime": "2018-10-19T12:31:15Z"
},
{
"checksumSHA1": "GQ9bu6PuydK3Yor1JgtVKUfEJm8=",
"path": "github.com/tent/http-link-go",

View File

@ -0,0 +1,154 @@
---
description: |
The `tencentcloud-cvm` Packer builder plugin provide the capability to build
customized images based on an existing base images.
layout: docs
page_title: Tencentcloud Image Builder
sidebar_current: 'docs-builders-tencentcloud-ecs'
---
# Tencentcloud Image Builder
Type: `tencentcloud-cvm`
The `tencentcloud-cvm` Packer builder plugin provide the capability to build
customized images based on an existing base images.
## Configuration Reference
The following configuration options are available for building Tencentcloud images.
In addition to the options listed here,
a [communicator](/docs/templates/communicator.html) can be configured for this
builder.
### Required:
- `secret_id` (string) - Tencentcloud secret id. You should set it directly,
or set the `TENCENTCLOUD_ACCESS_KEY` environment variable.
- `secret_key` (string) - Tencentcloud secret key. You should set it directly,
or set the `TENCENTCLOUD_SECRET_KEY` environment variable.
- `region` (string) - The region where your cvm will be launch. You should
reference [Region and Zone](https://cloud.tencent.com/document/product/213/6091)
for parameter taking.
- `zone` (string) - The zone where your cvm will be launch. You should
reference [Region and Zone](https://cloud.tencent.com/document/product/213/6091)
for parameter taking.
- `instance_type` (string) - The instance type your cvm will be launched by.
You should reference [Instace Type](https://cloud.tencent.com/document/product/213/11518)
for parameter taking.
- `source_image_id` (string) - The base image id of Image you want to create
your customized image from.
- `image_name` (string) - The name you want to create your customize image,
it should be composed of no more than 20 characters, of letters, numbers
or minus sign.
### Optional:
- `force_poweroff` (boolean) - Whether to force power off cvm when create image.
Default value is `false`.
Your cvm will try to shutdown normally; if shutdown failed and `force_poweroff`
set, your cvm will be powered off, otherwise task will fail.
- `image_description` (string) - Image description.
- `reboot` (boolean) - Whether shutdown cvm to create Image. Default value is
`false`.
If `reboot` is not set and cvm is running, create image task will fail.
- `sysprep` (boolean) - Whether enable Sysprep during creating windows image.
- `image_copy_regions` (array of strings) - regions that will be copied to after
your image created.
- `image_share_accounts` (array of strings) - accounts that will be shared to
after your image created.
- `skip_region_validation` (boolean) - Do not check region and zone when validate.
- `associate_public_ip_address` (boolean) - Whether allocate public ip to your cvm.
Default value is `false`.
If not set, you could access your cvm from the same vpc.
- `instance_name` (string) - Instance name.
- `disk_type` (string) - Root disk type your cvm will be launched by. you could
reference [Disk Type](https://cloud.tencent.com/document/api/213/15753#SystemDisk)
for parameter taking.
- `disk_size` (number) - Root disk size your cvm will be launched by. values range(in GB):
- LOCAL_BASIC: 50
- Other: 50 ~ 1000 (need whitelist if > 50)
- `vpc_id` - Specify vpc your cvm will be launched by.
- `vpc_name` - Specify vpc name you will create. if `vpc_id` is not set, packer will
create a vpc for you named this parameter.
- `cidr_block` (boolean) - Specify cider block of the vpc you will create if `vpc_id` not set
- `subnet_id` - Specify subnet your cvm will be launched by.
- 'subnet_name' - Specify subnet name you will create. if `subnet_id` is not set, packer will
create a subnet for you named this parameter.
- `subnect_cidr_block` (boolean) - Specify cider block of the subnet you will create if
`subnet_id` not set
- `internet_max_bandwidth_out` (number) - Max bandwidth out your cvm will be launched by(in MB).
values can be set between 1 ~ 100.
- `security_group_id` - Specify security group your cvm will be launched by.
- `security_group_name` - Specify security name you will create if `security_group_id` not set.
- `user_data` - userdata.
- `user_data_file` - userdata file.
- `host_name` - host name.
## Basic Example
Here is a basic example for Tencentcloud.
``` json
{
"variables": {
"secret_id": "{{env `TENCENTCLOUD_ACCESS_KEY`}}",
"secret_key": "{{env `TENCENTCLOUD_SECRET_KEY`}}"
},
"builders": [{
"type": "tencentcloud-cvm",
"secret_id": "{{user `secret_id`}}",
"secret_key": "{{user `secret_key`}}",
"region": "ap-guangzhou",
"zone": "ap-guangzhou-3",
"instance_type": "S3.SMALL1",
"source_image_id": "img-oikl1tzv",
"ssh_username" : "root",
"image_name": "packerTest2",
"packer_debug": true,
"associate_public_ip_address": true
}],
"provisioners": [{
"type": "shell",
"inline": [
"sleep 30",
"yum install redis.x86_64 -y"
]
}]
}
```
See the
[examples/tencentcloud](https://github.com/hashicorp/packer/tree/master/examples/tencentcloud)
folder in the packer project for more examples.