Merge branch 'master' into pr/6950
This commit is contained in:
commit
9f7b4ffc17
|
@ -107,7 +107,9 @@ From there, open your fork in your browser to open a new pull-request.
|
|||
will break if you `git clone` your fork instead of using `go get` on the main
|
||||
Packer project.
|
||||
|
||||
**Note:** See [Working on forks](#Working on forks) for a better way to use `git push ...`.
|
||||
**Note:** See '[Working with
|
||||
forks](https://help.github.com/articles/working-with-forks/)' for a better way
|
||||
to use `git push ...`.
|
||||
|
||||
### Pull Request Lifecycle
|
||||
|
||||
|
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -1,3 +1,30 @@
|
|||
## 1.3.4 (upcoming)
|
||||
### IMPROVEMENTS:
|
||||
* builder/alicloud: delete copied image and snapshots if corresponding options
|
||||
are specified [GH-7050]
|
||||
* builder/amazon: allow to interpolate more variables [GH-7059]
|
||||
* builder/amazon: Check that the KMS key ID is valid [GH-7090]
|
||||
* builder/amazon: Clean up logging for aws waiters so that it only runs once
|
||||
per builder [GH-7080]
|
||||
* builder/amazon: don't Cleanup Temp Keys when there is no communicator to
|
||||
avoid a panic [GH-7100] [GH-7095]
|
||||
* builder/azure: allow to configure disk caching [GH-7061]
|
||||
* builder/openstack: Don't require network v2 [GH-6933]
|
||||
* builder/openstack: Support for tagging new images [GH-7037]
|
||||
* core/shell: Add env vars "PACKER_HTTP_IP" and "PACKER_HTTP_PORT" to shell
|
||||
provisioners [GH-7075]
|
||||
* core: Deprecate mitchellh/go-homedir package in favor of os/user [GH-7062]
|
||||
* core: make packer inspect not print sensitive variables [GH-7084]
|
||||
* provisioner/ansible-remote: add `-o IdentitiesOnly=yes`as a default flag
|
||||
[GH-7115]
|
||||
* provisioner/windows-restart: wait for already-scheduled reboot [GH-7056] and
|
||||
ignore reboot specific errors [GH-7071]
|
||||
|
||||
### BUG FIXES:
|
||||
* builder/hcloud: fix go mod dependency [GH-7099]
|
||||
* builder/hcloud: prevent panic when ssh key was not passed [GH-7118]
|
||||
* core: removed a flaky race condition in tests [GH-7119]
|
||||
|
||||
## 1.3.3 (December 5, 2018)
|
||||
### IMPROVEMENTS:
|
||||
* builder/alicloud: Add options for system disk properties [GH-6939]
|
||||
|
@ -44,7 +71,7 @@
|
|||
### BUG FIXES:
|
||||
* builder/amazon: Better error handling of region/credential guessing from
|
||||
metadata [GH-6931]
|
||||
* builder.amazon: move region validation to run so that we don't break
|
||||
* builder/amazon: move region validation to run so that we don't break
|
||||
validation when no credentials are set [GH-7032]
|
||||
* builder/hyperv: Remove -Copy:$false when calling Hyper-V\Compare-VM
|
||||
compatability report [GH-7030]
|
||||
|
|
|
@ -110,6 +110,7 @@ func (c *AccessConfig) Session() (*session.Session, error) {
|
|||
if c.DecodeAuthZMessages {
|
||||
DecodeAuthZMessages(c.session)
|
||||
}
|
||||
LogEnvOverrideWarnings()
|
||||
|
||||
return c.session, nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package common
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
@ -62,6 +63,23 @@ func (c *AMIConfig) Prepare(accessConfig *AccessConfig, ctx *interpolate.Context
|
|||
errs = append(errs, fmt.Errorf("Cannot share AMI with encrypted boot volume"))
|
||||
}
|
||||
|
||||
var kmsKeys []string
|
||||
if len(c.AMIKmsKeyId) > 0 {
|
||||
kmsKeys = append(kmsKeys, c.AMIKmsKeyId)
|
||||
}
|
||||
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
||||
for _, kmsKey := range c.AMIRegionKMSKeyIDs {
|
||||
if len(kmsKey) == 0 {
|
||||
kmsKeys = append(kmsKeys, c.AMIKmsKeyId)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, kmsKey := range kmsKeys {
|
||||
if !validateKmsKey(kmsKey) {
|
||||
errs = append(errs, fmt.Errorf("%s is not a valid KMS Key Id.", kmsKey))
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.SnapshotUsers) > 0 {
|
||||
if len(c.AMIKmsKeyId) == 0 && c.AMIEncryptBootVolume {
|
||||
errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted with default KMS key"))
|
||||
|
@ -128,3 +146,23 @@ func (c *AMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) {
|
|||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html
|
||||
func validateKmsKey(kmsKey string) (valid bool) {
|
||||
kmsKeyIdPattern := `[a-f0-9-]+$`
|
||||
aliasPattern := `alias/[a-zA-Z0-9:/_-]+$`
|
||||
kmsArnStartPattern := `^arn:aws:kms:([a-z]{2}-(gov-)?[a-z]+-\d{1})?:(\d{12}):`
|
||||
if regexp.MustCompile(fmt.Sprintf("^%s", kmsKeyIdPattern)).MatchString(kmsKey) {
|
||||
return true
|
||||
}
|
||||
if regexp.MustCompile(fmt.Sprintf("^%s", aliasPattern)).MatchString(kmsKey) {
|
||||
return true
|
||||
}
|
||||
if regexp.MustCompile(fmt.Sprintf("%skey/%s", kmsArnStartPattern, kmsKeyIdPattern)).MatchString(kmsKey) {
|
||||
return true
|
||||
}
|
||||
if regexp.MustCompile(fmt.Sprintf("%s%s", kmsArnStartPattern, aliasPattern)).MatchString(kmsKey) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -176,6 +176,41 @@ func TestAMIConfigPrepare_Share_EncryptedBoot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_ValidateKmsKey(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
c.AMIEncryptBootVolume = true
|
||||
|
||||
accessConf := testAccessConfig()
|
||||
|
||||
validCases := []string{
|
||||
"abcd1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"alias/foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef",
|
||||
"arn:aws:kms:us-east-1:012345678910:alias/foo/bar",
|
||||
}
|
||||
for _, validCase := range validCases {
|
||||
c.AMIKmsKeyId = validCase
|
||||
if err := c.Prepare(accessConf, nil); err != nil {
|
||||
t.Fatalf("%s should not have failed KMS key validation", validCase)
|
||||
}
|
||||
}
|
||||
|
||||
invalidCases := []string{
|
||||
"ABCD1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"ghij1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"ghij1234+e567_890f-a12b-a123b4cd56ef",
|
||||
"foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:foo/bar",
|
||||
}
|
||||
for _, invalidCase := range invalidCases {
|
||||
c.AMIKmsKeyId = invalidCase
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatalf("%s should have failed KMS key validation", invalidCase)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAMINameValidation(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -307,6 +308,36 @@ func getEnvOverrides() overridableWaitVars {
|
|||
return envValues
|
||||
}
|
||||
|
||||
func LogEnvOverrideWarnings() {
|
||||
pollDelay := os.Getenv("AWS_POLL_DELAY_SECONDS")
|
||||
timeoutSeconds := os.Getenv("AWS_TIMEOUT_SECONDS")
|
||||
maxAttempts := os.Getenv("AWS_MAX_ATTEMPTS")
|
||||
|
||||
if maxAttempts != "" && timeoutSeconds != "" {
|
||||
warning := fmt.Sprintf("[WARNING] (aws): AWS_MAX_ATTEMPTS and " +
|
||||
"AWS_TIMEOUT_SECONDS are both set. Packer will use " +
|
||||
"AWS_MAX_ATTEMPTS and discard AWS_TIMEOUT_SECONDS.")
|
||||
if pollDelay == "" {
|
||||
warning = fmt.Sprintf("%s Since you have not set the poll delay, "+
|
||||
"Packer will default to a 2-second delay.", warning)
|
||||
}
|
||||
log.Printf(warning)
|
||||
} else if timeoutSeconds != "" {
|
||||
log.Printf("[WARNING] (aws): env var AWS_TIMEOUT_SECONDS is " +
|
||||
"deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " +
|
||||
"explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " +
|
||||
"poll delay of 2 seconds, regardless of the AWS waiter's default.")
|
||||
}
|
||||
if maxAttempts == "" && timeoutSeconds == "" && pollDelay == "" {
|
||||
log.Printf("[INFO] (aws): No AWS timeout and polling overrides have been set. " +
|
||||
"Packer will default to waiter-specific delays and timeouts. If you would " +
|
||||
"like to customize the length of time between retries and max " +
|
||||
"number of retries you may do so by setting the environment " +
|
||||
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " +
|
||||
"desired values.")
|
||||
}
|
||||
}
|
||||
|
||||
func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption {
|
||||
waitOpts := make([]request.WaiterOption, 0)
|
||||
// If user has set poll delay seconds, overwrite it. If user has NOT,
|
||||
|
@ -320,18 +351,7 @@ func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption
|
|||
// attempts, default to whatever the waiter has set as a default.
|
||||
if envOverrides.awsMaxAttempts.overridden {
|
||||
waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(envOverrides.awsMaxAttempts.Val))
|
||||
}
|
||||
|
||||
if envOverrides.awsMaxAttempts.overridden && envOverrides.awsTimeoutSeconds.overridden {
|
||||
log.Printf("WARNING: AWS_MAX_ATTEMPTS and AWS_TIMEOUT_SECONDS are" +
|
||||
" both set. Packer will be using AWS_MAX_ATTEMPTS and discarding " +
|
||||
"AWS_TIMEOUT_SECONDS. If you have not set AWS_POLL_DELAY_SECONDS, " +
|
||||
"Packer will default to a 2 second poll delay.")
|
||||
} else if envOverrides.awsTimeoutSeconds.overridden {
|
||||
log.Printf("DEPRECATION WARNING: env var AWS_TIMEOUT_SECONDS is " +
|
||||
"deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " +
|
||||
"explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " +
|
||||
"poll delay of 2 seconds, regardless of the AWS waiter's default.")
|
||||
maxAttempts := envOverrides.awsTimeoutSeconds.Val / envOverrides.awsPollDelaySeconds.Val
|
||||
// override the delay so we can get the timeout right
|
||||
if !envOverrides.awsPollDelaySeconds.overridden {
|
||||
|
@ -340,14 +360,6 @@ func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption
|
|||
}
|
||||
waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(maxAttempts))
|
||||
}
|
||||
if len(waitOpts) == 0 {
|
||||
log.Printf("No AWS timeout and polling overrides have been set. " +
|
||||
"Packer will default to waiter-specific delays and timeouts. If you would " +
|
||||
"like to customize the length of time between retries and max " +
|
||||
"number of retries you may do so by setting the environment " +
|
||||
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " +
|
||||
"desired values.")
|
||||
}
|
||||
|
||||
return waitOpts
|
||||
}
|
||||
|
|
|
@ -183,8 +183,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
NewStepGetOSDisk(azureClient, ui),
|
||||
NewStepGetAdditionalDisks(azureClient, ui),
|
||||
NewStepPowerOffCompute(azureClient, ui),
|
||||
NewStepSnapshotOSDisk(azureClient, ui, b.config.isManagedImage()),
|
||||
NewStepSnapshotDataDisks(azureClient, ui, b.config.isManagedImage()),
|
||||
NewStepSnapshotOSDisk(azureClient, ui, b.config),
|
||||
NewStepSnapshotDataDisks(azureClient, ui, b.config),
|
||||
NewStepCaptureImage(azureClient, ui),
|
||||
NewStepDeleteResourceGroup(azureClient, ui),
|
||||
NewStepDeleteOSDisk(azureClient, ui),
|
||||
|
@ -220,8 +220,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&packerCommon.StepProvision{},
|
||||
NewStepGetOSDisk(azureClient, ui),
|
||||
NewStepGetAdditionalDisks(azureClient, ui),
|
||||
NewStepSnapshotOSDisk(azureClient, ui, b.config.isManagedImage()),
|
||||
NewStepSnapshotDataDisks(azureClient, ui, b.config.isManagedImage()),
|
||||
NewStepSnapshotOSDisk(azureClient, ui, b.config),
|
||||
NewStepSnapshotDataDisks(azureClient, ui, b.config),
|
||||
NewStepPowerOffCompute(azureClient, ui),
|
||||
NewStepCaptureImage(azureClient, ui),
|
||||
NewStepDeleteResourceGroup(azureClient, ui),
|
||||
|
|
|
@ -56,8 +56,8 @@ var (
|
|||
reCaptureNamePrefix = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9_\\-\\.]{0,23}$")
|
||||
reManagedDiskName = regexp.MustCompile(validManagedDiskName)
|
||||
reResourceGroupName = regexp.MustCompile(validResourceGroupNameRe)
|
||||
reSnapshotName = regexp.MustCompile("^[A-Za-z0-9_]{10,79}$")
|
||||
reSnapshotPrefix = regexp.MustCompile("^[A-Za-z0-9_]{10,59}$")
|
||||
reSnapshotName = regexp.MustCompile("^[A-Za-z0-9_]{1,79}$")
|
||||
reSnapshotPrefix = regexp.MustCompile("^[A-Za-z0-9_]{1,59}$")
|
||||
)
|
||||
|
||||
type PlanInformation struct {
|
||||
|
|
|
@ -669,7 +669,6 @@ func TestConfigShouldRejectMalformedManagedImageOSDiskSnapshotName(t *testing.T)
|
|||
}
|
||||
|
||||
malformedManagedImageOSDiskSnapshotName := []string{
|
||||
"min_ten",
|
||||
"-leading-hyphen",
|
||||
"trailing-hyphen-",
|
||||
"trailing-period.",
|
||||
|
@ -720,7 +719,6 @@ func TestConfigShouldRejectMalformedManagedImageDataDiskSnapshotPrefix(t *testin
|
|||
}
|
||||
|
||||
malformedManagedImageDataDiskSnapshotPrefix := []string{
|
||||
"more_ten",
|
||||
"-leading-hyphen",
|
||||
"trailing-hyphen-",
|
||||
"trailing-period.",
|
||||
|
|
|
@ -13,19 +13,19 @@ import (
|
|||
)
|
||||
|
||||
type StepSnapshotDataDisks struct {
|
||||
client *AzureClient
|
||||
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
|
||||
say func(message string)
|
||||
error func(e error)
|
||||
isManagedImage bool
|
||||
client *AzureClient
|
||||
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
|
||||
say func(message string)
|
||||
error func(e error)
|
||||
enable func() bool
|
||||
}
|
||||
|
||||
func NewStepSnapshotDataDisks(client *AzureClient, ui packer.Ui, isManagedImage bool) *StepSnapshotDataDisks {
|
||||
func NewStepSnapshotDataDisks(client *AzureClient, ui packer.Ui, config *Config) *StepSnapshotDataDisks {
|
||||
var step = &StepSnapshotDataDisks{
|
||||
client: client,
|
||||
say: func(message string) { ui.Say(message) },
|
||||
error: func(e error) { ui.Error(e.Error()) },
|
||||
isManagedImage: isManagedImage,
|
||||
client: client,
|
||||
say: func(message string) { ui.Say(message) },
|
||||
error: func(e error) { ui.Error(e.Error()) },
|
||||
enable: func() bool { return config.isManagedImage() && config.ManagedImageDataDiskSnapshotPrefix != "" },
|
||||
}
|
||||
|
||||
step.create = step.createDataDiskSnapshot
|
||||
|
@ -66,32 +66,38 @@ func (s *StepSnapshotDataDisks) createDataDiskSnapshot(ctx context.Context, reso
|
|||
return err
|
||||
}
|
||||
|
||||
s.say(fmt.Sprintf(" -> Managed Image Data Disk Snapshot : '%s'", *(createdSnapshot.ID)))
|
||||
|
||||
s.say(fmt.Sprintf(" -> Snapshot ID : '%s'", *(createdSnapshot.ID)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StepSnapshotDataDisks) Run(ctx context.Context, stateBag multistep.StateBag) multistep.StepAction {
|
||||
if s.isManagedImage {
|
||||
if !s.enable() {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
s.say("Taking snapshot of data disk ...")
|
||||
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
|
||||
var location = stateBag.Get(constants.ArmLocation).(string)
|
||||
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
|
||||
var additionalDisks = stateBag.Get(constants.ArmAdditionalDiskVhds).([]string)
|
||||
var dstSnapshotPrefix = stateBag.Get(constants.ArmManagedImageDataDiskSnapshotPrefix).(string)
|
||||
|
||||
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
|
||||
var location = stateBag.Get(constants.ArmLocation).(string)
|
||||
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
|
||||
var additionalDisks = stateBag.Get(constants.ArmAdditionalDiskVhds).([]string)
|
||||
var dstSnapshotPrefix = stateBag.Get(constants.ArmManagedImageDataDiskSnapshotPrefix).(string)
|
||||
if len(additionalDisks) == 1 {
|
||||
s.say(fmt.Sprintf("Snapshotting data disk ..."))
|
||||
} else {
|
||||
s.say(fmt.Sprintf("Snapshotting data disks ..."))
|
||||
}
|
||||
|
||||
for i, disk := range additionalDisks {
|
||||
dstSnapshotName := dstSnapshotPrefix + strconv.Itoa(i)
|
||||
err := s.create(ctx, resourceGroupName, disk, location, tags, dstSnapshotName)
|
||||
for i, disk := range additionalDisks {
|
||||
s.say(fmt.Sprintf(" -> Data Disk : '%s'", disk))
|
||||
|
||||
if err != nil {
|
||||
stateBag.Put(constants.Error, err)
|
||||
s.error(err)
|
||||
dstSnapshotName := dstSnapshotPrefix + strconv.Itoa(i)
|
||||
err := s.create(ctx, resourceGroupName, disk, location, tags, dstSnapshotName)
|
||||
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
if err != nil {
|
||||
stateBag.Put(constants.Error, err)
|
||||
s.error(err)
|
||||
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ func TestStepSnapshotDataDisksShouldFailIfSnapshotFails(t *testing.T) {
|
|||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return fmt.Errorf("!! Unit Test FAIL !!")
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
isManagedImage: true,
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return true },
|
||||
}
|
||||
|
||||
stateBag := createTestStateBagStepSnapshotDataDisks()
|
||||
|
@ -30,14 +30,30 @@ func TestStepSnapshotDataDisksShouldFailIfSnapshotFails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStepSnapshotDataDisksShouldNotExecute(t *testing.T) {
|
||||
var testSubject = &StepSnapshotDataDisks{
|
||||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return fmt.Errorf("!! Unit Test FAIL !!")
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return false },
|
||||
}
|
||||
|
||||
var result = testSubject.Run(context.Background(), nil)
|
||||
if result != multistep.ActionContinue {
|
||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepSnapshotDataDisksShouldPassIfSnapshotPasses(t *testing.T) {
|
||||
var testSubject = &StepSnapshotDataDisks{
|
||||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return nil
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
isManagedImage: true,
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return true },
|
||||
}
|
||||
|
||||
stateBag := createTestStateBagStepSnapshotDataDisks()
|
||||
|
|
|
@ -11,19 +11,19 @@ import (
|
|||
)
|
||||
|
||||
type StepSnapshotOSDisk struct {
|
||||
client *AzureClient
|
||||
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
|
||||
say func(message string)
|
||||
error func(e error)
|
||||
isManagedImage bool
|
||||
client *AzureClient
|
||||
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
|
||||
say func(message string)
|
||||
error func(e error)
|
||||
enable func() bool
|
||||
}
|
||||
|
||||
func NewStepSnapshotOSDisk(client *AzureClient, ui packer.Ui, isManagedImage bool) *StepSnapshotOSDisk {
|
||||
func NewStepSnapshotOSDisk(client *AzureClient, ui packer.Ui, config *Config) *StepSnapshotOSDisk {
|
||||
var step = &StepSnapshotOSDisk{
|
||||
client: client,
|
||||
say: func(message string) { ui.Say(message) },
|
||||
error: func(e error) { ui.Error(e.Error()) },
|
||||
isManagedImage: isManagedImage,
|
||||
client: client,
|
||||
say: func(message string) { ui.Say(message) },
|
||||
error: func(e error) { ui.Error(e.Error()) },
|
||||
enable: func() bool { return config.isManagedImage() && config.ManagedImageOSDiskSnapshotName != "" },
|
||||
}
|
||||
|
||||
step.create = step.createSnapshot
|
||||
|
@ -64,30 +64,31 @@ func (s *StepSnapshotOSDisk) createSnapshot(ctx context.Context, resourceGroupNa
|
|||
return err
|
||||
}
|
||||
|
||||
s.say(fmt.Sprintf(" -> Managed Image OS Disk Snapshot : '%s'", *(createdSnapshot.ID)))
|
||||
|
||||
s.say(fmt.Sprintf(" -> Snapshot ID : '%s'", *(createdSnapshot.ID)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StepSnapshotOSDisk) Run(ctx context.Context, stateBag multistep.StateBag) multistep.StepAction {
|
||||
if s.isManagedImage {
|
||||
if !s.enable() {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
s.say("Taking snapshot of OS disk ...")
|
||||
s.say("Snapshotting OS disk ...")
|
||||
|
||||
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
|
||||
var location = stateBag.Get(constants.ArmLocation).(string)
|
||||
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
|
||||
var srcUriVhd = stateBag.Get(constants.ArmOSDiskVhd).(string)
|
||||
var dstSnapshotName = stateBag.Get(constants.ArmManagedImageOSDiskSnapshotName).(string)
|
||||
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
|
||||
var location = stateBag.Get(constants.ArmLocation).(string)
|
||||
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
|
||||
var srcUriVhd = stateBag.Get(constants.ArmOSDiskVhd).(string)
|
||||
var dstSnapshotName = stateBag.Get(constants.ArmManagedImageOSDiskSnapshotName).(string)
|
||||
|
||||
err := s.create(ctx, resourceGroupName, srcUriVhd, location, tags, dstSnapshotName)
|
||||
s.say(fmt.Sprintf(" -> OS Disk : '%s'", srcUriVhd))
|
||||
err := s.create(ctx, resourceGroupName, srcUriVhd, location, tags, dstSnapshotName)
|
||||
|
||||
if err != nil {
|
||||
stateBag.Put(constants.Error, err)
|
||||
s.error(err)
|
||||
if err != nil {
|
||||
stateBag.Put(constants.Error, err)
|
||||
s.error(err)
|
||||
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
|
|
@ -13,9 +13,9 @@ func TestStepSnapshotOSDiskShouldFailIfSnapshotFails(t *testing.T) {
|
|||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return fmt.Errorf("!! Unit Test FAIL !!")
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
isManagedImage: true,
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return true },
|
||||
}
|
||||
|
||||
stateBag := createTestStateBagStepSnapshotOSDisk()
|
||||
|
@ -30,14 +30,30 @@ func TestStepSnapshotOSDiskShouldFailIfSnapshotFails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStepSnapshotOSDiskShouldNotExecute(t *testing.T) {
|
||||
var testSubject = &StepSnapshotOSDisk{
|
||||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return fmt.Errorf("!! Unit Test FAIL !!")
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return false },
|
||||
}
|
||||
|
||||
var result = testSubject.Run(context.Background(), nil)
|
||||
if result != multistep.ActionContinue {
|
||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepSnapshotOSDiskShouldPassIfSnapshotPasses(t *testing.T) {
|
||||
var testSubject = &StepSnapshotOSDisk{
|
||||
create: func(context.Context, string, string, string, map[string]*string, string) error {
|
||||
return nil
|
||||
},
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
isManagedImage: true,
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
enable: func() bool { return true },
|
||||
}
|
||||
|
||||
stateBag := createTestStateBagStepSnapshotOSDisk()
|
||||
|
|
|
@ -73,7 +73,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
|
||||
// Defaults
|
||||
if len(c.RunCommand) == 0 {
|
||||
c.RunCommand = []string{"-d", "-i", "-t", "--entrypoint=/bin/sh", "--", "{{.Image}}"}
|
||||
c.RunCommand = []string{"-d", "-i", "-t", "{{.Image}}", "/bin/bash"}
|
||||
}
|
||||
|
||||
// Default Pull if it wasn't set
|
||||
|
|
|
@ -20,7 +20,7 @@ type Driver interface {
|
|||
Export(id string, dst io.Writer) error
|
||||
|
||||
// Import imports a container from a tar file
|
||||
Import(path, repo string) (string, error)
|
||||
Import(path string, changes []string, repo string) (string, error)
|
||||
|
||||
// IPAddress returns the address of the container that can be used
|
||||
// for external access.
|
||||
|
|
|
@ -97,12 +97,23 @@ func (d *DockerDriver) Export(id string, dst io.Writer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerDriver) Import(path string, repo string) (string, error) {
|
||||
func (d *DockerDriver) Import(path string, changes []string, repo string) (string, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.Command("docker", "import", "-", repo)
|
||||
|
||||
args := []string{"import"}
|
||||
|
||||
for _, change := range changes {
|
||||
args = append(args, "--change", change)
|
||||
}
|
||||
|
||||
args = append(args, "-")
|
||||
args = append(args, repo)
|
||||
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
stdin, err := cmd.StdinPipe()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -114,6 +125,8 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
log.Printf("Importing tarball with args: %v", args)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ func (d *MockDriver) Export(id string, dst io.Writer) error {
|
|||
return d.ExportError
|
||||
}
|
||||
|
||||
func (d *MockDriver) Import(path, repo string) (string, error) {
|
||||
func (d *MockDriver) Import(path string, changes []string, repo string) (string, error) {
|
||||
d.ImportCalled = true
|
||||
d.ImportPath = path
|
||||
d.ImportRepo = repo
|
||||
|
|
|
@ -43,6 +43,10 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
|||
state.Put("error", fmt.Errorf("Error fetching SSH key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
if sshKey == nil {
|
||||
state.Put("error", fmt.Errorf("Could not find key: %s", k))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
sshKeys = append(sshKeys, sshKey)
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,9 @@ type Driver interface {
|
|||
|
||||
SetVmNetworkAdapterMacAddress(string, string) error
|
||||
|
||||
//Replace the network adapter with a (non-)legacy adapter
|
||||
ReplaceVirtualMachineNetworkAdapter(string, bool) error
|
||||
|
||||
UntagVirtualMachineNetworkAdapterVlan(string, string) error
|
||||
|
||||
CreateExternalVirtualSwitch(string, string) error
|
||||
|
@ -70,11 +73,11 @@ type Driver interface {
|
|||
|
||||
DeleteVirtualSwitch(string) error
|
||||
|
||||
CreateVirtualMachine(string, string, string, int64, int64, int64, string, uint, bool, bool) error
|
||||
CreateVirtualMachine(string, string, string, int64, int64, int64, string, uint, bool, bool, string) error
|
||||
|
||||
AddVirtualMachineHardDrive(string, string, string, int64, int64, string) error
|
||||
|
||||
CloneVirtualMachine(string, string, string, bool, string, string, string, int64, string) error
|
||||
CloneVirtualMachine(string, string, string, bool, string, string, string, int64, string, bool) error
|
||||
|
||||
DeleteVirtualMachine(string) error
|
||||
|
||||
|
|
|
@ -66,6 +66,11 @@ type DriverMock struct {
|
|||
GetVirtualMachineNetworkAdapterAddress_Return string
|
||||
GetVirtualMachineNetworkAdapterAddress_Err error
|
||||
|
||||
ReplaceVirtualMachineNetworkAdapter_Called bool
|
||||
ReplaceVirtualMachineNetworkAdapter_VmName string
|
||||
ReplaceVirtualMachineNetworkAdapter_Replace bool
|
||||
ReplaceVirtualMachineNetworkAdapter_Err error
|
||||
|
||||
SetNetworkAdapterVlanId_Called bool
|
||||
SetNetworkAdapterVlanId_SwitchName string
|
||||
SetNetworkAdapterVlanId_VlanId string
|
||||
|
@ -131,6 +136,7 @@ type DriverMock struct {
|
|||
CreateVirtualMachine_Generation uint
|
||||
CreateVirtualMachine_DifferentialDisk bool
|
||||
CreateVirtualMachine_FixedVHD bool
|
||||
CreateVirtualMachine_Version string
|
||||
CreateVirtualMachine_Err error
|
||||
|
||||
CloneVirtualMachine_Called bool
|
||||
|
@ -143,6 +149,7 @@ type DriverMock struct {
|
|||
CloneVirtualMachine_HarddrivePath string
|
||||
CloneVirtualMachine_Ram int64
|
||||
CloneVirtualMachine_SwitchName string
|
||||
CloneVirtualMachine_Copy bool
|
||||
CloneVirtualMachine_Err error
|
||||
|
||||
DeleteVirtualMachine_Called bool
|
||||
|
@ -334,6 +341,13 @@ func (d *DriverMock) GetVirtualMachineNetworkAdapterAddress(vmName string) (stri
|
|||
return d.GetVirtualMachineNetworkAdapterAddress_Return, d.GetVirtualMachineNetworkAdapterAddress_Err
|
||||
}
|
||||
|
||||
func (d *DriverMock) ReplaceVirtualMachineNetworkAdapter(vmName string, replace bool) error {
|
||||
d.ReplaceVirtualMachineNetworkAdapter_Called = true
|
||||
d.ReplaceVirtualMachineNetworkAdapter_VmName = vmName
|
||||
d.ReplaceVirtualMachineNetworkAdapter_Replace = replace
|
||||
return d.ReplaceVirtualMachineNetworkAdapter_Err
|
||||
}
|
||||
|
||||
func (d *DriverMock) SetNetworkAdapterVlanId(switchName string, vlanId string) error {
|
||||
d.SetNetworkAdapterVlanId_Called = true
|
||||
d.SetNetworkAdapterVlanId_SwitchName = switchName
|
||||
|
@ -409,7 +423,7 @@ func (d *DriverMock) AddVirtualMachineHardDrive(vmName string, vhdFile string, v
|
|||
|
||||
func (d *DriverMock) CreateVirtualMachine(vmName string, path string, harddrivePath string,
|
||||
ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint,
|
||||
diffDisks bool, fixedVHD bool) error {
|
||||
diffDisks bool, fixedVHD bool, version string) error {
|
||||
d.CreateVirtualMachine_Called = true
|
||||
d.CreateVirtualMachine_VmName = vmName
|
||||
d.CreateVirtualMachine_Path = path
|
||||
|
@ -420,12 +434,13 @@ func (d *DriverMock) CreateVirtualMachine(vmName string, path string, harddriveP
|
|||
d.CreateVirtualMachine_SwitchName = switchName
|
||||
d.CreateVirtualMachine_Generation = generation
|
||||
d.CreateVirtualMachine_DifferentialDisk = diffDisks
|
||||
d.CreateVirtualMachine_Version = version
|
||||
return d.CreateVirtualMachine_Err
|
||||
}
|
||||
|
||||
func (d *DriverMock) CloneVirtualMachine(cloneFromVmcxPath string, cloneFromVmName string,
|
||||
cloneFromSnapshotName string, cloneAllSnapshots bool, vmName string, path string,
|
||||
harddrivePath string, ram int64, switchName string) error {
|
||||
harddrivePath string, ram int64, switchName string, copyTF bool) error {
|
||||
d.CloneVirtualMachine_Called = true
|
||||
d.CloneVirtualMachine_CloneFromVmcxPath = cloneFromVmcxPath
|
||||
d.CloneVirtualMachine_CloneFromVmName = cloneFromVmName
|
||||
|
@ -436,6 +451,8 @@ func (d *DriverMock) CloneVirtualMachine(cloneFromVmcxPath string, cloneFromVmNa
|
|||
d.CloneVirtualMachine_HarddrivePath = harddrivePath
|
||||
d.CloneVirtualMachine_Ram = ram
|
||||
d.CloneVirtualMachine_SwitchName = switchName
|
||||
d.CloneVirtualMachine_Copy = copyTF
|
||||
|
||||
return d.CloneVirtualMachine_Err
|
||||
}
|
||||
|
||||
|
|
|
@ -151,6 +151,11 @@ func (d *HypervPS4Driver) SetVmNetworkAdapterMacAddress(vmName string, mac strin
|
|||
return hyperv.SetVmNetworkAdapterMacAddress(vmName, mac)
|
||||
}
|
||||
|
||||
//Replace the network adapter with a (non-)legacy adapter
|
||||
func (d *HypervPS4Driver) ReplaceVirtualMachineNetworkAdapter(vmName string, virtual bool) error {
|
||||
return hyperv.ReplaceVirtualMachineNetworkAdapter(vmName, virtual)
|
||||
}
|
||||
|
||||
func (d *HypervPS4Driver) UntagVirtualMachineNetworkAdapterVlan(vmName string, switchName string) error {
|
||||
return hyperv.UntagVirtualMachineNetworkAdapterVlan(vmName, switchName)
|
||||
}
|
||||
|
@ -183,16 +188,16 @@ func (d *HypervPS4Driver) AddVirtualMachineHardDrive(vmName string, vhdFile stri
|
|||
|
||||
func (d *HypervPS4Driver) CreateVirtualMachine(vmName string, path string, harddrivePath string, ram int64,
|
||||
diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool,
|
||||
fixedVHD bool) error {
|
||||
fixedVHD bool, version string) error {
|
||||
return hyperv.CreateVirtualMachine(vmName, path, harddrivePath, ram, diskSize, diskBlockSize, switchName,
|
||||
generation, diffDisks, fixedVHD)
|
||||
generation, diffDisks, fixedVHD, version)
|
||||
}
|
||||
|
||||
func (d *HypervPS4Driver) CloneVirtualMachine(cloneFromVmcxPath string, cloneFromVmName string,
|
||||
cloneFromSnapshotName string, cloneAllSnapshots bool, vmName string, path string, harddrivePath string,
|
||||
ram int64, switchName string) error {
|
||||
ram int64, switchName string, copyTF bool) error {
|
||||
return hyperv.CloneVirtualMachine(cloneFromVmcxPath, cloneFromVmName, cloneFromSnapshotName,
|
||||
cloneAllSnapshots, vmName, path, harddrivePath, ram, switchName)
|
||||
cloneAllSnapshots, vmName, path, harddrivePath, ram, switchName, copyTF)
|
||||
}
|
||||
|
||||
func (d *HypervPS4Driver) DeleteVirtualMachine(vmName string) error {
|
||||
|
|
|
@ -1,22 +1,33 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func CommHost(state multistep.StateBag) (string, error) {
|
||||
vmName := state.Get("vmName").(string)
|
||||
driver := state.Get("driver").(Driver)
|
||||
func CommHost(host string) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
|
||||
mac, err := driver.Mac(vmName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// Skip IP auto detection if the configuration has an ssh host configured.
|
||||
if host != "" {
|
||||
log.Printf("Using ssh_host value: %s", host)
|
||||
return host, nil
|
||||
}
|
||||
|
||||
vmName := state.Get("vmName").(string)
|
||||
driver := state.Get("driver").(Driver)
|
||||
|
||||
mac, err := driver.Mac(vmName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ip, err := driver.IpAddress(mac)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ip, err := driver.IpAddress(mac)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ type StepCloneVM struct {
|
|||
CloneAllSnapshots bool
|
||||
VMName string
|
||||
SwitchName string
|
||||
CompareCopy bool
|
||||
RamSize uint
|
||||
Cpu uint
|
||||
EnableMacSpoofing bool
|
||||
|
@ -55,8 +56,9 @@ func (s *StepCloneVM) Run(_ context.Context, state multistep.StateBag) multistep
|
|||
// convert the MB to bytes
|
||||
ramSize := int64(s.RamSize * 1024 * 1024)
|
||||
|
||||
err := driver.CloneVirtualMachine(s.CloneFromVMCXPath, s.CloneFromVMName, s.CloneFromSnapshotName,
|
||||
s.CloneAllSnapshots, s.VMName, path, harddrivePath, ramSize, s.SwitchName)
|
||||
err := driver.CloneVirtualMachine(s.CloneFromVMCXPath, s.CloneFromVMName,
|
||||
s.CloneFromSnapshotName, s.CloneAllSnapshots, s.VMName, path,
|
||||
harddrivePath, ramSize, s.SwitchName, s.CompareCopy)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error cloning virtual machine: %s", err)
|
||||
state.Put("error", err)
|
||||
|
|
|
@ -22,6 +22,7 @@ type StepCreateVM struct {
|
|||
RamSize uint
|
||||
DiskSize uint
|
||||
DiskBlockSize uint
|
||||
UseLegacyNetworkAdapter bool
|
||||
Generation uint
|
||||
Cpu uint
|
||||
EnableMacSpoofing bool
|
||||
|
@ -33,6 +34,7 @@ type StepCreateVM struct {
|
|||
DifferencingDisk bool
|
||||
MacAddress string
|
||||
FixedVHD bool
|
||||
Version string
|
||||
}
|
||||
|
||||
func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -64,7 +66,7 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
|
|||
diskBlockSize := int64(s.DiskBlockSize * 1024 * 1024)
|
||||
|
||||
err := driver.CreateVirtualMachine(s.VMName, path, harddrivePath, ramSize, diskSize, diskBlockSize,
|
||||
s.SwitchName, s.Generation, s.DifferencingDisk, s.FixedVHD)
|
||||
s.SwitchName, s.Generation, s.DifferencingDisk, s.FixedVHD, s.Version)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating virtual machine: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -72,6 +74,16 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if s.UseLegacyNetworkAdapter {
|
||||
err := driver.ReplaceVirtualMachineNetworkAdapter(s.VMName, true)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating legacy network adapter: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
err = driver.SetVirtualMachineCpuCount(s.VMName, s.Cpu)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error setting virtual machine cpu count: %s", err)
|
||||
|
|
|
@ -90,11 +90,13 @@ type Config struct {
|
|||
Cpu uint `mapstructure:"cpu"`
|
||||
Generation uint `mapstructure:"generation"`
|
||||
EnableMacSpoofing bool `mapstructure:"enable_mac_spoofing"`
|
||||
UseLegacyNetworkAdapter bool `mapstructure:"use_legacy_network_adapter"`
|
||||
EnableDynamicMemory bool `mapstructure:"enable_dynamic_memory"`
|
||||
EnableSecureBoot bool `mapstructure:"enable_secure_boot"`
|
||||
SecureBootTemplate string `mapstructure:"secure_boot_template"`
|
||||
EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"`
|
||||
TempPath string `mapstructure:"temp_path"`
|
||||
Version string `mapstructure:"configuration_version"`
|
||||
|
||||
Communicator string `mapstructure:"communicator"`
|
||||
|
||||
|
@ -188,6 +190,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
err = errors.New("Generation 2 vms don't support floppy drives. Use ISO image instead.")
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
if b.config.UseLegacyNetworkAdapter {
|
||||
err = errors.New("Generation 2 vms don't support legacy network adapters.")
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.config.AdditionalDiskSize) > 64 {
|
||||
|
@ -408,10 +414,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
EnableSecureBoot: b.config.EnableSecureBoot,
|
||||
SecureBootTemplate: b.config.SecureBootTemplate,
|
||||
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
|
||||
UseLegacyNetworkAdapter: b.config.UseLegacyNetworkAdapter,
|
||||
AdditionalDiskSize: b.config.AdditionalDiskSize,
|
||||
DifferencingDisk: b.config.DifferencingDisk,
|
||||
MacAddress: b.config.MacAddress,
|
||||
FixedVHD: b.config.FixedVHD,
|
||||
Version: b.config.Version,
|
||||
},
|
||||
&hypervcommon.StepEnableIntegrationService{},
|
||||
|
||||
|
@ -453,7 +461,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
// configure the communicator ssh, winrm
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: hypervcommon.CommHost,
|
||||
Host: hypervcommon.CommHost(b.config.SSHConfig.Comm.SSHHost),
|
||||
SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(),
|
||||
},
|
||||
|
||||
|
|
|
@ -631,3 +631,32 @@ func TestUserVariablesInBootCommand(t *testing.T) {
|
|||
t.Fatalf("should not have error: %#v", ret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_UseLegacyNetworkAdapter(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// should be allowed for default config
|
||||
config["use_legacy_network_adapter"] = true
|
||||
|
||||
b = Builder{}
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// should not be allowed for gen 2
|
||||
config["generation"] = 2
|
||||
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ type Config struct {
|
|||
DifferencingDisk bool `mapstructure:"differencing_disk"`
|
||||
|
||||
SwitchName string `mapstructure:"switch_name"`
|
||||
CompareCopy bool `mapstructure:"copy_in_compare"`
|
||||
SwitchVlanId string `mapstructure:"switch_vlan_id"`
|
||||
MacAddress string `mapstructure:"mac_address"`
|
||||
VlanId string `mapstructure:"vlan_id"`
|
||||
|
@ -92,6 +93,7 @@ type Config struct {
|
|||
SecureBootTemplate string `mapstructure:"secure_boot_template"`
|
||||
EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"`
|
||||
TempPath string `mapstructure:"temp_path"`
|
||||
Version string `mapstructure:"configuration_version"`
|
||||
|
||||
Communicator string `mapstructure:"communicator"`
|
||||
|
||||
|
@ -430,6 +432,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
CloneAllSnapshots: b.config.CloneAllSnapshots,
|
||||
VMName: b.config.VMName,
|
||||
SwitchName: b.config.SwitchName,
|
||||
CompareCopy: b.config.CompareCopy,
|
||||
RamSize: b.config.RamSize,
|
||||
Cpu: b.config.Cpu,
|
||||
EnableMacSpoofing: b.config.EnableMacSpoofing,
|
||||
|
@ -480,7 +483,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
// configure the communicator ssh, winrm
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: hypervcommon.CommHost,
|
||||
Host: hypervcommon.CommHost(b.config.SSHConfig.Comm.SSHHost),
|
||||
SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(),
|
||||
},
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
NewStepCreateLoginKey(conn, ui),
|
||||
NewStepCreateServerInstance(conn, ui, b.config),
|
||||
NewStepCreateBlockStorageInstance(conn, ui, b.config),
|
||||
NewStepGetRootPassword(conn, ui),
|
||||
NewStepGetRootPassword(conn, ui, b.config),
|
||||
NewStepCreatePublicIPInstance(conn, ui, b.config),
|
||||
&communicator.StepConnectSSH{
|
||||
Config: &b.config.Comm,
|
||||
|
@ -68,7 +68,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
NewStepCreateLoginKey(conn, ui),
|
||||
NewStepCreateServerInstance(conn, ui, b.config),
|
||||
NewStepCreateBlockStorageInstance(conn, ui, b.config),
|
||||
NewStepGetRootPassword(conn, ui),
|
||||
NewStepGetRootPassword(conn, ui, b.config),
|
||||
NewStepCreatePublicIPInstance(conn, ui, b.config),
|
||||
&communicator.StepConnectWinRM{
|
||||
Config: &b.config.Comm,
|
||||
|
@ -78,7 +78,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
WinRMConfig: func(state multistep.StateBag) (*communicator.WinRMConfig, error) {
|
||||
return &communicator.WinRMConfig{
|
||||
Username: b.config.Comm.WinRMUser,
|
||||
Password: state.Get("Password").(string),
|
||||
Password: b.config.Comm.WinRMPassword,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
|
|
@ -105,7 +105,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
errs = packer.MultiErrorAppend(errs, errors.New("If user_data field is set, length of UserData should be max 21847"))
|
||||
}
|
||||
|
||||
if c.Comm.Type == "wrinrm" && c.AccessControlGroupConfigurationNo == "" {
|
||||
if c.Comm.Type == "winrm" && c.AccessControlGroupConfigurationNo == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("If Communicator is winrm, access_control_group_configuration_no is required"))
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,15 @@ type StepGetRootPassword struct {
|
|||
GetRootPassword func(serverInstanceNo string, privateKey string) (string, error)
|
||||
Say func(message string)
|
||||
Error func(e error)
|
||||
Config *Config
|
||||
}
|
||||
|
||||
func NewStepGetRootPassword(conn *ncloud.Conn, ui packer.Ui) *StepGetRootPassword {
|
||||
func NewStepGetRootPassword(conn *ncloud.Conn, ui packer.Ui, config *Config) *StepGetRootPassword {
|
||||
var step = &StepGetRootPassword{
|
||||
Conn: conn,
|
||||
Say: func(message string) { ui.Say(message) },
|
||||
Error: func(e error) { ui.Error(e.Error()) },
|
||||
Conn: conn,
|
||||
Say: func(message string) { ui.Say(message) },
|
||||
Error: func(e error) { ui.Error(e.Error()) },
|
||||
Config: config,
|
||||
}
|
||||
|
||||
step.GetRootPassword = step.getRootPassword
|
||||
|
@ -51,7 +53,11 @@ func (s *StepGetRootPassword) Run(_ context.Context, state multistep.StateBag) m
|
|||
|
||||
rootPassword, err := s.GetRootPassword(serverInstanceNo, loginKey.PrivateKey)
|
||||
|
||||
state.Put("Password", rootPassword)
|
||||
if s.Config.Comm.Type == "ssh" {
|
||||
s.Config.Comm.SSHPassword = rootPassword
|
||||
} else if s.Config.Comm.Type == "winrm" {
|
||||
s.Config.Comm.WinRMPassword = rootPassword
|
||||
}
|
||||
|
||||
return processStepResult(err, s.Error, state)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ func TestStepGetRootPasswordShouldFailIfOperationGetRootPasswordFails(t *testing
|
|||
GetRootPassword: func(string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") },
|
||||
Say: func(message string) {},
|
||||
Error: func(e error) {},
|
||||
Config: &Config{},
|
||||
}
|
||||
|
||||
stateBag := DeleteTestStateBagStepGetRootPassword()
|
||||
|
@ -33,6 +34,7 @@ func TestStepGetRootPasswordShouldPassIfOperationGetRootPasswordPasses(t *testin
|
|||
GetRootPassword: func(string, string) (string, error) { return "a", nil },
|
||||
Say: func(message string) {},
|
||||
Error: func(e error) {},
|
||||
Config: &Config{},
|
||||
}
|
||||
|
||||
stateBag := DeleteTestStateBagStepGetRootPassword()
|
||||
|
|
|
@ -142,6 +142,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&stepCreateImage{
|
||||
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
|
||||
},
|
||||
&stepUpdateImageTags{},
|
||||
&stepUpdateImageVisibility{},
|
||||
&stepAddImageMembers{},
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ type ImageConfig struct {
|
|||
ImageVisibility imageservice.ImageVisibility `mapstructure:"image_visibility"`
|
||||
ImageMembers []string `mapstructure:"image_members"`
|
||||
ImageDiskFormat string `mapstructure:"image_disk_format"`
|
||||
ImageTags []string `mapstructure:"image_tags"`
|
||||
}
|
||||
|
||||
func (c *ImageConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
|
|
|
@ -40,6 +40,7 @@ type RunConfig struct {
|
|||
UseBlockStorageVolume bool `mapstructure:"use_blockstorage_volume"`
|
||||
VolumeName string `mapstructure:"volume_name"`
|
||||
VolumeType string `mapstructure:"volume_type"`
|
||||
VolumeSize int `mapstructure:"volume_size"`
|
||||
VolumeAvailabilityZone string `mapstructure:"volume_availability_zone"`
|
||||
|
||||
// Not really used, but here for BC
|
||||
|
|
|
@ -21,6 +21,17 @@ func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multis
|
|||
config := state.Get("config").(*Config)
|
||||
server := state.Get("server").(*servers.Server)
|
||||
|
||||
var instanceIP floatingips.FloatingIP
|
||||
|
||||
// This is here in case we error out before putting instanceIp into the
|
||||
// statebag below, because it is requested by Cleanup()
|
||||
state.Put("access_ip", &instanceIP)
|
||||
|
||||
if s.FloatingIP == "" && !s.ReuseIPs && s.FloatingIPNetwork == "" {
|
||||
ui.Message("Floating IP not required")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
|
@ -37,12 +48,6 @@ func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multis
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
var instanceIP floatingips.FloatingIP
|
||||
|
||||
// This is here in case we error out before putting instanceIp into the
|
||||
// statebag below, because it is requested by Cleanup()
|
||||
state.Put("access_ip", &instanceIP)
|
||||
|
||||
// Try to Use the OpenStack floating IP by checking provided parameters in
|
||||
// the following order:
|
||||
// - try to use "FloatingIP" ID directly if it's provided
|
||||
|
@ -142,6 +147,11 @@ func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
|
|||
ui := state.Get("ui").(packer.Ui)
|
||||
instanceIP := state.Get("access_ip").(*floatingips.FloatingIP)
|
||||
|
||||
// Don't clean up if unless required
|
||||
if instanceIP.ID == "" && instanceIP.FloatingIP == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't delete pool addresses we didn't allocate
|
||||
if state.Get("floatingip_istemp") == false {
|
||||
return
|
||||
|
|
|
@ -35,20 +35,25 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
|
|||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
imageClient, err := config.imageV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing image client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
volumeSize := config.VolumeSize
|
||||
|
||||
// Get needed volume size from the source image.
|
||||
volumeSize, err := GetVolumeSize(imageClient, sourceImage)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
if volumeSize == 0 {
|
||||
imageClient, err := config.imageV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing image client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
volumeSize, err = GetVolumeSize(imageClient, sourceImage)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("Creating volume...")
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package openstack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
imageservice "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type stepUpdateImageTags struct{}
|
||||
|
||||
func (s *stepUpdateImageTags) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
imageId := state.Get("image").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
config := state.Get("config").(*Config)
|
||||
|
||||
if len(config.ImageTags) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
imageClient, err := config.imageV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing image service client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Updating image tags to %s", strings.Join(config.ImageTags, ", ")))
|
||||
r := imageservice.Update(
|
||||
imageClient,
|
||||
imageId,
|
||||
imageservice.UpdateOpts{
|
||||
imageservice.ReplaceImageTags{
|
||||
NewTags: config.ImageTags,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if _, err = r.Extract(); err != nil {
|
||||
err = fmt.Errorf("Error updating image tags: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepUpdateImageTags) Cleanup(multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
|
@ -89,9 +89,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
return nil, err
|
||||
}
|
||||
|
||||
image, ok := state.GetOk("image")
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &Artifact{
|
||||
Image: state.Get("image").(core.Image),
|
||||
Image: image.(core.Image),
|
||||
Region: region,
|
||||
driver: driver,
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ var accels = map[string]struct{}{
|
|||
"xen": {},
|
||||
"hax": {},
|
||||
"hvf": {},
|
||||
"whpx": {},
|
||||
}
|
||||
|
||||
var netDevice = map[string]bool{
|
||||
|
@ -274,7 +275,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
|
||||
if _, ok := accels[b.config.Accelerator]; !ok {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', or 'none' are allowed"))
|
||||
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
|
||||
}
|
||||
|
||||
if _, ok := netDevice[b.config.NetDevice]; !ok {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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, ®ion)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,9 +24,11 @@ func (d *VBox42Driver) CreateSATAController(vmName string, name string, portcoun
|
|||
return err
|
||||
}
|
||||
|
||||
portCountArg := "--sataportcount"
|
||||
if strings.HasPrefix(version, "4.3") || strings.HasPrefix(version, "5.") || strings.HasPrefix(version, "6.") {
|
||||
portCountArg = "--portcount"
|
||||
portCountArg := "--portcount"
|
||||
if strings.HasPrefix(version, "0.") || strings.HasPrefix(version, "1.") || strings.HasPrefix(version, "2.") ||
|
||||
strings.HasPrefix(version, "3.") || strings.HasPrefix(version, "4.0") || strings.HasPrefix(version, "4.1") ||
|
||||
strings.HasPrefix(version, "4.2") {
|
||||
portCountArg = "--sataportcount"
|
||||
}
|
||||
|
||||
command := []string{
|
||||
|
|
|
@ -62,10 +62,10 @@ func NewArtifact(remoteType string, format string, exportOutputPath string, vmNa
|
|||
if remoteType != "" && !skipExport {
|
||||
dir = new(LocalOutputDir)
|
||||
dir.SetOutputDir(exportOutputPath)
|
||||
files, err = dir.ListFiles()
|
||||
} else {
|
||||
files, err = state.Get("dir").(OutputDir).ListFiles()
|
||||
dir = state.Get("dir").(OutputDir)
|
||||
}
|
||||
files, err = dir.ListFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -25,8 +25,6 @@ type vmxTemplateData struct {
|
|||
CpuCount string
|
||||
MemorySize string
|
||||
|
||||
HDD_BootOrder string
|
||||
|
||||
SCSI_Present string
|
||||
SCSI_diskAdapterType string
|
||||
SATA_Present string
|
||||
|
@ -167,7 +165,6 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist
|
|||
NVME_Present: "FALSE",
|
||||
|
||||
DiskType: "scsi",
|
||||
HDD_BootOrder: "scsi0:0",
|
||||
CDROMType: "ide",
|
||||
CDROMType_PrimarySecondary: "0",
|
||||
|
||||
|
@ -190,20 +187,17 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist
|
|||
templateData.DiskType = "ide"
|
||||
templateData.CDROMType = "ide"
|
||||
templateData.CDROMType_PrimarySecondary = "1"
|
||||
templateData.HDD_BootOrder = "ide0:0"
|
||||
case "sata":
|
||||
templateData.SATA_Present = "TRUE"
|
||||
templateData.DiskType = "sata"
|
||||
templateData.CDROMType = "sata"
|
||||
templateData.CDROMType_PrimarySecondary = "1"
|
||||
templateData.HDD_BootOrder = "sata0:0"
|
||||
case "nvme":
|
||||
templateData.NVME_Present = "TRUE"
|
||||
templateData.DiskType = "nvme"
|
||||
templateData.SATA_Present = "TRUE"
|
||||
templateData.CDROMType = "sata"
|
||||
templateData.CDROMType_PrimarySecondary = "0"
|
||||
templateData.HDD_BootOrder = "nvme0:0"
|
||||
case "scsi":
|
||||
diskAdapterType = "lsilogic"
|
||||
fallthrough
|
||||
|
@ -213,7 +207,6 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist
|
|||
templateData.DiskType = "scsi"
|
||||
templateData.CDROMType = "ide"
|
||||
templateData.CDROMType_PrimarySecondary = "0"
|
||||
templateData.HDD_BootOrder = "scsi0:0"
|
||||
}
|
||||
|
||||
/// Handle the cdrom adapter type. If the disk adapter type and the
|
||||
|
@ -477,7 +470,6 @@ nvram = "{{ .Name }}.nvram"
|
|||
|
||||
floppy0.present = "FALSE"
|
||||
bios.bootOrder = "hdd,cdrom"
|
||||
bios.hddOrder = "{{ .HDD_BootOrder }}"
|
||||
|
||||
// Configuration
|
||||
extendedConfigFile = "{{ .Name }}.vmxf"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1,15 +1,34 @@
|
|||
package hyperv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/hashicorp/packer/common/powershell"
|
||||
)
|
||||
|
||||
type scriptOptions struct {
|
||||
Version string
|
||||
VMName string
|
||||
VHDX string
|
||||
Path string
|
||||
HardDrivePath string
|
||||
MemoryStartupBytes int64
|
||||
NewVHDSizeBytes int64
|
||||
VHDBlockSizeBytes int64
|
||||
SwitchName string
|
||||
Generation uint
|
||||
DiffDisks bool
|
||||
FixedVHD bool
|
||||
}
|
||||
|
||||
func GetHostAdapterIpAddressForSwitch(switchName string) (string, error) {
|
||||
var script = `
|
||||
param([string]$switchName, [int]$addressIndex)
|
||||
|
@ -135,7 +154,7 @@ func SetBootDvdDrive(vmName string, controllerNumber uint, controllerLocation ui
|
|||
if generation < 2 {
|
||||
script := `
|
||||
param([string]$vmName)
|
||||
Hyper-V\Set-VMBios -VMName $vmName -StartupOrder @("CD", "IDE","LegacyNetworkAdapter","Floppy")
|
||||
Hyper-V\Set-VMBios -VMName $vmName -StartupOrder @("IDE","CD","LegacyNetworkAdapter","Floppy")
|
||||
`
|
||||
var ps powershell.PowerShellCmd
|
||||
err := ps.Run(script, vmName)
|
||||
|
@ -202,77 +221,101 @@ Hyper-V\Set-VMFloppyDiskDrive -VMName $vmName -Path $null
|
|||
return err
|
||||
}
|
||||
|
||||
// This was created as a proof of concept for moving logic out of the powershell
|
||||
// scripting so that we can test the pathways activated by our variables.
|
||||
// Rather than creating a powershell script with several conditionals, this will
|
||||
// generate a conditional-free script which already sets all of the necessary
|
||||
// variables inline.
|
||||
//
|
||||
// For examples of what this template will generate, you can look at the
|
||||
// test cases in ./hyperv_test.go
|
||||
//
|
||||
func getCreateVMScript(opts *scriptOptions) (string, error) {
|
||||
|
||||
if opts.FixedVHD && opts.Generation == 2 {
|
||||
return "", fmt.Errorf("Generation 2 VMs don't support fixed disks.")
|
||||
}
|
||||
|
||||
opts.VHDX = opts.VMName + ".vhdx"
|
||||
if opts.FixedVHD {
|
||||
opts.VHDX = opts.VMName + ".vhd"
|
||||
}
|
||||
|
||||
var tpl = template.Must(template.New("createVM").Parse(`
|
||||
$vhdPath = Join-Path -Path {{ .Path }} -ChildPath {{ .VHDX }}
|
||||
|
||||
{{ if ne .HardDrivePath "" -}}
|
||||
{{- if .DiffDisks -}}
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath {{ .HardDrivePath }} -Differencing -BlockSizeBytes {{ .VHDBlockSizeBytes }}
|
||||
{{- else -}}
|
||||
Copy-Item -Path {{ .HardDrivePath }} -Destination $vhdPath
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if .FixedVHD -}}
|
||||
Hyper-V\New-VHD -Path $vhdPath -Fixed -SizeBytes {{ .NewVHDSizeBytes }}
|
||||
{{- else -}}
|
||||
Hyper-V\New-VHD -Path $vhdPath -SizeBytes {{ .NewVHDSizeBytes }} -BlockSizeBytes {{ .VHDBlockSizeBytes }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
Hyper-V\New-VM -Name {{ .VMName }} -Path {{ .Path }} -MemoryStartupBytes {{ .MemoryStartupBytes }} -VHDPath $vhdPath -SwitchName {{ .SwitchName }}
|
||||
{{- if eq .Generation 2}} -Generation {{ .Generation }} {{- end -}}
|
||||
{{- if ne .Version ""}} -Version {{ .Version }} {{- end -}}
|
||||
`))
|
||||
|
||||
var b bytes.Buffer
|
||||
err := tpl.Execute(&b, opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Tidy away the excess newlines left over by the template
|
||||
regex, err := regexp.Compile("^\n")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
final := regex.ReplaceAllString(b.String(), "")
|
||||
regex, err = regexp.Compile("\n\n")
|
||||
final = regex.ReplaceAllString(final, "\n")
|
||||
|
||||
return final, nil
|
||||
}
|
||||
|
||||
func CreateVirtualMachine(vmName string, path string, harddrivePath string, ram int64,
|
||||
diskSize int64, diskBlockSize int64, switchName string, generation uint,
|
||||
diffDisks bool, fixedVHD bool) error {
|
||||
diffDisks bool, fixedVHD bool, version string) error {
|
||||
|
||||
if generation == 2 {
|
||||
var script = `
|
||||
param([string]$vmName, [string]$path, [string]$harddrivePath, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [long]$vhdBlockSizeBytes, [string]$switchName, [int]$generation, [string]$diffDisks)
|
||||
$vhdx = $vmName + '.vhdx'
|
||||
$vhdPath = Join-Path -Path $path -ChildPath $vhdx
|
||||
if ($harddrivePath){
|
||||
if($diffDisks -eq "true"){
|
||||
New-VHD -Path $vhdPath -ParentPath $harddrivePath -Differencing -BlockSizeBytes $vhdBlockSizeBytes
|
||||
} else {
|
||||
Copy-Item -Path $harddrivePath -Destination $vhdPath
|
||||
opts := scriptOptions{
|
||||
Version: version,
|
||||
VMName: vmName,
|
||||
Path: path,
|
||||
HardDrivePath: harddrivePath,
|
||||
MemoryStartupBytes: ram,
|
||||
NewVHDSizeBytes: diskSize,
|
||||
VHDBlockSizeBytes: diskBlockSize,
|
||||
SwitchName: switchName,
|
||||
Generation: generation,
|
||||
DiffDisks: diffDisks,
|
||||
FixedVHD: fixedVHD,
|
||||
}
|
||||
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName -Generation $generation
|
||||
} else {
|
||||
Hyper-V\New-VHD -Path $vhdPath -SizeBytes $newVHDSizeBytes -BlockSizeBytes $vhdBlockSizeBytes
|
||||
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName -Generation $generation
|
||||
}
|
||||
`
|
||||
var ps powershell.PowerShellCmd
|
||||
if err := ps.Run(script, vmName, path, harddrivePath, strconv.FormatInt(ram, 10),
|
||||
strconv.FormatInt(diskSize, 10), strconv.FormatInt(diskBlockSize, 10),
|
||||
switchName, strconv.FormatInt(int64(generation), 10),
|
||||
strconv.FormatBool(diffDisks)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return DisableAutomaticCheckpoints(vmName)
|
||||
} else {
|
||||
var script = `
|
||||
param([string]$vmName, [string]$path, [string]$harddrivePath, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [long]$vhdBlockSizeBytes, [string]$switchName, [string]$diffDisks, [string]$fixedVHD)
|
||||
if($fixedVHD -eq "true"){
|
||||
$vhdx = $vmName + '.vhd'
|
||||
}
|
||||
else{
|
||||
$vhdx = $vmName + '.vhdx'
|
||||
}
|
||||
$vhdPath = Join-Path -Path $path -ChildPath $vhdx
|
||||
if ($harddrivePath){
|
||||
if($diffDisks -eq "true"){
|
||||
New-VHD -Path $vhdPath -ParentPath $harddrivePath -Differencing -BlockSizeBytes $vhdBlockSizeBytes
|
||||
script, err := getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
else{
|
||||
Copy-Item -Path $harddrivePath -Destination $vhdPath
|
||||
}
|
||||
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName
|
||||
} else {
|
||||
if($fixedVHD -eq "true"){
|
||||
Hyper-V\New-VHD -Path $vhdPath -Fixed -SizeBytes $newVHDSizeBytes
|
||||
}
|
||||
else {
|
||||
Hyper-V\New-VHD -Path $vhdPath -SizeBytes $newVHDSizeBytes -BlockSizeBytes $vhdBlockSizeBytes
|
||||
}
|
||||
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName
|
||||
}
|
||||
`
|
||||
var ps powershell.PowerShellCmd
|
||||
if err := ps.Run(script, vmName, path, harddrivePath, strconv.FormatInt(ram, 10),
|
||||
strconv.FormatInt(diskSize, 10), strconv.FormatInt(diskBlockSize, 10),
|
||||
switchName, strconv.FormatBool(diffDisks), strconv.FormatBool(fixedVHD)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := DisableAutomaticCheckpoints(vmName); err != nil {
|
||||
return err
|
||||
}
|
||||
var ps powershell.PowerShellCmd
|
||||
if err = ps.Run(script); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := DisableAutomaticCheckpoints(vmName); err != nil {
|
||||
return err
|
||||
}
|
||||
if generation != 2 {
|
||||
return DeleteAllDvdDrives(vmName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableAutomaticCheckpoints(vmName string) error {
|
||||
|
@ -366,10 +409,10 @@ Hyper-V\Set-VMNetworkAdapter $vmName -staticmacaddress $mac
|
|||
}
|
||||
|
||||
func ImportVmcxVirtualMachine(importPath string, vmName string, harddrivePath string,
|
||||
ram int64, switchName string) error {
|
||||
ram int64, switchName string, copyTF bool) error {
|
||||
|
||||
var script = `
|
||||
param([string]$importPath, [string]$vmName, [string]$harddrivePath, [long]$memoryStartupBytes, [string]$switchName)
|
||||
param([string]$importPath, [string]$vmName, [string]$harddrivePath, [long]$memoryStartupBytes, [string]$switchName, [string]$copy)
|
||||
|
||||
$VirtualHarddisksPath = Join-Path -Path $importPath -ChildPath 'Virtual Hard Disks'
|
||||
if (!(Test-Path $VirtualHarddisksPath)) {
|
||||
|
@ -395,7 +438,13 @@ if (!$VirtualMachinePath){
|
|||
$VirtualMachinePath = Get-ChildItem -Path $importPath -Filter *.xml -Recurse -ErrorAction SilentlyContinue | select -First 1 | %{$_.FullName}
|
||||
}
|
||||
|
||||
$compatibilityReport = Hyper-V\Compare-VM -Path $VirtualMachinePath -VirtualMachinePath $importPath -SmartPagingFilePath $importPath -SnapshotFilePath $importPath -VhdDestinationPath $VirtualHarddisksPath -GenerateNewId
|
||||
$copyBool = $false
|
||||
switch($copy) {
|
||||
"true" { $copyBool = $true }
|
||||
default { $copyBool = $false }
|
||||
}
|
||||
|
||||
$compatibilityReport = Hyper-V\Compare-VM -Path $VirtualMachinePath -VirtualMachinePath $importPath -SmartPagingFilePath $importPath -SnapshotFilePath $importPath -VhdDestinationPath $VirtualHarddisksPath -GenerateNewId -Copy:$false
|
||||
if ($vhdPath){
|
||||
Copy-Item -Path $harddrivePath -Destination $vhdPath
|
||||
$existingFirstHarddrive = $compatibilityReport.VM.HardDrives | Select -First 1
|
||||
|
@ -415,16 +464,15 @@ if ($vm) {
|
|||
$result = Hyper-V\Rename-VM -VM $vm -NewName $VMName
|
||||
}
|
||||
`
|
||||
|
||||
var ps powershell.PowerShellCmd
|
||||
err := ps.Run(script, importPath, vmName, harddrivePath, strconv.FormatInt(ram, 10), switchName)
|
||||
err := ps.Run(script, importPath, vmName, harddrivePath, strconv.FormatInt(ram, 10), switchName, strconv.FormatBool(copyTF))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func CloneVirtualMachine(cloneFromVmcxPath string, cloneFromVmName string,
|
||||
cloneFromSnapshotName string, cloneAllSnapshots bool, vmName string,
|
||||
path string, harddrivePath string, ram int64, switchName string) error {
|
||||
path string, harddrivePath string, ram int64, switchName string, copyTF bool) error {
|
||||
|
||||
if cloneFromVmName != "" {
|
||||
if err := ExportVmcxVirtualMachine(path, cloneFromVmName,
|
||||
|
@ -439,7 +487,7 @@ func CloneVirtualMachine(cloneFromVmcxPath string, cloneFromVmName string,
|
|||
}
|
||||
}
|
||||
|
||||
if err := ImportVmcxVirtualMachine(path, vmName, harddrivePath, ram, switchName); err != nil {
|
||||
if err := ImportVmcxVirtualMachine(path, vmName, harddrivePath, ram, switchName, copyTF); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -950,6 +998,24 @@ Hyper-V\Set-VMNetworkAdapterVlan -VMName $vmName -Access -VlanId $vlanId
|
|||
return err
|
||||
}
|
||||
|
||||
func ReplaceVirtualMachineNetworkAdapter(vmName string, legacy bool) error {
|
||||
|
||||
var script = `
|
||||
param([string]$vmName,[string]$legacyString)
|
||||
$legacy = [System.Boolean]::Parse($legacyString)
|
||||
$switch = (Get-VMNetworkAdapter -VMName $vmName).SwitchName
|
||||
Remove-VMNetworkAdapter -VMName $vmName
|
||||
Add-VMNetworkAdapter -VMName $vmName -SwitchName $switch -Name $vmName -IsLegacy $legacy
|
||||
`
|
||||
legacyString := "False"
|
||||
if legacy {
|
||||
legacyString = "True"
|
||||
}
|
||||
var ps powershell.PowerShellCmd
|
||||
err := ps.Run(script, vmName, legacyString)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetExternalOnlineVirtualSwitch() (string, error) {
|
||||
|
||||
var script = `
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
package hyperv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_getCreateVMScript(t *testing.T) {
|
||||
opts := scriptOptions{
|
||||
Version: "5.0",
|
||||
VMName: "myvm",
|
||||
Path: "C://mypath",
|
||||
HardDrivePath: "C://harddrivepath",
|
||||
MemoryStartupBytes: int64(1024),
|
||||
NewVHDSizeBytes: int64(8192),
|
||||
VHDBlockSizeBytes: int64(10),
|
||||
SwitchName: "hyperv-vmx-switch",
|
||||
Generation: uint(1),
|
||||
DiffDisks: true,
|
||||
FixedVHD: true,
|
||||
}
|
||||
|
||||
// Check Fixed VHD conditional set
|
||||
scriptString, err := getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected := `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhd
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
// We should never get here thanks to good template validation, but it's
|
||||
// good to fail rather than trying to run the ps script and erroring.
|
||||
opts.Generation = uint(2)
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err == nil {
|
||||
t.Fatalf("Should have Error: %s", err.Error())
|
||||
}
|
||||
|
||||
// Check VHDX conditional set
|
||||
opts.FixedVHD = false
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Generation 2 -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
// Check generation 1 no fixed VHD
|
||||
opts.FixedVHD = false
|
||||
opts.Generation = uint(1)
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
// Check that we use generation one template even if generation is unset
|
||||
opts.Generation = uint(0)
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
// same "expected" as above
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
opts.Version = ""
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
opts.DiffDisks = false
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Copy-Item -Path C://harddrivepath -Destination $vhdPath
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
opts.HardDrivePath = ""
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -SizeBytes 8192 -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
opts.FixedVHD = true
|
||||
scriptString, err = getCreateVMScript(&opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhd
|
||||
Hyper-V\New-VHD -Path $vhdPath -Fixed -SizeBytes 8192
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
}
|
|
@ -30,10 +30,15 @@
|
|||
}],
|
||||
"provisioners": [{
|
||||
"type": "powershell",
|
||||
"inline": [
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
"inline": [
|
||||
" # NOTE: the following *3* lines are only needed if the you have installed the Guest Agent.",
|
||||
" while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureTelemetryService).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -35,10 +35,15 @@
|
|||
],
|
||||
"provisioners": [{
|
||||
"type": "powershell",
|
||||
"inline": [
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
"inline": [
|
||||
" # NOTE: the following *3* lines are only needed if the you have installed the Guest Agent.",
|
||||
" while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureTelemetryService).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -26,10 +26,15 @@
|
|||
}],
|
||||
"provisioners": [{
|
||||
"type": "powershell",
|
||||
"inline": [
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
"inline": [
|
||||
" # NOTE: the following *3* lines are only needed if the you have installed the Guest Agent.",
|
||||
" while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureTelemetryService).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
" while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
|
||||
|
||||
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
|
||||
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
|
||||
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}]
|
||||
}
|
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -22,12 +22,17 @@ func ConfigDir() (string, error) {
|
|||
}
|
||||
|
||||
func homeDir() (string, error) {
|
||||
// First prefer the HOME environmental variable
|
||||
// Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
log.Printf("Detected home directory from env var: %s", home)
|
||||
return home, nil
|
||||
}
|
||||
|
||||
if home := os.Getenv("APPDATA"); home != "" {
|
||||
log.Printf("Detected home directory from env var: %s", home)
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// Fall back to the passwd database if not found which follows
|
||||
// the same semantics as bourne shell
|
||||
u, err := user.Current()
|
||||
|
|
|
@ -34,3 +34,15 @@ func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error {
|
|||
func (t *MockProvisioner) Cancel() {
|
||||
t.CancelCalled = true
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) Communicator() Communicator {
|
||||
return t.ProvCommunicator
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) ElevatedUser() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) ElevatedPassword() string {
|
||||
return "password"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
|
@ -17,14 +18,17 @@ func TestMuxBroker(t *testing.T) {
|
|||
go bc.Run()
|
||||
go bs.Run()
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errChan)
|
||||
c, err := bc.Dial(5)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
errChan <- fmt.Errorf("err dialing: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := c.Write([]byte{42}); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
errChan <- fmt.Errorf("err writing: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -41,6 +45,12 @@ func TestMuxBroker(t *testing.T) {
|
|||
if data[0] != 42 {
|
||||
t.Fatalf("bad: %d", data[0])
|
||||
}
|
||||
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testYamux(t *testing.T) (client *yamux.Session, server *yamux.Session) {
|
||||
|
|
|
@ -103,6 +103,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
|||
if len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
awscommon.LogEnvOverrideWarnings()
|
||||
|
||||
packer.LogSecretFilter.Set(p.config.AccessKey, p.config.SecretKey, p.config.Token)
|
||||
log.Println(p.config)
|
||||
|
|
|
@ -16,8 +16,9 @@ const BuilderId = "packer.post-processor.docker-import"
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
Repository string `mapstructure:"repository"`
|
||||
Tag string `mapstructure:"tag"`
|
||||
Repository string `mapstructure:"repository"`
|
||||
Tag string `mapstructure:"tag"`
|
||||
Changes []string `mapstructure:"changes"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
@ -62,7 +63,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
|
|||
|
||||
ui.Message("Importing image: " + artifact.Id())
|
||||
ui.Message("Repository: " + importRepo)
|
||||
id, err := driver.Import(artifact.Files()[0], importRepo)
|
||||
id, err := driver.Import(artifact.Files()[0], p.config.Changes, importRepo)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
|
@ -376,7 +376,7 @@ func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) erro
|
|||
func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error {
|
||||
inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile)))
|
||||
|
||||
extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ",
|
||||
extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s -o IdentitiesOnly=yes\" ",
|
||||
p.config.PackerBuildName, p.config.PackerBuilderType, common.GetHTTPAddr())
|
||||
if len(p.config.ExtraArguments) > 0 {
|
||||
extraArgs = extraArgs + strings.Join(p.config.ExtraArguments, " ")
|
||||
|
|
|
@ -355,7 +355,7 @@ func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator, pri
|
|||
|
||||
var envvars []string
|
||||
|
||||
args := []string{"--extra-vars", fmt.Sprintf("packer_build_name=%s packer_builder_type=%s",
|
||||
args := []string{"--extra-vars", fmt.Sprintf("packer_build_name=%s packer_builder_type=%s -o IdentitiesOnly=yes",
|
||||
p.config.PackerBuildName, p.config.PackerBuilderType),
|
||||
"-i", inventory, playbook}
|
||||
if len(privKeyFile) > 0 {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/uuid"
|
||||
commonhelper "github.com/hashicorp/packer/helper/common"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/provisioner"
|
||||
|
@ -50,6 +51,8 @@ type Config struct {
|
|||
ChefEnvironment string `mapstructure:"chef_environment"`
|
||||
ClientKey string `mapstructure:"client_key"`
|
||||
ConfigTemplate string `mapstructure:"config_template"`
|
||||
ElevatedUser string `mapstructure:"elevated_user"`
|
||||
ElevatedPassword string `mapstructure:"elevated_password"`
|
||||
EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"`
|
||||
ExecuteCommand string `mapstructure:"execute_command"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
|
@ -76,6 +79,7 @@ type Config struct {
|
|||
|
||||
type Provisioner struct {
|
||||
config Config
|
||||
communicator packer.Communicator
|
||||
guestOSTypeConfig guestOSTypeConfig
|
||||
guestCommands *provisioner.GuestCommands
|
||||
}
|
||||
|
@ -94,6 +98,10 @@ type ConfigTemplate struct {
|
|||
ValidationKeyPath string
|
||||
}
|
||||
|
||||
type EnvVarsTemplate struct {
|
||||
WinRMPassword string
|
||||
}
|
||||
|
||||
type ExecuteTemplate struct {
|
||||
ConfigPath string
|
||||
JsonPath string
|
||||
|
@ -111,6 +119,12 @@ type KnifeTemplate struct {
|
|||
}
|
||||
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
// Create passthrough for winrm password so we can fill it in once we know
|
||||
// it
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: `{{.WinRMPassword}}`,
|
||||
}
|
||||
|
||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &p.config.ctx,
|
||||
|
@ -221,6 +235,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
|||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
p.communicator = comm
|
||||
|
||||
nodeName := p.config.NodeName
|
||||
if nodeName == "" {
|
||||
nodeName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
|
@ -551,6 +567,13 @@ func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config
|
|||
return err
|
||||
}
|
||||
|
||||
if p.config.ElevatedUser != "" {
|
||||
command, err = provisioner.GenerateElevatedRunner(command, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Executing Chef: %s", command))
|
||||
|
||||
cmd := &packer.RemoteCmd{
|
||||
|
@ -676,6 +699,31 @@ func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func getWinRMPassword(buildName string) string {
|
||||
winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
|
||||
packer.LogSecretFilter.Set(winRMPass)
|
||||
return winRMPass
|
||||
}
|
||||
|
||||
func (p *Provisioner) Communicator() packer.Communicator {
|
||||
return p.communicator
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedUser() string {
|
||||
return p.config.ElevatedUser
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedPassword() string {
|
||||
// Replace ElevatedPassword for winrm users who used this feature
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
|
||||
}
|
||||
|
||||
elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
|
||||
|
||||
return elevatedPassword
|
||||
}
|
||||
|
||||
var DefaultConfigTemplate = `
|
||||
log_level :info
|
||||
log_location STDOUT
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/hashicorp/packer/common/uuid"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type ElevatedProvisioner interface {
|
||||
Communicator() packer.Communicator
|
||||
ElevatedUser() string
|
||||
ElevatedPassword() string
|
||||
}
|
||||
|
||||
type elevatedOptions struct {
|
||||
User string
|
||||
Password string
|
||||
TaskName string
|
||||
TaskDescription string
|
||||
LogFile string
|
||||
XMLEscapedCommand string
|
||||
}
|
||||
|
||||
var psEscape = strings.NewReplacer(
|
||||
"$", "`$",
|
||||
"\"", "`\"",
|
||||
"`", "``",
|
||||
"'", "`'",
|
||||
)
|
||||
|
||||
var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(`
|
||||
$name = "{{.TaskName}}"
|
||||
$log = [System.Environment]::ExpandEnvironmentVariables("{{.LogFile}}")
|
||||
$s = New-Object -ComObject "Schedule.Service"
|
||||
$s.Connect()
|
||||
$t = $s.NewTask($null)
|
||||
$xml = [xml]@'
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Description>{{.TaskDescription}}</Description>
|
||||
</RegistrationInfo>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>{{.User}}</UserId>
|
||||
<LogonType>Password</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>true</AllowHardTerminate>
|
||||
<StartWhenAvailable>false</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT24H</ExecutionTimeLimit>
|
||||
<Priority>4</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>cmd</Command>
|
||||
<Arguments>/c {{.XMLEscapedCommand}}</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
'@
|
||||
$logon_type = 1
|
||||
$password = "{{.Password}}"
|
||||
if ($password.Length -eq 0) {
|
||||
$logon_type = 5
|
||||
$password = $null
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
||||
$ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI)
|
||||
$node = $xml.SelectSingleNode("/ns:Task/ns:Principals/ns:Principal/ns:LogonType", $ns)
|
||||
$node.ParentNode.RemoveChild($node) | Out-Null
|
||||
}
|
||||
$t.XmlText = $xml.OuterXml
|
||||
if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"}
|
||||
$f = $s.GetFolder("\")
|
||||
$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", $password, $logon_type, $null) | Out-Null
|
||||
$t = $f.GetTask("\$name")
|
||||
$t.Run($null) | Out-Null
|
||||
$timeout = 10
|
||||
$sec = 0
|
||||
while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) {
|
||||
Start-Sleep -s 1
|
||||
$sec++
|
||||
}
|
||||
|
||||
$line = 0
|
||||
do {
|
||||
Start-Sleep -m 100
|
||||
if (Test-Path $log) {
|
||||
Get-Content $log | select -skip $line | ForEach {
|
||||
$line += 1
|
||||
Write-Output "$_"
|
||||
}
|
||||
}
|
||||
} while (!($t.state -eq 3))
|
||||
$result = $t.LastTaskResult
|
||||
if (Test-Path $log) {
|
||||
Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null
|
||||
exit $result`))
|
||||
|
||||
func GenerateElevatedRunner(command string, p ElevatedProvisioner) (uploadedPath string, err error) {
|
||||
log.Printf("Building elevated command wrapper for: %s", command)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// Output from the elevated command cannot be returned directly to the
|
||||
// Packer console. In order to be able to view output from elevated
|
||||
// commands and scripts an indirect approach is used by which the commands
|
||||
// output is first redirected to file. The output file is then 'watched'
|
||||
// by Packer while the elevated command is running and any content
|
||||
// appearing in the file is written out to the console. Below the portion
|
||||
// of command required to redirect output from the command to file is
|
||||
// built and appended to the existing command string
|
||||
taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
// Only use %ENVVAR% format for environment variables when setting the log
|
||||
// file path; Do NOT use $env:ENVVAR format as it won't be expanded
|
||||
// correctly in the elevatedTemplate
|
||||
logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out"
|
||||
command += fmt.Sprintf(" > %s 2>&1", logFile)
|
||||
|
||||
// elevatedTemplate wraps the command in a single quoted XML text string
|
||||
// so we need to escape characters considered 'special' in XML.
|
||||
err = xml.EscapeText(&buffer, []byte(command))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err)
|
||||
}
|
||||
escapedCommand := buffer.String()
|
||||
log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand)
|
||||
buffer.Reset()
|
||||
|
||||
// Escape chars special to PowerShell in the ElevatedUser string
|
||||
elevatedUser := p.ElevatedUser()
|
||||
escapedElevatedUser := psEscape.Replace(elevatedUser)
|
||||
if escapedElevatedUser != elevatedUser {
|
||||
log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell",
|
||||
elevatedUser, escapedElevatedUser)
|
||||
}
|
||||
|
||||
// Escape chars special to PowerShell in the ElevatedPassword string
|
||||
elevatedPassword := p.ElevatedPassword()
|
||||
escapedElevatedPassword := psEscape.Replace(elevatedPassword)
|
||||
if escapedElevatedPassword != elevatedPassword {
|
||||
log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell",
|
||||
elevatedPassword, escapedElevatedPassword)
|
||||
}
|
||||
|
||||
// Generate command
|
||||
err = elevatedTemplate.Execute(&buffer, elevatedOptions{
|
||||
User: escapedElevatedUser,
|
||||
Password: escapedElevatedPassword,
|
||||
TaskName: taskName,
|
||||
TaskDescription: "Packer elevated task",
|
||||
LogFile: logFile,
|
||||
XMLEscapedCommand: escapedCommand,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating elevated template: %s", err)
|
||||
return "", err
|
||||
}
|
||||
uuid := uuid.TimeOrderedUUID()
|
||||
path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid)
|
||||
log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path)
|
||||
err = p.Communicator().Upload(path, &buffer, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error preparing elevated powershell script: %s", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path), err
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"inline": []interface{}{"foo", "bar"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisioner_GenerateElevatedRunner(t *testing.T) {
|
||||
|
||||
// Non-elevated
|
||||
config := testConfig()
|
||||
p := new(packer.MockProvisioner)
|
||||
p.Prepare(config)
|
||||
comm := new(packer.MockCommunicator)
|
||||
p.ProvCommunicator = comm
|
||||
path, err := GenerateElevatedRunner("whoami", p)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Did not expect error: %s", err.Error())
|
||||
}
|
||||
|
||||
if comm.UploadCalled != true {
|
||||
t.Fatalf("Should have uploaded file")
|
||||
}
|
||||
|
||||
matched, _ := regexp.MatchString("C:/Windows/Temp/packer-elevated-shell.*", path)
|
||||
if !matched {
|
||||
t.Fatalf("Got unexpected file: %s", path)
|
||||
}
|
||||
}
|
|
@ -172,7 +172,7 @@ func (p *Provisioner) ProvisionUpload(ui packer.Ui, comm packer.Communicator) er
|
|||
}
|
||||
|
||||
if strings.HasSuffix(dst, "/") {
|
||||
dst = filepath.Join(dst, filepath.Base(src))
|
||||
dst = dst + filepath.Base(src)
|
||||
}
|
||||
|
||||
// Get a default progress bar
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
package powershell
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type elevatedOptions struct {
|
||||
User string
|
||||
Password string
|
||||
TaskName string
|
||||
TaskDescription string
|
||||
LogFile string
|
||||
XMLEscapedCommand string
|
||||
}
|
||||
|
||||
var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(`
|
||||
$name = "{{.TaskName}}"
|
||||
$log = [System.Environment]::ExpandEnvironmentVariables("{{.LogFile}}")
|
||||
$s = New-Object -ComObject "Schedule.Service"
|
||||
$s.Connect()
|
||||
$t = $s.NewTask($null)
|
||||
$xml = [xml]@'
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Description>{{.TaskDescription}}</Description>
|
||||
</RegistrationInfo>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>{{.User}}</UserId>
|
||||
<LogonType>Password</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>true</AllowHardTerminate>
|
||||
<StartWhenAvailable>false</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT24H</ExecutionTimeLimit>
|
||||
<Priority>4</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>cmd</Command>
|
||||
<Arguments>/c {{.XMLEscapedCommand}}</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
'@
|
||||
$logon_type = 1
|
||||
$password = "{{.Password}}"
|
||||
if ($password.Length -eq 0) {
|
||||
$logon_type = 5
|
||||
$password = $null
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
||||
$ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI)
|
||||
$node = $xml.SelectSingleNode("/ns:Task/ns:Principals/ns:Principal/ns:LogonType", $ns)
|
||||
$node.ParentNode.RemoveChild($node) | Out-Null
|
||||
}
|
||||
$t.XmlText = $xml.OuterXml
|
||||
if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"}
|
||||
$f = $s.GetFolder("\")
|
||||
$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", $password, $logon_type, $null) | Out-Null
|
||||
$t = $f.GetTask("\$name")
|
||||
$t.Run($null) | Out-Null
|
||||
$timeout = 10
|
||||
$sec = 0
|
||||
while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) {
|
||||
Start-Sleep -s 1
|
||||
$sec++
|
||||
}
|
||||
|
||||
$line = 0
|
||||
do {
|
||||
Start-Sleep -m 100
|
||||
if (Test-Path $log) {
|
||||
Get-Content $log | select -skip $line | ForEach {
|
||||
$line += 1
|
||||
Write-Output "$_"
|
||||
}
|
||||
}
|
||||
} while (!($t.state -eq 3))
|
||||
$result = $t.LastTaskResult
|
||||
if (Test-Path $log) {
|
||||
Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null
|
||||
exit $result`))
|
|
@ -4,8 +4,6 @@ package powershell
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
@ -20,6 +18,7 @@ import (
|
|||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/packer/tmp"
|
||||
"github.com/hashicorp/packer/provisioner"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
|
@ -507,90 +506,29 @@ func (p *Provisioner) createCommandTextPrivileged() (command string, err error)
|
|||
return "", fmt.Errorf("Error processing command: %s", err)
|
||||
}
|
||||
|
||||
// OK so we need an elevated shell runner to wrap our command, this is
|
||||
// going to have its own path generate the script and update the command
|
||||
// runner in the process
|
||||
path, err := p.generateElevatedRunner(command)
|
||||
command, err = provisioner.GenerateElevatedRunner(command, p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error generating elevated runner: %s", err)
|
||||
}
|
||||
|
||||
// Return the path to the elevated shell wrapper
|
||||
command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path)
|
||||
|
||||
return command, err
|
||||
}
|
||||
|
||||
func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) {
|
||||
log.Printf("Building elevated command wrapper for: %s", command)
|
||||
func (p *Provisioner) Communicator() packer.Communicator {
|
||||
return p.communicator
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
func (p *Provisioner) ElevatedUser() string {
|
||||
return p.config.ElevatedUser
|
||||
}
|
||||
|
||||
// Output from the elevated command cannot be returned directly to the
|
||||
// Packer console. In order to be able to view output from elevated
|
||||
// commands and scripts an indirect approach is used by which the commands
|
||||
// output is first redirected to file. The output file is then 'watched'
|
||||
// by Packer while the elevated command is running and any content
|
||||
// appearing in the file is written out to the console. Below the portion
|
||||
// of command required to redirect output from the command to file is
|
||||
// built and appended to the existing command string
|
||||
taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
// Only use %ENVVAR% format for environment variables when setting the log
|
||||
// file path; Do NOT use $env:ENVVAR format as it won't be expanded
|
||||
// correctly in the elevatedTemplate
|
||||
logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out"
|
||||
command += fmt.Sprintf(" > %s 2>&1", logFile)
|
||||
|
||||
// elevatedTemplate wraps the command in a single quoted XML text string
|
||||
// so we need to escape characters considered 'special' in XML.
|
||||
err = xml.EscapeText(&buffer, []byte(command))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err)
|
||||
}
|
||||
escapedCommand := buffer.String()
|
||||
log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand)
|
||||
buffer.Reset()
|
||||
|
||||
// Escape chars special to PowerShell in the ElevatedUser string
|
||||
escapedElevatedUser := psEscape.Replace(p.config.ElevatedUser)
|
||||
if escapedElevatedUser != p.config.ElevatedUser {
|
||||
log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell",
|
||||
p.config.ElevatedUser, escapedElevatedUser)
|
||||
}
|
||||
func (p *Provisioner) ElevatedPassword() string {
|
||||
// Replace ElevatedPassword for winrm users who used this feature
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
|
||||
}
|
||||
|
||||
p.config.ElevatedPassword, _ = interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
|
||||
elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
|
||||
|
||||
// Escape chars special to PowerShell in the ElevatedPassword string
|
||||
escapedElevatedPassword := psEscape.Replace(p.config.ElevatedPassword)
|
||||
if escapedElevatedPassword != p.config.ElevatedPassword {
|
||||
log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell",
|
||||
p.config.ElevatedPassword, escapedElevatedPassword)
|
||||
}
|
||||
|
||||
// Generate command
|
||||
err = elevatedTemplate.Execute(&buffer, elevatedOptions{
|
||||
User: escapedElevatedUser,
|
||||
Password: escapedElevatedPassword,
|
||||
TaskName: taskName,
|
||||
TaskDescription: "Packer elevated task",
|
||||
LogFile: logFile,
|
||||
XMLEscapedCommand: escapedCommand,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating elevated template: %s", err)
|
||||
return "", err
|
||||
}
|
||||
uuid := uuid.TimeOrderedUUID()
|
||||
path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid)
|
||||
log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path)
|
||||
err = p.communicator.Upload(path, &buffer, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error preparing elevated powershell script: %s", err)
|
||||
}
|
||||
return path, err
|
||||
return elevatedPassword
|
||||
}
|
||||
|
|
|
@ -654,30 +654,6 @@ func TestProvision_uploadEnvVars(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProvision_generateElevatedShellRunner(t *testing.T) {
|
||||
|
||||
// Non-elevated
|
||||
config := testConfig()
|
||||
p := new(Provisioner)
|
||||
p.Prepare(config)
|
||||
comm := new(packer.MockCommunicator)
|
||||
p.communicator = comm
|
||||
path, err := p.generateElevatedRunner("whoami")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Did not expect error: %s", err.Error())
|
||||
}
|
||||
|
||||
if comm.UploadCalled != true {
|
||||
t.Fatalf("Should have uploaded file")
|
||||
}
|
||||
|
||||
matched, _ := regexp.MatchString("C:/Windows/Temp/packer-elevated-shell.*", path)
|
||||
if !matched {
|
||||
t.Fatalf("Got unexpected file: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryable(t *testing.T) {
|
||||
config := testConfig()
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
commonhelper "github.com/hashicorp/packer/helper/common"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/provisioner"
|
||||
|
@ -65,6 +66,12 @@ type Config struct {
|
|||
// The directory from which the command will be executed.
|
||||
// Packer requires the directory to exist when running puppet.
|
||||
WorkingDir string `mapstructure:"working_directory"`
|
||||
|
||||
// Instructs the communicator to run the remote script as a Windows
|
||||
// scheduled task, effectively elevating the remote user by impersonating
|
||||
// a logged-in user
|
||||
ElevatedUser string `mapstructure:"elevated_user"`
|
||||
ElevatedPassword string `mapstructure:"elevated_password"`
|
||||
}
|
||||
|
||||
type guestOSTypeConfig struct {
|
||||
|
@ -117,6 +124,7 @@ var guestOSTypeConfigs = map[string]guestOSTypeConfig{
|
|||
|
||||
type Provisioner struct {
|
||||
config Config
|
||||
communicator packer.Communicator
|
||||
guestOSTypeConfig guestOSTypeConfig
|
||||
guestCommands *provisioner.GuestCommands
|
||||
}
|
||||
|
@ -135,7 +143,17 @@ type ExecuteTemplate struct {
|
|||
WorkingDir string
|
||||
}
|
||||
|
||||
type EnvVarsTemplate struct {
|
||||
WinRMPassword string
|
||||
}
|
||||
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
// Create passthrough for winrm password so we can fill it in once we know
|
||||
// it
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: `{{.WinRMPassword}}`,
|
||||
}
|
||||
|
||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &p.config.ctx,
|
||||
|
@ -240,6 +258,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
|||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
ui.Say("Provisioning with Puppet...")
|
||||
p.communicator = comm
|
||||
ui.Message("Creating Puppet staging directory...")
|
||||
if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
|
||||
return fmt.Errorf("Error creating staging directory: %s", err)
|
||||
|
@ -316,6 +335,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if p.config.ElevatedUser != "" {
|
||||
command, err = provisioner.GenerateElevatedRunner(command, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
}
|
||||
|
@ -432,10 +458,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri
|
|||
}
|
||||
|
||||
func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: fmt.Sprintf("rm -fr '%s'", dir),
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)}
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -460,3 +483,28 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds
|
|||
|
||||
return comm.UploadDir(dst, src, nil)
|
||||
}
|
||||
|
||||
func getWinRMPassword(buildName string) string {
|
||||
winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
|
||||
packer.LogSecretFilter.Set(winRMPass)
|
||||
return winRMPass
|
||||
}
|
||||
|
||||
func (p *Provisioner) Communicator() packer.Communicator {
|
||||
return p.communicator
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedUser() string {
|
||||
return p.config.ElevatedUser
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedPassword() string {
|
||||
// Replace ElevatedPassword for winrm users who used this feature
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
|
||||
}
|
||||
|
||||
elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
|
||||
|
||||
return elevatedPassword
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
commonhelper "github.com/hashicorp/packer/helper/common"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/provisioner"
|
||||
|
@ -63,6 +64,12 @@ type Config struct {
|
|||
// The directory from which the command will be executed.
|
||||
// Packer requires the directory to exist when running puppet.
|
||||
WorkingDir string `mapstructure:"working_directory"`
|
||||
|
||||
// Instructs the communicator to run the remote script as a Windows
|
||||
// scheduled task, effectively elevating the remote user by impersonating
|
||||
// a logged-in user
|
||||
ElevatedUser string `mapstructure:"elevated_user"`
|
||||
ElevatedPassword string `mapstructure:"elevated_password"`
|
||||
}
|
||||
|
||||
type guestOSTypeConfig struct {
|
||||
|
@ -112,6 +119,7 @@ var guestOSTypeConfigs = map[string]guestOSTypeConfig{
|
|||
|
||||
type Provisioner struct {
|
||||
config Config
|
||||
communicator packer.Communicator
|
||||
guestOSTypeConfig guestOSTypeConfig
|
||||
guestCommands *provisioner.GuestCommands
|
||||
}
|
||||
|
@ -129,7 +137,17 @@ type ExecuteTemplate struct {
|
|||
WorkingDir string
|
||||
}
|
||||
|
||||
type EnvVarsTemplate struct {
|
||||
WinRMPassword string
|
||||
}
|
||||
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
// Create passthrough for winrm password so we can fill it in once we know
|
||||
// it
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: `{{.WinRMPassword}}`,
|
||||
}
|
||||
|
||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &p.config.ctx,
|
||||
|
@ -210,6 +228,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
|||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
ui.Say("Provisioning with Puppet...")
|
||||
p.communicator = comm
|
||||
ui.Message("Creating Puppet staging directory...")
|
||||
if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
|
||||
return fmt.Errorf("Error creating staging directory: %s", err)
|
||||
|
@ -269,6 +288,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if p.config.ElevatedUser != "" {
|
||||
command, err = provisioner.GenerateElevatedRunner(command, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
}
|
||||
|
@ -321,10 +347,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri
|
|||
}
|
||||
|
||||
func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: fmt.Sprintf("rm -fr '%s'", dir),
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)}
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -349,3 +372,28 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds
|
|||
|
||||
return comm.UploadDir(dst, src, nil)
|
||||
}
|
||||
|
||||
func getWinRMPassword(buildName string) string {
|
||||
winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
|
||||
packer.LogSecretFilter.Set(winRMPass)
|
||||
return winRMPass
|
||||
}
|
||||
|
||||
func (p *Provisioner) Communicator() packer.Communicator {
|
||||
return p.communicator
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedUser() string {
|
||||
return p.config.ElevatedUser
|
||||
}
|
||||
|
||||
func (p *Provisioner) ElevatedPassword() string {
|
||||
// Replace ElevatedPassword for winrm users who used this feature
|
||||
p.config.ctx.Data = &EnvVarsTemplate{
|
||||
WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
|
||||
}
|
||||
|
||||
elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
|
||||
|
||||
return elevatedPassword
|
||||
}
|
||||
|
|
|
@ -23,6 +23,12 @@ var retryableSleep = 5 * time.Second
|
|||
var TryCheckReboot = `shutdown /r /f /t 60 /c "packer restart test"`
|
||||
var AbortReboot = `shutdown /a`
|
||||
|
||||
var DefaultRegistryKeys = []string{
|
||||
"HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending",
|
||||
"HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending",
|
||||
"HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress",
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
|
@ -36,6 +42,12 @@ type Config struct {
|
|||
// The timeout for waiting for the machine to restart
|
||||
RestartTimeout time.Duration `mapstructure:"restart_timeout"`
|
||||
|
||||
// Whether to check the registry (see RegistryKeys) for pending reboots
|
||||
CheckKey bool `mapstructure:"check_registry"`
|
||||
|
||||
// custom keys to check for
|
||||
RegistryKeys []string `mapstructure:"registry_keys"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
|
@ -73,6 +85,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
|||
p.config.RestartTimeout = 5 * time.Minute
|
||||
}
|
||||
|
||||
if len(p.config.RegistryKeys) == 0 {
|
||||
p.config.RegistryKeys = DefaultRegistryKeys
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -212,7 +228,6 @@ var waitForCommunicator = func(p *Provisioner) error {
|
|||
log.Printf("Connected to machine")
|
||||
runCustomRestartCheck = false
|
||||
}
|
||||
|
||||
// This is the non-user-configurable check that powershell
|
||||
// modules have loaded.
|
||||
|
||||
|
@ -234,6 +249,37 @@ var waitForCommunicator = func(p *Provisioner) error {
|
|||
log.Printf("echo didn't succeed; retrying...")
|
||||
continue
|
||||
}
|
||||
|
||||
if p.config.CheckKey {
|
||||
log.Printf("Connected to machine")
|
||||
shouldContinue := false
|
||||
for _, RegKey := range p.config.RegistryKeys {
|
||||
KeyTestCommand := winrm.Powershell(fmt.Sprintf(`Test-Path "%s"`, RegKey))
|
||||
cmdKeyCheck := &packer.RemoteCmd{Command: KeyTestCommand}
|
||||
log.Printf("Checking registry for pending reboots")
|
||||
var buf, buf2 bytes.Buffer
|
||||
cmdKeyCheck.Stdout = &buf
|
||||
cmdKeyCheck.Stdout = io.MultiWriter(cmdKeyCheck.Stdout, &buf2)
|
||||
|
||||
err := p.comm.Start(cmdKeyCheck)
|
||||
if err != nil {
|
||||
log.Printf("Communication connection err: %s", err)
|
||||
shouldContinue = true
|
||||
}
|
||||
cmdKeyCheck.Wait()
|
||||
|
||||
stdoutToRead := buf2.String()
|
||||
if strings.Contains(stdoutToRead, "True") {
|
||||
log.Printf("RegistryKey %s exists; waiting...", KeyTestCommand)
|
||||
shouldContinue = true
|
||||
} else {
|
||||
log.Printf("No Registry keys found; exiting wait loop")
|
||||
}
|
||||
}
|
||||
if shouldContinue {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
88
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal file
88
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal 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
|
||||
}
|
40
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal file
40
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal 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,
|
||||
}
|
||||
}
|
35
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal file
35
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal 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
|
||||
}
|
231
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal file
231
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal 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
|
||||
}
|
69
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal file
69
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal 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
|
||||
}
|
13
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
13
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
package profile
|
||||
|
||||
type ClientProfile struct {
|
||||
HttpProfile *HttpProfile
|
||||
SignMethod string
|
||||
}
|
||||
|
||||
func NewClientProfile() *ClientProfile {
|
||||
return &ClientProfile{
|
||||
HttpProfile: NewHttpProfile(),
|
||||
SignMethod: "HmacSHA256",
|
||||
}
|
||||
}
|
17
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal file
17
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal 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",
|
||||
}
|
||||
}
|
75
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal file
75
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal 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
|
||||
}
|
47
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal file
47
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal 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
|
||||
}
|
1679
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312/client.go
generated
vendored
Normal file
1679
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312/client.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2771
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312/models.go
generated
vendored
Normal file
2771
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312/models.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue