Merge branch 'master' into pr/6950

This commit is contained in:
Adrien Delorme 2019-01-09 10:11:18 +01:00 committed by GitHub
commit 9f7b4ffc17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 16974 additions and 527 deletions

View File

@ -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

View File

@ -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]

View File

@ -110,6 +110,7 @@ func (c *AccessConfig) Session() (*session.Session, error) {
if c.DecodeAuthZMessages {
DecodeAuthZMessages(c.session)
}
LogEnvOverrideWarnings()
return c.session, nil
}

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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),

View File

@ -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 {

View File

@ -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.",

View File

@ -17,15 +17,15 @@ type StepSnapshotDataDisks struct {
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
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,
enable: func() bool { return config.isManagedImage() && config.ManagedImageDataDiskSnapshotPrefix != "" },
}
step.create = step.createDataDiskSnapshot
@ -66,15 +66,14 @@ 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 {
s.say("Taking snapshot of data disk ...")
if !s.enable() {
return multistep.ActionContinue
}
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
var location = stateBag.Get(constants.ArmLocation).(string)
@ -82,7 +81,15 @@ func (s *StepSnapshotDataDisks) Run(ctx context.Context, stateBag multistep.Stat
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 {
s.say(fmt.Sprintf(" -> Data Disk : '%s'", disk))
dstSnapshotName := dstSnapshotPrefix + strconv.Itoa(i)
err := s.create(ctx, resourceGroupName, disk, location, tags, dstSnapshotName)
@ -93,7 +100,6 @@ func (s *StepSnapshotDataDisks) Run(ctx context.Context, stateBag multistep.Stat
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}

View File

@ -15,7 +15,7 @@ func TestStepSnapshotDataDisksShouldFailIfSnapshotFails(t *testing.T) {
},
say: func(message string) {},
error: func(e error) {},
isManagedImage: true,
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotDataDisks()
@ -30,6 +30,22 @@ 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 {
@ -37,7 +53,7 @@ func TestStepSnapshotDataDisksShouldPassIfSnapshotPasses(t *testing.T) {
},
say: func(message string) {},
error: func(e error) {},
isManagedImage: true,
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotDataDisks()

View File

@ -15,15 +15,15 @@ type StepSnapshotOSDisk struct {
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
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,
enable: func() bool { return config.isManagedImage() && config.ManagedImageOSDiskSnapshotName != "" },
}
step.create = step.createSnapshot
@ -64,15 +64,16 @@ 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)
@ -80,6 +81,7 @@ func (s *StepSnapshotOSDisk) Run(ctx context.Context, stateBag multistep.StateBa
var srcUriVhd = stateBag.Get(constants.ArmOSDiskVhd).(string)
var dstSnapshotName = stateBag.Get(constants.ArmManagedImageOSDiskSnapshotName).(string)
s.say(fmt.Sprintf(" -> OS Disk : '%s'", srcUriVhd))
err := s.create(ctx, resourceGroupName, srcUriVhd, location, tags, dstSnapshotName)
if err != nil {
@ -88,7 +90,6 @@ func (s *StepSnapshotOSDisk) Run(ctx context.Context, stateBag multistep.StateBa
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}

View File

@ -15,7 +15,7 @@ func TestStepSnapshotOSDiskShouldFailIfSnapshotFails(t *testing.T) {
},
say: func(message string) {},
error: func(e error) {},
isManagedImage: true,
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotOSDisk()
@ -30,6 +30,22 @@ 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 {
@ -37,7 +53,7 @@ func TestStepSnapshotOSDiskShouldPassIfSnapshotPasses(t *testing.T) {
},
say: func(message string) {},
error: func(e error) {},
isManagedImage: true,
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotOSDisk()

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -1,10 +1,20 @@
package common
import (
"log"
"github.com/hashicorp/packer/helper/multistep"
)
func CommHost(state multistep.StateBag) (string, error) {
func CommHost(host string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
// 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)
@ -19,4 +29,5 @@ func CommHost(state multistep.StateBag) (string, error) {
}
return ip, nil
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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(),
},

View File

@ -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")
}
}

View File

@ -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(),
},

View File

@ -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
},
},

View File

@ -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"))
}

View File

@ -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()) },
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)
}

View File

@ -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()

View File

@ -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{},
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -35,6 +35,11 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
state.Put("error", err)
return multistep.ActionHalt
}
volumeSize := config.VolumeSize
// Get needed volume size from the source image.
if volumeSize == 0 {
imageClient, err := config.imageV2Client()
if err != nil {
err = fmt.Errorf("Error initializing image client: %s", err)
@ -42,14 +47,14 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionHalt
}
// Get needed volume size from the source image.
volumeSize, err := GetVolumeSize(imageClient, sourceImage)
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...")
volumeOpts := volumes.CreateOpts{

View File

@ -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...
}

View File

@ -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,
}

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{

View File

@ -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
}

View File

@ -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"

View File

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

View File

@ -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 {
script, err := getCreateVMScript(&opts)
if 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
}
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 {
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 = `

View File

@ -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)
}
}

View File

@ -31,6 +31,11 @@
"provisioners": [{
"type": "powershell",
"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 } }"

View File

@ -36,6 +36,11 @@
"provisioners": [{
"type": "powershell",
"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 } }"

View File

@ -27,6 +27,11 @@
"provisioners": [{
"type": "powershell",
"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 } }"

View File

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

View File

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

1
go.mod
View File

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

2
go.sum
View File

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

View File

@ -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()

View File

@ -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"
}

View File

@ -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) {

View File

@ -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)

View File

@ -18,6 +18,7 @@ type Config struct {
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
}

View File

@ -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, " ")

View File

@ -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 {

View File

@ -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

192
provisioner/elevated.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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`))

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More