Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44eda339d3 | |||
| 2152fa3313 | |||
| fbfe31ceaf | |||
| 7de6c21a8a | |||
| f28c06a861 | |||
| ada2a431ef | |||
| 99800619b7 | |||
| 5b27fc7d61 | |||
| a1524bf96a | |||
| 01d45d67c8 | |||
| 6307710485 | |||
| 229e80bcba | |||
| a2cfaace59 | |||
| 3b2bedf794 | |||
| 20eeffee0d | |||
| b0d2201d55 | |||
| 0a5cd953fc | |||
| aa2418cf01 | |||
| c88ff4ec45 | |||
| d74b47b734 | |||
| 3cdf1d6234 | |||
| 1a0d5a93ee | |||
| 788dc32598 | |||
| d20b34e71a | |||
| 46a26a3d9e | |||
| 56f6a976de | |||
| 156b6ac2b4 | |||
| 9c8daed65e | |||
| 1a248e4868 | |||
| 2dcf950441 | |||
| d486459a00 | |||
| 89d57b8635 | |||
| 086141f9a9 | |||
| 8b4993e44c | |||
| 39a8dee4ea | |||
| 99f503a22b | |||
| 62c3743890 | |||
| acba3ea3e7 | |||
| a2277e312f | |||
| 29b08e54bc | |||
| 321aa090df | |||
| 2a80bff8c2 | |||
| b5b28c55fb | |||
| 87b290ae67 | |||
| 78c280b1b7 | |||
| e97c56dc72 | |||
| 55812fe8a5 | |||
| 5700f74c8c | |||
| d91839b93e | |||
| ba6d11d518 | |||
| 54469c4728 | |||
| e475db6a66 | |||
| 9d453624ff | |||
| 956651e711 | |||
| 7abeafaa04 | |||
| 6f8a909cef | |||
| 0aa4d7117d | |||
| 0a3ff3b190 | |||
| 3a1fc8c6f4 | |||
| 6e918c4bd3 | |||
| 5016df92b8 | |||
| 04f952a2f4 | |||
| aed97e1a8b | |||
| 920da9d0a4 | |||
| 703023d47e | |||
| 13d1b50d8a | |||
| 2ffbe8240f | |||
| 8666ebceca | |||
| 33e3f62ff7 | |||
| 027eb40a97 | |||
| 676c3a21bf | |||
| 4c3912bfdc | |||
| c3ba818893 | |||
| 05c46635a3 | |||
| f8bb9008c0 | |||
| 5f119a5464 | |||
| 0f399ace57 | |||
| 7c28d5590c | |||
| c0757a1bba | |||
| d3f48622a3 | |||
| 80960d7a64 | |||
| bde75c9a96 | |||
| a447d151bb | |||
| d826711e7a | |||
| daccfc42cf | |||
| c43d99ccfd | |||
| 6cb0a381a4 | |||
| 23d5432333 | |||
| 109b2a0ca9 | |||
| 94c9b87431 | |||
| 33eb8483ba | |||
| f824cbb79d | |||
| 67479f0b23 | |||
| a704e1b6b1 | |||
| b72a75278a | |||
| ae23f891e1 | |||
| 69d951d34f | |||
| 15a008a408 | |||
| ecb825ee7a | |||
| 03220c0b94 | |||
| bf7cc2587e | |||
| 4c627c57ff | |||
| fc8346bd27 | |||
| 46364ddf34 | |||
| 97acdf2374 | |||
| 8f362656ee | |||
| 8ea6414969 | |||
| 759d2451a4 | |||
| 643d6d165b | |||
| ddd85da1a6 | |||
| dd2927f871 | |||
| 484dbfe6ad | |||
| 961d9b4f08 | |||
| fa163c2514 | |||
| 6d1a146143 | |||
| 24739270cf | |||
| cf999e07db | |||
| cdd33d3ff2 | |||
| a2ea308881 | |||
| 04d7a10aa0 | |||
| 78ce1dbde9 | |||
| 4964c54f60 | |||
| 6d0b5a6957 | |||
| 25f2ec48d3 | |||
| 09852b89a7 | |||
| ab52d83fb0 | |||
| de5c5873a2 | |||
| 3f61498704 | |||
| 171d725425 | |||
| fd683daa22 | |||
| b52143f528 | |||
| dcacf7ad4c | |||
| aede34079d | |||
| a53e81d9a2 | |||
| 9181a435aa | |||
| 6b8d086e94 | |||
| 825deef43d | |||
| dd37a10355 | |||
| f295b6d2e5 | |||
| 6a8f49ad1d | |||
| c63e260d7d | |||
| 2ac9f5a276 | |||
| 5493285f65 | |||
| 133c8a68b0 | |||
| fb1a4bd86f | |||
| 5ce6c8564c | |||
| 13a4e47efe | |||
| 364c415294 | |||
| 3ce148110c | |||
| a28edbaa0b | |||
| c6e9bde97b |
@@ -8,6 +8,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Add track-internal
|
- name: Add track-internal
|
||||||
uses: andymckay/labeler@1.0.2
|
uses: andymckay/labeler@1.0.2
|
||||||
|
if: github.event.issue.pull_request == null
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.Github_Token }}
|
repo-token: ${{ secrets.Github_Token }}
|
||||||
add-labels: "track-internal"
|
add-labels: "track-internal"
|
||||||
|
|||||||
@@ -1,5 +1,67 @@
|
|||||||
## 1.6.2 (Upcoming)
|
## 1.6.2 (Upcoming)
|
||||||
|
|
||||||
|
### IMPROVEMENTS:
|
||||||
|
* builder/amazon: Add all of the custom AWS template engines to `build`
|
||||||
|
template function for use by provisioners. [GH-9751]
|
||||||
|
* builder/amazon: Add aws_polling config option to override env variables.
|
||||||
|
[GH-9777]
|
||||||
|
* builder/azure: Add FreeBSD support to azure/chroot builder. [GH-9697]
|
||||||
|
* builder/vmware-esx: Add `network_name` option to vmware so that users can set
|
||||||
|
a network without using vmx data. [GH-9718]
|
||||||
|
* builder/vmware: Add a `remote_output_directory option` so users can tell
|
||||||
|
Packer where on a datastore to create a vm. [GH-9784]
|
||||||
|
* builder/vmware: Add progress tracker to vmware-esx5 iso upload. [GH-9779]
|
||||||
|
* builder/vsphere: Add new `directory_permission` config export option.
|
||||||
|
[GH-9704]
|
||||||
|
* builder/vsphere: Add option to import OVF templates to the Content Library
|
||||||
|
[GH-9755]
|
||||||
|
* builder/vsphere: Add step and options to customize cloned VMs. [GH-9665]
|
||||||
|
* core/hcl: Add provisioner "override" option to HCL2 templates. [GH-9764]
|
||||||
|
* core/hcl: Add vault integration as an HCL2 function function. [GH-9746]
|
||||||
|
* core: Add colored prefix to progress bar so it's clearer what build each
|
||||||
|
progress bar belongs to. [GH-9780]
|
||||||
|
* core: Ui now pretty prints build durations. [GH-9749]
|
||||||
|
* core: When a build is cancelled, Packer will skip postprocessors gracefully
|
||||||
|
rather than failing them. [GH-9720]
|
||||||
|
* integrations/secretsmanager: Add support for plaintext non-key/pair secrets.
|
||||||
|
[GH-9773]
|
||||||
|
* post-processor/vsphere: Improve UI to catch bad credentials and print errors.
|
||||||
|
[GH-9649]
|
||||||
|
* provisioner/file: Clean up, bugfix, and document previously-hidden `sources`
|
||||||
|
option. [GH-9725] [GH-9735]
|
||||||
|
* provisioner/salt-masterless: Add option to option to download community
|
||||||
|
SaltStack formulas. [GH-9726]
|
||||||
|
|
||||||
|
### BUG FIXES:
|
||||||
|
* build: Fix bug in code generator that caused generation to fail in nested
|
||||||
|
packer/packer dirs [GH-9728]
|
||||||
|
* build: Fix Makefile so that default target doesn't crash and creates dev
|
||||||
|
binaries. [GH-9706]
|
||||||
|
* builder/amazon-ebssurrogate: Make skip_save_build_region option work in the
|
||||||
|
ebssurrogate builder, not just the ebs builder. [GH-9666]
|
||||||
|
* builder/file: Make sure that UploadDir receives the interpolated destination.
|
||||||
|
[GH-9698]
|
||||||
|
* builder/googlecompute: Fix bug where startup script hang would cause export
|
||||||
|
to hang. [GH-9708]
|
||||||
|
* builder/hyperv: Send boot command in small chunks to make it more stable.
|
||||||
|
[GH-9765]
|
||||||
|
* builder/scaleway: Fix config issue that made scaleway builder fail when used
|
||||||
|
with HCL2 config. [GH-9677]
|
||||||
|
* builder/vmware: Fully destroy vm if it was cancelled or errored. This will
|
||||||
|
make orphaned vms easier to destroy through vCenter. [GH-9782]
|
||||||
|
* builder/vsphere: Fix `alt`, `ctrl`, and `shift` keypresses in the
|
||||||
|
boot_command. [GH-9702] [GH-9739]
|
||||||
|
* builder/vsphere: Fix bug where Packer timed out if two interfaces were
|
||||||
|
defined but only one had an available IP. [GH-9748]
|
||||||
|
* builder/vsphere: Fix the configuration_parameters option so that it is always
|
||||||
|
applied, not just when the tool sync policy is set. [GH-9713]
|
||||||
|
* communicator: Fix `pause_before_connect` option to force a reconnect after
|
||||||
|
the pause. [GH-9772]
|
||||||
|
* core: Make `max_retries` provisioner option a string to allow variable
|
||||||
|
interpolation. [GH-9673]
|
||||||
|
* post-processor/vsphere-template: Fix ReregisterVM to default to true instead
|
||||||
|
of false. [GH-9736]
|
||||||
|
|
||||||
## 1.6.1 (July 30, 2020)
|
## 1.6.1 (July 30, 2020)
|
||||||
|
|
||||||
### BACKWARDS INCOMPATABILITIES:
|
### BACKWARDS INCOMPATABILITIES:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export GOLDFLAGS
|
|||||||
.PHONY: bin checkversion ci ci-lint default install-build-deps install-gen-deps fmt fmt-docs fmt-examples generate install-lint-deps lint \
|
.PHONY: bin checkversion ci ci-lint default install-build-deps install-gen-deps fmt fmt-docs fmt-examples generate install-lint-deps lint \
|
||||||
releasebin test testacc testrace
|
releasebin test testacc testrace
|
||||||
|
|
||||||
default: install-build-deps install-gen-deps generate testrace dev releasebin package dev fmt fmt-check mode-check fmt-docs fmt-examples
|
default: install-build-deps install-gen-deps generate bin
|
||||||
|
|
||||||
ci: testrace ## Test in continuous integration
|
ci: testrace ## Test in continuous integration
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,9 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
||||||
generatedData := []string{"SourceAMIName", "Device", "MountPath"}
|
generatedData := awscommon.GetGeneratedDataList()
|
||||||
|
generatedData = append(generatedData, "Device", "MountPath")
|
||||||
|
|
||||||
return generatedData, warns, nil
|
return generatedData, warns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,12 +398,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
GeneratedData: generatedData,
|
GeneratedData: generatedData,
|
||||||
},
|
},
|
||||||
&StepCreateVolume{
|
&StepCreateVolume{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
RootVolumeType: b.config.RootVolumeType,
|
RootVolumeType: b.config.RootVolumeType,
|
||||||
RootVolumeSize: b.config.RootVolumeSize,
|
RootVolumeSize: b.config.RootVolumeSize,
|
||||||
RootVolumeTags: b.config.RootVolumeTags,
|
RootVolumeTags: b.config.RootVolumeTags,
|
||||||
Ctx: b.config.ctx,
|
Ctx: b.config.ctx,
|
||||||
},
|
},
|
||||||
&StepAttachVolume{},
|
&StepAttachVolume{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
|
},
|
||||||
&StepEarlyUnflock{},
|
&StepEarlyUnflock{},
|
||||||
&chroot.StepPreMountCommands{
|
&chroot.StepPreMountCommands{
|
||||||
Commands: b.config.PreMountCommands,
|
Commands: b.config.PreMountCommands,
|
||||||
@@ -420,9 +425,14 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
&chroot.StepCopyFiles{
|
&chroot.StepCopyFiles{
|
||||||
Files: b.config.CopyFiles,
|
Files: b.config.CopyFiles,
|
||||||
},
|
},
|
||||||
|
&awscommon.StepSetGeneratedData{
|
||||||
|
GeneratedData: generatedData,
|
||||||
|
},
|
||||||
&chroot.StepChrootProvision{},
|
&chroot.StepChrootProvision{},
|
||||||
&chroot.StepEarlyCleanup{},
|
&chroot.StepEarlyCleanup{},
|
||||||
&StepSnapshot{},
|
&StepSnapshot{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
|
},
|
||||||
&awscommon.StepDeregisterAMI{
|
&awscommon.StepDeregisterAMI{
|
||||||
AccessConfig: &b.config.AccessConfig,
|
AccessConfig: &b.config.AccessConfig,
|
||||||
ForceDeregister: b.config.AMIForceDeregister,
|
ForceDeregister: b.config.AMIForceDeregister,
|
||||||
@@ -435,6 +445,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
|
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
|
||||||
EnableAMIENASupport: b.config.AMIENASupport,
|
EnableAMIENASupport: b.config.AMIENASupport,
|
||||||
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
},
|
},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
AccessConfig: &b.config.AccessConfig,
|
AccessConfig: &b.config.AccessConfig,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type FlatConfig struct {
|
|||||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||||
|
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||||
AMIMappings []common.FlatBlockDevice `mapstructure:"ami_block_device_mappings" hcl2-schema-generator:"ami_block_device_mappings,direct" required:"false" cty:"ami_block_device_mappings" hcl:"ami_block_device_mappings"`
|
AMIMappings []common.FlatBlockDevice `mapstructure:"ami_block_device_mappings" hcl2-schema-generator:"ami_block_device_mappings,direct" required:"false" cty:"ami_block_device_mappings" hcl:"ami_block_device_mappings"`
|
||||||
ChrootMounts [][]string `mapstructure:"chroot_mounts" required:"false" cty:"chroot_mounts" hcl:"chroot_mounts"`
|
ChrootMounts [][]string `mapstructure:"chroot_mounts" required:"false" cty:"chroot_mounts" hcl:"chroot_mounts"`
|
||||||
CommandWrapper *string `mapstructure:"command_wrapper" required:"false" cty:"command_wrapper" hcl:"command_wrapper"`
|
CommandWrapper *string `mapstructure:"command_wrapper" required:"false" cty:"command_wrapper" hcl:"command_wrapper"`
|
||||||
@@ -127,6 +128,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||||
|
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||||
"ami_block_device_mappings": &hcldec.BlockListSpec{TypeName: "ami_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
|
"ami_block_device_mappings": &hcldec.BlockListSpec{TypeName: "ami_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
|
||||||
"chroot_mounts": &hcldec.AttrSpec{Name: "chroot_mounts", Type: cty.List(cty.List(cty.String)), Required: false},
|
"chroot_mounts": &hcldec.AttrSpec{Name: "chroot_mounts", Type: cty.List(cty.List(cty.String)), Required: false},
|
||||||
"command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false},
|
"command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false},
|
||||||
|
|||||||
@@ -227,10 +227,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
|
|||||||
if generatedData[0] != "SourceAMIName" {
|
if generatedData[0] != "SourceAMIName" {
|
||||||
t.Fatalf("Generated data should contain SourceAMIName")
|
t.Fatalf("Generated data should contain SourceAMIName")
|
||||||
}
|
}
|
||||||
if generatedData[1] != "Device" {
|
if generatedData[1] != "BuildRegion" {
|
||||||
|
t.Fatalf("Generated data should contain BuildRegion")
|
||||||
|
}
|
||||||
|
if generatedData[2] != "SourceAMI" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMI")
|
||||||
|
}
|
||||||
|
if generatedData[3] != "SourceAMICreationDate" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMICreationDate")
|
||||||
|
}
|
||||||
|
if generatedData[4] != "SourceAMIOwner" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwner")
|
||||||
|
}
|
||||||
|
if generatedData[5] != "SourceAMIOwnerName" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwnerName")
|
||||||
|
}
|
||||||
|
if generatedData[6] != "Device" {
|
||||||
t.Fatalf("Generated data should contain Device")
|
t.Fatalf("Generated data should contain Device")
|
||||||
}
|
}
|
||||||
if generatedData[2] != "MountPath" {
|
if generatedData[7] != "MountPath" {
|
||||||
t.Fatalf("Generated data should contain MountPath")
|
t.Fatalf("Generated data should contain MountPath")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import (
|
|||||||
// device string - The location where the volume was attached.
|
// device string - The location where the volume was attached.
|
||||||
// attach_cleanup CleanupFunc
|
// attach_cleanup CleanupFunc
|
||||||
type StepAttachVolume struct {
|
type StepAttachVolume struct {
|
||||||
attached bool
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
volumeId string
|
attached bool
|
||||||
|
volumeId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepAttachVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
func (s *StepAttachVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
@@ -51,7 +52,7 @@ func (s *StepAttachVolume) Run(ctx context.Context, state multistep.StateBag) mu
|
|||||||
s.volumeId = volumeId
|
s.volumeId = volumeId
|
||||||
|
|
||||||
// Wait for the volume to become attached
|
// Wait for the volume to become attached
|
||||||
err = awscommon.WaitUntilVolumeAttached(ctx, ec2conn, s.volumeId)
|
err = s.PollingConfig.WaitUntilVolumeAttached(ctx, ec2conn, s.volumeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error waiting for volume: %s", err)
|
err := fmt.Errorf("Error waiting for volume: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
@@ -87,7 +88,7 @@ func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
|
|||||||
s.attached = false
|
s.attached = false
|
||||||
|
|
||||||
// Wait for the volume to detach
|
// Wait for the volume to detach
|
||||||
err = awscommon.WaitUntilVolumeDetached(aws.BackgroundContext(), ec2conn, s.volumeId)
|
err = s.PollingConfig.WaitUntilVolumeDetached(aws.BackgroundContext(), ec2conn, s.volumeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error waiting for volume: %s", err)
|
return fmt.Errorf("Error waiting for volume: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
// Produces:
|
// Produces:
|
||||||
// volume_id string - The ID of the created volume
|
// volume_id string - The ID of the created volume
|
||||||
type StepCreateVolume struct {
|
type StepCreateVolume struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
volumeId string
|
volumeId string
|
||||||
RootVolumeSize int64
|
RootVolumeSize int64
|
||||||
RootVolumeType string
|
RootVolumeType string
|
||||||
@@ -110,7 +111,7 @@ func (s *StepCreateVolume) Run(ctx context.Context, state multistep.StateBag) mu
|
|||||||
log.Printf("Volume ID: %s", s.volumeId)
|
log.Printf("Volume ID: %s", s.volumeId)
|
||||||
|
|
||||||
// Wait for the volume to become ready
|
// Wait for the volume to become ready
|
||||||
err = awscommon.WaitUntilVolumeAvailable(ctx, ec2conn, s.volumeId)
|
err = s.PollingConfig.WaitUntilVolumeAvailable(ctx, ec2conn, s.volumeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error waiting for volume: %s", err)
|
err := fmt.Errorf("Error waiting for volume: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
// StepRegisterAMI creates the AMI.
|
// StepRegisterAMI creates the AMI.
|
||||||
type StepRegisterAMI struct {
|
type StepRegisterAMI struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
RootVolumeSize int64
|
RootVolumeSize int64
|
||||||
EnableAMIENASupport confighelper.Trilean
|
EnableAMIENASupport confighelper.Trilean
|
||||||
EnableAMISriovNetSupport bool
|
EnableAMISriovNetSupport bool
|
||||||
@@ -81,7 +82,7 @@ func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) mul
|
|||||||
state.Put("amis", amis)
|
state.Put("amis", amis)
|
||||||
|
|
||||||
ui.Say("Waiting for AMI to become ready...")
|
ui.Say("Waiting for AMI to become ready...")
|
||||||
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
if err := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
||||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import (
|
|||||||
// Produces:
|
// Produces:
|
||||||
// snapshot_id string - ID of the created snapshot
|
// snapshot_id string - ID of the created snapshot
|
||||||
type StepSnapshot struct {
|
type StepSnapshot struct {
|
||||||
snapshotId string
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
|
snapshotId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
func (s *StepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
@@ -43,7 +44,7 @@ func (s *StepSnapshot) Run(ctx context.Context, state multistep.StateBag) multis
|
|||||||
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
|
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
|
||||||
|
|
||||||
// Wait for the snapshot to be ready
|
// Wait for the snapshot to be ready
|
||||||
err = awscommon.WaitUntilSnapshotDone(ctx, ec2conn, s.snapshotId)
|
err = s.PollingConfig.WaitUntilSnapshotDone(ctx, ec2conn, s.snapshotId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error waiting for snapshot: %s", err)
|
err := fmt.Errorf("Error waiting for snapshot: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ type AccessConfig struct {
|
|||||||
// }
|
// }
|
||||||
// ```
|
// ```
|
||||||
VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false"`
|
VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false"`
|
||||||
|
// [Polling configuration](#polling-configuration) for the AWS waiter. Configures the waiter that checks
|
||||||
|
// resource state.
|
||||||
|
PollingConfig *AWSPollingConfig `mapstructure:"aws_polling" required:"false"`
|
||||||
|
|
||||||
getEC2Connection func() ec2iface.EC2API
|
getEC2Connection func() ec2iface.EC2API
|
||||||
}
|
}
|
||||||
@@ -215,7 +218,6 @@ func (c *AccessConfig) Session() (*session.Session, error) {
|
|||||||
if c.DecodeAuthZMessages {
|
if c.DecodeAuthZMessages {
|
||||||
DecodeAuthZMessages(c.session)
|
DecodeAuthZMessages(c.session)
|
||||||
}
|
}
|
||||||
LogEnvOverrideWarnings()
|
|
||||||
|
|
||||||
return c.session, nil
|
return c.session, nil
|
||||||
}
|
}
|
||||||
@@ -296,6 +298,11 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
|||||||
fmt.Errorf("`access_key` and `secret_key` must both be either set or not set."))
|
fmt.Errorf("`access_key` and `secret_key` must both be either set or not set."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.PollingConfig == nil {
|
||||||
|
c.PollingConfig = new(AWSPollingConfig)
|
||||||
|
}
|
||||||
|
c.PollingConfig.LogEnvOverrideWarnings()
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func testAccessConfig() *AccessConfig {
|
|||||||
getEC2Connection: func() ec2iface.EC2API {
|
getEC2Connection: func() ec2iface.EC2API {
|
||||||
return &mockEC2Client{}
|
return &mockEC2Client{}
|
||||||
},
|
},
|
||||||
|
PollingConfig: new(AWSPollingConfig),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,18 +79,32 @@ type AMIConfig struct {
|
|||||||
// Default false.
|
// Default false.
|
||||||
AMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot" required:"false"`
|
AMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot" required:"false"`
|
||||||
// Whether or not to encrypt the resulting AMI when
|
// Whether or not to encrypt the resulting AMI when
|
||||||
// copying a provisioned instance to an AMI. By default, Packer will keep the
|
// copying a provisioned instance to an AMI. By default, Packer will keep
|
||||||
// encryption setting to what it was in the source image. Setting false will
|
// the encryption setting to what it was in the source image. Setting false
|
||||||
// result in an unencrypted image, and true will result in an encrypted one.
|
// will result in an unencrypted image, and true will result in an encrypted
|
||||||
|
// one.
|
||||||
|
//
|
||||||
// If you have used the `launch_block_device_mappings` to set an encryption
|
// If you have used the `launch_block_device_mappings` to set an encryption
|
||||||
// key and that key is the same as the one you want the image encrypted with
|
// key and that key is the same as the one you want the image encrypted with
|
||||||
// at the end, then you don't need to set this field; leaving it empty will
|
// at the end, then you don't need to set this field; leaving it empty will
|
||||||
// prevent an unnecessary extra copy step and save you some time.
|
// prevent an unnecessary extra copy step and save you some time.
|
||||||
AMIEncryptBootVolume config.Trilean `mapstructure:"encrypt_boot" required:"false"`
|
AMIEncryptBootVolume config.Trilean `mapstructure:"encrypt_boot" required:"false"`
|
||||||
// ID, alias or ARN of the KMS key to use for boot volume encryption. This
|
// ID, alias or ARN of the KMS key to use for AMI encryption. This
|
||||||
// only applies to the main `region`, other regions where the AMI will be
|
// only applies to the main `region` -- any regions the AMI gets copied to
|
||||||
// copied will be encrypted by the default EBS KMS key. For valid formats
|
// copied will be encrypted by the default EBS KMS key for that region,
|
||||||
// see *KmsKeyId* in the [AWS API docs -
|
// unless you set region-specific keys in AMIRegionKMSKeyIDs.
|
||||||
|
//
|
||||||
|
// Set this value if you select `encrypt_boot`, but don't want to use the
|
||||||
|
// region's default KMS key.
|
||||||
|
//
|
||||||
|
// If you have a custom kms key you'd like to apply to the launch volume,
|
||||||
|
// and are only building in one region, it is more efficient to leave this
|
||||||
|
// and `encrypt_boot` empty and to instead set the key id in the
|
||||||
|
// launch_block_device_mappings (you can find an example below). This saves
|
||||||
|
// potentially many minutes at the end of the build by preventing Packer
|
||||||
|
// from having to copy and re-encrypt the image at the end of the build.
|
||||||
|
//
|
||||||
|
// For valid formats see *KmsKeyId* in the [AWS API docs -
|
||||||
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
|
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
|
||||||
// This field is validated by Packer, when using an alias, you will have to
|
// This field is validated by Packer, when using an alias, you will have to
|
||||||
// prefix `kms_key_id` with `alias/`.
|
// prefix `kms_key_id` with `alias/`.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/hashicorp/packer/template/interpolate"
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These will be attached when booting a new instance from your AMI. Your
|
// These will be attached when launching your instance. Your
|
||||||
// options here may vary depending on the type of VM you use.
|
// options here may vary depending on the type of VM you use.
|
||||||
//
|
//
|
||||||
// Example use case:
|
// Example use case:
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
// JSON example:
|
// JSON example:
|
||||||
//
|
//
|
||||||
// ```json
|
// ```json
|
||||||
// ami_block_device_mappings: [
|
// launch_block_device_mappings: [
|
||||||
// {
|
// {
|
||||||
// "device_name": "/dev/sda1",
|
// "device_name": "/dev/sda1",
|
||||||
// "encrypted": true,
|
// "encrypted": true,
|
||||||
@@ -36,13 +36,16 @@ import (
|
|||||||
// HCL2 example:
|
// HCL2 example:
|
||||||
//
|
//
|
||||||
// ```hcl
|
// ```hcl
|
||||||
// ami_block_device_mappings {
|
// launch_block_device_mappings {
|
||||||
// device_name = "/dev/sda1"
|
// device_name = "/dev/sda1"
|
||||||
// encrypted = true
|
// encrypted = true
|
||||||
// kms_key_id = "1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"
|
// kms_key_id = "1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"
|
||||||
// }
|
// }
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
|
// Please note that the kms_key_id option in this example exists for
|
||||||
|
// launch_block_device_mappings but not ami_block_device_mappings.
|
||||||
|
//
|
||||||
// Documentation for Block Devices Mappings can be found here:
|
// Documentation for Block Devices Mappings can be found here:
|
||||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
||||||
//
|
//
|
||||||
@@ -80,12 +83,14 @@ type BlockDevice struct {
|
|||||||
// The size of the volume, in GiB. Required if not specifying a
|
// The size of the volume, in GiB. Required if not specifying a
|
||||||
// snapshot_id.
|
// snapshot_id.
|
||||||
VolumeSize int64 `mapstructure:"volume_size" required:"false"`
|
VolumeSize int64 `mapstructure:"volume_size" required:"false"`
|
||||||
// ID, alias or ARN of the KMS key to use for boot volume encryption. This
|
// ID, alias or ARN of the KMS key to use for boot volume encryption.
|
||||||
// only applies to the main region, other regions where the AMI will be
|
// This option exists for launch_block_device_mappings but not
|
||||||
// copied will be encrypted by the default EBS KMS key. For valid formats
|
// ami_block_device_mappings. The kms key id defined here only applies to
|
||||||
// see KmsKeyId in the [AWS API docs -
|
// the original build region; if the AMI gets copied to other regions, the
|
||||||
|
// volume in those regions will be encrypted by the default EBS KMS key.
|
||||||
|
// For valid formats see KmsKeyId in the [AWS API docs -
|
||||||
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html)
|
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html)
|
||||||
// This field is validated by Packer, when using an alias, you will have to
|
// This field is validated by Packer. When using an alias, you will have to
|
||||||
// prefix kms_key_id with alias/.
|
// prefix kms_key_id with alias/.
|
||||||
KmsKeyId string `mapstructure:"kms_key_id" required:"false"`
|
KmsKeyId string `mapstructure:"kms_key_id" required:"false"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ func extractBuildInfo(region string, state multistep.StateBag, generatedData *bu
|
|||||||
SourceAMIOwnerName: aws.StringValue(sourceAMI.ImageOwnerAlias),
|
SourceAMIOwnerName: aws.StringValue(sourceAMI.ImageOwnerAlias),
|
||||||
SourceAMITags: sourceAMITags,
|
SourceAMITags: sourceAMITags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generatedData.Put("BuildRegion", buildInfoTemplate.BuildRegion)
|
||||||
|
generatedData.Put("SourceAMI", buildInfoTemplate.SourceAMI)
|
||||||
|
generatedData.Put("SourceAMICreationDate", buildInfoTemplate.SourceAMICreationDate)
|
||||||
generatedData.Put("SourceAMIName", buildInfoTemplate.SourceAMIName)
|
generatedData.Put("SourceAMIName", buildInfoTemplate.SourceAMIName)
|
||||||
|
generatedData.Put("SourceAMIOwner", buildInfoTemplate.SourceAMIOwner)
|
||||||
|
generatedData.Put("SourceAMIOwnerName", buildInfoTemplate.SourceAMIOwnerName)
|
||||||
|
|
||||||
return buildInfoTemplate
|
return buildInfoTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGeneratedDataList() []string {
|
||||||
|
return []string{
|
||||||
|
"SourceAMIName",
|
||||||
|
"BuildRegion",
|
||||||
|
"SourceAMI",
|
||||||
|
"SourceAMICreationDate",
|
||||||
|
"SourceAMIOwner",
|
||||||
|
"SourceAMIOwnerName",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:generate struct-markdown
|
||||||
|
//go:generate mapstructure-to-hcl2 -type AWSPollingConfig
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -38,12 +40,45 @@ type StateChangeConf struct {
|
|||||||
// Following are wrapper functions that use Packer's environment-variables to
|
// Following are wrapper functions that use Packer's environment-variables to
|
||||||
// determine retry logic, then call the AWS SDK's built-in waiters.
|
// determine retry logic, then call the AWS SDK's built-in waiters.
|
||||||
|
|
||||||
func WaitUntilAMIAvailable(ctx aws.Context, conn ec2iface.EC2API, imageId string) error {
|
// Polling configuration for the AWS waiter. Configures the waiter for resources creation or actions like attaching
|
||||||
|
// volumes or importing image.
|
||||||
|
// Usage example:
|
||||||
|
//
|
||||||
|
// In JSON:
|
||||||
|
// ```json
|
||||||
|
// "aws_polling" : {
|
||||||
|
// "delay_seconds": 30,
|
||||||
|
// "max_attempts": 50
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// aws_polling {
|
||||||
|
// delay_seconds = 30
|
||||||
|
// max_attempts = 50
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
type AWSPollingConfig struct {
|
||||||
|
// Specifies the maximum number of attempts the waiter will check for resource state.
|
||||||
|
// This value can also be set via the AWS_MAX_ATTEMPTS.
|
||||||
|
// If both option and environment variable are set, the max_attempts will be considered over the AWS_MAX_ATTEMPTS.
|
||||||
|
// If none is set, defaults to AWS waiter default which is 40 max_attempts.
|
||||||
|
MaxAttempts int `mapstructure:"max_attempts" required:"false"`
|
||||||
|
// Specifies the delay in seconds between attempts to check the resource state.
|
||||||
|
// This value can also be set via the AWS_POLL_DELAY_SECONDS.
|
||||||
|
// If both option and environment variable are set, the delay_seconds will be considered over the AWS_POLL_DELAY_SECONDS.
|
||||||
|
// If none is set, defaults to AWS waiter default which is 15 seconds.
|
||||||
|
DelaySeconds int `mapstructure:"delay_seconds" required:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AWSPollingConfig) WaitUntilAMIAvailable(ctx aws.Context, conn ec2iface.EC2API, imageId string) error {
|
||||||
imageInput := ec2.DescribeImagesInput{
|
imageInput := ec2.DescribeImagesInput{
|
||||||
ImageIds: []*string{&imageId},
|
ImageIds: []*string{&imageId},
|
||||||
}
|
}
|
||||||
|
|
||||||
waitOpts := getWaiterOptions()
|
waitOpts := w.getWaiterOptions()
|
||||||
if len(waitOpts) == 0 {
|
if len(waitOpts) == 0 {
|
||||||
// Bump this default to 30 minutes because the aws default
|
// Bump this default to 30 minutes because the aws default
|
||||||
// of ten minutes doesn't work for some of our long-running copies.
|
// of ten minutes doesn't work for some of our long-running copies.
|
||||||
@@ -66,7 +101,7 @@ func WaitUntilAMIAvailable(ctx aws.Context, conn ec2iface.EC2API, imageId string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilInstanceRunning(ctx aws.Context, conn *ec2.EC2, instanceId string) error {
|
func (w *AWSPollingConfig) WaitUntilInstanceRunning(ctx aws.Context, conn *ec2.EC2, instanceId string) error {
|
||||||
|
|
||||||
instanceInput := ec2.DescribeInstancesInput{
|
instanceInput := ec2.DescribeInstancesInput{
|
||||||
InstanceIds: []*string{&instanceId},
|
InstanceIds: []*string{&instanceId},
|
||||||
@@ -75,12 +110,11 @@ func WaitUntilInstanceRunning(ctx aws.Context, conn *ec2.EC2, instanceId string)
|
|||||||
err := conn.WaitUntilInstanceRunningWithContext(
|
err := conn.WaitUntilInstanceRunningWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
&instanceInput,
|
&instanceInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId string) error {
|
func (w *AWSPollingConfig) WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId string) error {
|
||||||
|
|
||||||
instanceInput := ec2.DescribeInstancesInput{
|
instanceInput := ec2.DescribeInstancesInput{
|
||||||
InstanceIds: []*string{&instanceId},
|
InstanceIds: []*string{&instanceId},
|
||||||
}
|
}
|
||||||
@@ -88,12 +122,12 @@ func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId stri
|
|||||||
err := conn.WaitUntilInstanceTerminatedWithContext(
|
err := conn.WaitUntilInstanceTerminatedWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
&instanceInput,
|
&instanceInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function works for both requesting and cancelling spot instances.
|
// This function works for both requesting and cancelling spot instances.
|
||||||
func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId string) error {
|
func (w *AWSPollingConfig) WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId string) error {
|
||||||
spotRequestInput := ec2.DescribeSpotInstanceRequestsInput{
|
spotRequestInput := ec2.DescribeSpotInstanceRequestsInput{
|
||||||
SpotInstanceRequestIds: []*string{&spotRequestId},
|
SpotInstanceRequestIds: []*string{&spotRequestId},
|
||||||
}
|
}
|
||||||
@@ -101,11 +135,11 @@ func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId
|
|||||||
err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext(
|
err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
&spotRequestInput,
|
&spotRequestInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
func (w *AWSPollingConfig) WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
||||||
volumeInput := ec2.DescribeVolumesInput{
|
volumeInput := ec2.DescribeVolumesInput{
|
||||||
VolumeIds: []*string{&volumeId},
|
VolumeIds: []*string{&volumeId},
|
||||||
}
|
}
|
||||||
@@ -113,11 +147,11 @@ func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) e
|
|||||||
err := conn.WaitUntilVolumeAvailableWithContext(
|
err := conn.WaitUntilVolumeAvailableWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
&volumeInput,
|
&volumeInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) error {
|
func (w *AWSPollingConfig) WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) error {
|
||||||
snapInput := ec2.DescribeSnapshotsInput{
|
snapInput := ec2.DescribeSnapshotsInput{
|
||||||
SnapshotIds: []*string{&snapshotID},
|
SnapshotIds: []*string{&snapshotID},
|
||||||
}
|
}
|
||||||
@@ -125,13 +159,13 @@ func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) er
|
|||||||
err := conn.WaitUntilSnapshotCompletedWithContext(
|
err := conn.WaitUntilSnapshotCompletedWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
&snapInput,
|
&snapInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrappers for our custom AWS waiters
|
// Wrappers for our custom AWS waiters
|
||||||
|
|
||||||
func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
func (w *AWSPollingConfig) WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
||||||
volumeInput := ec2.DescribeVolumesInput{
|
volumeInput := ec2.DescribeVolumesInput{
|
||||||
VolumeIds: []*string{&volumeId},
|
VolumeIds: []*string{&volumeId},
|
||||||
}
|
}
|
||||||
@@ -139,11 +173,11 @@ func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) er
|
|||||||
err := WaitForVolumeToBeAttached(conn,
|
err := WaitForVolumeToBeAttached(conn,
|
||||||
ctx,
|
ctx,
|
||||||
&volumeInput,
|
&volumeInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
func (w *AWSPollingConfig) WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
|
||||||
volumeInput := ec2.DescribeVolumesInput{
|
volumeInput := ec2.DescribeVolumesInput{
|
||||||
VolumeIds: []*string{&volumeId},
|
VolumeIds: []*string{&volumeId},
|
||||||
}
|
}
|
||||||
@@ -151,11 +185,11 @@ func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) er
|
|||||||
err := WaitForVolumeToBeDetached(conn,
|
err := WaitForVolumeToBeDetached(conn,
|
||||||
ctx,
|
ctx,
|
||||||
&volumeInput,
|
&volumeInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error {
|
func (w *AWSPollingConfig) WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error {
|
||||||
importInput := ec2.DescribeImportImageTasksInput{
|
importInput := ec2.DescribeImportImageTasksInput{
|
||||||
ImportTaskIds: []*string{&taskID},
|
ImportTaskIds: []*string{&taskID},
|
||||||
}
|
}
|
||||||
@@ -163,7 +197,7 @@ func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error
|
|||||||
err := WaitForImageToBeImported(conn,
|
err := WaitForImageToBeImported(conn,
|
||||||
ctx,
|
ctx,
|
||||||
&importInput,
|
&importInput,
|
||||||
getWaiterOptions()...)
|
w.getWaiterOptions()...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +332,18 @@ type overridableWaitVars struct {
|
|||||||
awsTimeoutSeconds envInfo
|
awsTimeoutSeconds envInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWaiterOptions() []request.WaiterOption {
|
func (w *AWSPollingConfig) getWaiterOptions() []request.WaiterOption {
|
||||||
envOverrides := getEnvOverrides()
|
envOverrides := getEnvOverrides()
|
||||||
|
|
||||||
|
if w.MaxAttempts != 0 {
|
||||||
|
envOverrides.awsMaxAttempts.Val = w.MaxAttempts
|
||||||
|
envOverrides.awsMaxAttempts.overridden = true
|
||||||
|
}
|
||||||
|
if w.DelaySeconds != 0 {
|
||||||
|
envOverrides.awsPollDelaySeconds.Val = w.DelaySeconds
|
||||||
|
envOverrides.awsPollDelaySeconds.overridden = true
|
||||||
|
}
|
||||||
|
|
||||||
waitOpts := applyEnvOverrides(envOverrides)
|
waitOpts := applyEnvOverrides(envOverrides)
|
||||||
return waitOpts
|
return waitOpts
|
||||||
}
|
}
|
||||||
@@ -333,33 +377,38 @@ func getEnvOverrides() overridableWaitVars {
|
|||||||
return envValues
|
return envValues
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogEnvOverrideWarnings() {
|
func (w *AWSPollingConfig) LogEnvOverrideWarnings() {
|
||||||
pollDelay := os.Getenv("AWS_POLL_DELAY_SECONDS")
|
pollDelayEnv := os.Getenv("AWS_POLL_DELAY_SECONDS")
|
||||||
timeoutSeconds := os.Getenv("AWS_TIMEOUT_SECONDS")
|
timeoutSecondsEnv := os.Getenv("AWS_TIMEOUT_SECONDS")
|
||||||
maxAttempts := os.Getenv("AWS_MAX_ATTEMPTS")
|
maxAttemptsEnv := os.Getenv("AWS_MAX_ATTEMPTS")
|
||||||
|
|
||||||
if maxAttempts != "" && timeoutSeconds != "" {
|
maxAttemptsIsSet := maxAttemptsEnv != "" || w.MaxAttempts != 0
|
||||||
|
timeoutSecondsIsSet := timeoutSecondsEnv != ""
|
||||||
|
pollDelayIsSet := pollDelayEnv != "" || w.DelaySeconds != 0
|
||||||
|
|
||||||
|
if maxAttemptsIsSet && timeoutSecondsIsSet {
|
||||||
warning := fmt.Sprintf("[WARNING] (aws): AWS_MAX_ATTEMPTS and " +
|
warning := fmt.Sprintf("[WARNING] (aws): AWS_MAX_ATTEMPTS and " +
|
||||||
"AWS_TIMEOUT_SECONDS are both set. Packer will use " +
|
"AWS_TIMEOUT_SECONDS are both set. Packer will use " +
|
||||||
"AWS_MAX_ATTEMPTS and discard AWS_TIMEOUT_SECONDS.")
|
"AWS_MAX_ATTEMPTS and discard AWS_TIMEOUT_SECONDS.")
|
||||||
if pollDelay == "" {
|
if !pollDelayIsSet {
|
||||||
warning = fmt.Sprintf("%s Since you have not set the poll delay, "+
|
warning = fmt.Sprintf("%s Since you have not set the poll delay, "+
|
||||||
"Packer will default to a 2-second delay.", warning)
|
"Packer will default to a 2-second delay.", warning)
|
||||||
}
|
}
|
||||||
log.Printf(warning)
|
log.Printf(warning)
|
||||||
} else if timeoutSeconds != "" {
|
} else if timeoutSecondsIsSet {
|
||||||
log.Printf("[WARNING] (aws): env var AWS_TIMEOUT_SECONDS is " +
|
log.Printf("[WARNING] (aws): env var AWS_TIMEOUT_SECONDS is " +
|
||||||
"deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " +
|
"deprecated in favor of AWS_MAX_ATTEMPTS env or aws_polling_max_attempts config option. " +
|
||||||
"explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " +
|
"If you have not explicitly set AWS_POLL_DELAY_SECONDS env or aws_polling_delay_seconds config option, " +
|
||||||
"poll delay of 2 seconds, regardless of the AWS waiter's default.")
|
"we are defaulting to a poll delay of 2 seconds, regardless of the AWS waiter's default.")
|
||||||
}
|
}
|
||||||
if maxAttempts == "" && timeoutSeconds == "" && pollDelay == "" {
|
if !maxAttemptsIsSet && !timeoutSecondsIsSet && !pollDelayIsSet {
|
||||||
log.Printf("[INFO] (aws): No AWS timeout and polling overrides have been set. " +
|
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 " +
|
"Packer will default to waiter-specific delays and timeouts. If you would " +
|
||||||
"like to customize the length of time between retries and max " +
|
"like to customize the length of time between retries and max " +
|
||||||
"number of retries you may do so by setting the environment " +
|
"number of retries you may do so by setting the environment " +
|
||||||
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " +
|
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS or the " +
|
||||||
"desired values.")
|
"configuration options aws_polling_delay_seconds and aws_polling_max_attempts " +
|
||||||
|
"to your desired values.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Code generated by "mapstructure-to-hcl2 -type AWSPollingConfig"; DO NOT EDIT.
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FlatAWSPollingConfig is an auto-generated flat version of AWSPollingConfig.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatAWSPollingConfig struct {
|
||||||
|
MaxAttempts *int `mapstructure:"max_attempts" required:"false" cty:"max_attempts" hcl:"max_attempts"`
|
||||||
|
DelaySeconds *int `mapstructure:"delay_seconds" required:"false" cty:"delay_seconds" hcl:"delay_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatAWSPollingConfig.
|
||||||
|
// FlatAWSPollingConfig is an auto-generated flat version of AWSPollingConfig.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*AWSPollingConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatAWSPollingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a AWSPollingConfig.
|
||||||
|
// This spec is used by HCL to read the fields of AWSPollingConfig.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatAWSPollingConfig.
|
||||||
|
func (*FlatAWSPollingConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"max_attempts": &hcldec.AttrSpec{Name: "max_attempts", Type: cty.Number, Required: false},
|
||||||
|
"delay_seconds": &hcldec.AttrSpec{Name: "delay_seconds", Type: cty.Number, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -209,7 +209,7 @@ func (s *StepAMIRegionCopy) amiRegionCopy(ctx context.Context, state multistep.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the image to become ready
|
// Wait for the image to become ready
|
||||||
if err := WaitUntilAMIAvailable(ctx, regionconn, *resp.ImageId); err != nil {
|
if err := s.AccessConfig.PollingConfig.WaitUntilAMIAvailable(ctx, regionconn, *resp.ImageId); err != nil {
|
||||||
return "", snapshotIds, fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s",
|
return "", snapshotIds, fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s",
|
||||||
*resp.ImageId, target, err)
|
*resp.ImageId, target, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StepRunSourceInstance struct {
|
type StepRunSourceInstance struct {
|
||||||
|
PollingConfig *AWSPollingConfig
|
||||||
AssociatePublicIpAddress bool
|
AssociatePublicIpAddress bool
|
||||||
LaunchMappings EC2BlockDeviceMappingsBuilder
|
LaunchMappings EC2BlockDeviceMappingsBuilder
|
||||||
Comm *communicator.Config
|
Comm *communicator.Config
|
||||||
@@ -234,7 +235,7 @@ func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBa
|
|||||||
InstanceIds: []*string{aws.String(instanceId)},
|
InstanceIds: []*string{aws.String(instanceId)},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WaitUntilInstanceRunning(ctx, ec2conn, instanceId); err != nil {
|
if err := s.PollingConfig.WaitUntilInstanceRunning(ctx, ec2conn, instanceId); err != nil {
|
||||||
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
|
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
@@ -364,7 +365,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
|
if err := s.PollingConfig.WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type EC2BlockDeviceMappingsBuilder interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StepRunSpotInstance struct {
|
type StepRunSpotInstance struct {
|
||||||
|
PollingConfig *AWSPollingConfig
|
||||||
AssociatePublicIpAddress bool
|
AssociatePublicIpAddress bool
|
||||||
LaunchMappings EC2BlockDeviceMappingsBuilder
|
LaunchMappings EC2BlockDeviceMappingsBuilder
|
||||||
BlockDurationMinutes int64
|
BlockDurationMinutes int64
|
||||||
@@ -466,7 +467,7 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
|
if err := s.PollingConfig.WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func tStateSpot() multistep.StateBag {
|
|||||||
|
|
||||||
func getBasicStep() *StepRunSpotInstance {
|
func getBasicStep() *StepRunSpotInstance {
|
||||||
stepRunSpotInstance := StepRunSpotInstance{
|
stepRunSpotInstance := StepRunSpotInstance{
|
||||||
|
PollingConfig: new(AWSPollingConfig),
|
||||||
AssociatePublicIpAddress: false,
|
AssociatePublicIpAddress: false,
|
||||||
LaunchMappings: BlockDevices{},
|
LaunchMappings: BlockDevices{},
|
||||||
BlockDurationMinutes: 0,
|
BlockDurationMinutes: 0,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/hashicorp/packer/builder"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
// &awscommon.StepSetGeneratedData{
|
||||||
|
// GeneratedData: generatedData,
|
||||||
|
// },
|
||||||
|
|
||||||
|
type StepSetGeneratedData struct {
|
||||||
|
GeneratedData *builder.GeneratedData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepSetGeneratedData) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
|
|
||||||
|
extractBuildInfo(*ec2conn.Config.Region, state, s.GeneratedData)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepSetGeneratedData) Cleanup(state multistep.StateBag) {
|
||||||
|
// No cleanup...
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StepStopEBSBackedInstance struct {
|
type StepStopEBSBackedInstance struct {
|
||||||
|
PollingConfig *AWSPollingConfig
|
||||||
Skip bool
|
Skip bool
|
||||||
DisableStopInstance bool
|
DisableStopInstance bool
|
||||||
}
|
}
|
||||||
@@ -74,7 +75,7 @@ func (s *StepStopEBSBackedInstance) Run(ctx context.Context, state multistep.Sta
|
|||||||
&ec2.DescribeInstancesInput{
|
&ec2.DescribeInstancesInput{
|
||||||
InstanceIds: []*string{instance.InstanceId},
|
InstanceIds: []*string{instance.InstanceId},
|
||||||
},
|
},
|
||||||
getWaiterOptions()...)
|
s.PollingConfig.getWaiterOptions()...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
|
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||||||
|
|
||||||
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
||||||
|
|
||||||
generatedData := []string{"SourceAMIName"}
|
generatedData := awscommon.GetGeneratedDataList()
|
||||||
return generatedData, warns, nil
|
return generatedData, warns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
|
|
||||||
if b.config.IsSpotInstance() {
|
if b.config.IsSpotInstance() {
|
||||||
instanceStep = &awscommon.StepRunSpotInstance{
|
instanceStep = &awscommon.StepRunSpotInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
||||||
@@ -191,6 +192,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
instanceStep = &awscommon.StepRunSourceInstance{
|
instanceStep = &awscommon.StepRunSourceInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
@@ -284,11 +286,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
),
|
),
|
||||||
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
||||||
},
|
},
|
||||||
|
&awscommon.StepSetGeneratedData{
|
||||||
|
GeneratedData: generatedData,
|
||||||
|
},
|
||||||
&common.StepProvision{},
|
&common.StepProvision{},
|
||||||
&common.StepCleanupTempKeys{
|
&common.StepCleanupTempKeys{
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
},
|
},
|
||||||
&awscommon.StepStopEBSBackedInstance{
|
&awscommon.StepStopEBSBackedInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
Skip: b.config.IsSpotInstance(),
|
Skip: b.config.IsSpotInstance(),
|
||||||
DisableStopInstance: b.config.DisableStopInstance,
|
DisableStopInstance: b.config.DisableStopInstance,
|
||||||
},
|
},
|
||||||
@@ -305,6 +311,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
},
|
},
|
||||||
&stepCreateAMI{
|
&stepCreateAMI{
|
||||||
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
},
|
},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
AccessConfig: &b.config.AccessConfig,
|
AccessConfig: &b.config.AccessConfig,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type FlatConfig struct {
|
|||||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||||
|
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||||
AMIName *string `mapstructure:"ami_name" required:"true" cty:"ami_name" hcl:"ami_name"`
|
AMIName *string `mapstructure:"ami_name" required:"true" cty:"ami_name" hcl:"ami_name"`
|
||||||
AMIDescription *string `mapstructure:"ami_description" required:"false" cty:"ami_description" hcl:"ami_description"`
|
AMIDescription *string `mapstructure:"ami_description" required:"false" cty:"ami_description" hcl:"ami_description"`
|
||||||
AMIVirtType *string `mapstructure:"ami_virtualization_type" required:"false" cty:"ami_virtualization_type" hcl:"ami_virtualization_type"`
|
AMIVirtType *string `mapstructure:"ami_virtualization_type" required:"false" cty:"ami_virtualization_type" hcl:"ami_virtualization_type"`
|
||||||
@@ -171,6 +172,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||||
|
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||||
"ami_name": &hcldec.AttrSpec{Name: "ami_name", Type: cty.String, Required: false},
|
"ami_name": &hcldec.AttrSpec{Name: "ami_name", Type: cty.String, Required: false},
|
||||||
"ami_description": &hcldec.AttrSpec{Name: "ami_description", Type: cty.String, Required: false},
|
"ami_description": &hcldec.AttrSpec{Name: "ami_description", Type: cty.String, Required: false},
|
||||||
"ami_virtualization_type": &hcldec.AttrSpec{Name: "ami_virtualization_type", Type: cty.String, Required: false},
|
"ami_virtualization_type": &hcldec.AttrSpec{Name: "ami_virtualization_type", Type: cty.String, Required: false},
|
||||||
|
|||||||
@@ -144,7 +144,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
|
|||||||
if len(generatedData) == 0 {
|
if len(generatedData) == 0 {
|
||||||
t.Fatalf("Generated data should not be empty")
|
t.Fatalf("Generated data should not be empty")
|
||||||
}
|
}
|
||||||
|
if len(generatedData) == 0 {
|
||||||
|
t.Fatalf("Generated data should not be empty")
|
||||||
|
}
|
||||||
if generatedData[0] != "SourceAMIName" {
|
if generatedData[0] != "SourceAMIName" {
|
||||||
t.Fatalf("Generated data should contain SourceAMIName")
|
t.Fatalf("Generated data should contain SourceAMIName")
|
||||||
}
|
}
|
||||||
|
if generatedData[1] != "BuildRegion" {
|
||||||
|
t.Fatalf("Generated data should contain BuildRegion")
|
||||||
|
}
|
||||||
|
if generatedData[2] != "SourceAMI" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMI")
|
||||||
|
}
|
||||||
|
if generatedData[3] != "SourceAMICreationDate" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMICreationDate")
|
||||||
|
}
|
||||||
|
if generatedData[4] != "SourceAMIOwner" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwner")
|
||||||
|
}
|
||||||
|
if generatedData[5] != "SourceAMIOwnerName" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwnerName")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type stepCreateAMI struct {
|
type stepCreateAMI struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
image *ec2.Image
|
image *ec2.Image
|
||||||
AMISkipBuildRegion bool
|
AMISkipBuildRegion bool
|
||||||
}
|
}
|
||||||
@@ -85,18 +86,23 @@ func (s *stepCreateAMI) Run(ctx context.Context, state multistep.StateBag) multi
|
|||||||
|
|
||||||
// Wait for the image to become ready
|
// Wait for the image to become ready
|
||||||
ui.Say("Waiting for AMI to become ready...")
|
ui.Say("Waiting for AMI to become ready...")
|
||||||
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); err != nil {
|
if waitErr := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); waitErr != nil {
|
||||||
log.Printf("Error waiting for AMI: %s", err)
|
// waitErr should get bubbled up if the issue is a wait timeout
|
||||||
|
err := fmt.Errorf("Error waiting for AMI: %s", waitErr)
|
||||||
imResp, imerr := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{createResp.ImageId}})
|
imResp, imerr := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{createResp.ImageId}})
|
||||||
if imerr != nil {
|
if imerr != nil {
|
||||||
log.Printf("Unable to determine reason waiting for AMI failed: %s", err)
|
// If there's a failure describing images, bubble that error up too, but don't erase the waitErr.
|
||||||
err = fmt.Errorf("Unknown error waiting for AMI; %s", err)
|
log.Printf("DescribeImages call was unable to determine reason waiting for AMI failed: %s", imerr)
|
||||||
|
err = fmt.Errorf("Unknown error waiting for AMI; %s. DescribeImages returned an error: %s", waitErr, imerr)
|
||||||
}
|
}
|
||||||
if imResp != nil && len(imResp.Images) > 0 {
|
if imResp != nil && len(imResp.Images) > 0 {
|
||||||
|
// Finally, if there's a stateReason, store that with the wait err
|
||||||
image := imResp.Images[0]
|
image := imResp.Images[0]
|
||||||
if image != nil {
|
if image != nil {
|
||||||
stateReason := image.StateReason
|
stateReason := image.StateReason
|
||||||
err = fmt.Errorf("Error waiting for AMI. Reason: %s", stateReason)
|
if stateReason != nil {
|
||||||
|
err = fmt.Errorf("Error waiting for AMI: %s. DescribeImages returned the state reason: %s", waitErr, stateReason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||||||
|
|
||||||
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
||||||
|
|
||||||
generatedData := []string{"SourceAMIName"}
|
generatedData := awscommon.GetGeneratedDataList()
|
||||||
return generatedData, warns, nil
|
return generatedData, warns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +192,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
|
|
||||||
if b.config.IsSpotInstance() {
|
if b.config.IsSpotInstance() {
|
||||||
instanceStep = &awscommon.StepRunSpotInstance{
|
instanceStep = &awscommon.StepRunSpotInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
||||||
@@ -213,6 +214,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
instanceStep = &awscommon.StepRunSourceInstance{
|
instanceStep = &awscommon.StepRunSourceInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
@@ -238,11 +240,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
// Build the steps
|
// Build the steps
|
||||||
steps := []multistep.Step{
|
steps := []multistep.Step{
|
||||||
&awscommon.StepPreValidate{
|
&awscommon.StepPreValidate{
|
||||||
DestAmiName: b.config.AMIName,
|
DestAmiName: b.config.AMIName,
|
||||||
ForceDeregister: b.config.AMIForceDeregister,
|
ForceDeregister: b.config.AMIForceDeregister,
|
||||||
VpcId: b.config.VpcId,
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
SubnetId: b.config.SubnetId,
|
VpcId: b.config.VpcId,
|
||||||
HasSubnetFilter: !b.config.SubnetFilter.Empty(),
|
SubnetId: b.config.SubnetId,
|
||||||
|
HasSubnetFilter: !b.config.SubnetFilter.Empty(),
|
||||||
},
|
},
|
||||||
&awscommon.StepSourceAMIInfo{
|
&awscommon.StepSourceAMIInfo{
|
||||||
SourceAmi: b.config.SourceAmi,
|
SourceAmi: b.config.SourceAmi,
|
||||||
@@ -307,11 +310,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
),
|
),
|
||||||
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
||||||
},
|
},
|
||||||
|
&awscommon.StepSetGeneratedData{
|
||||||
|
GeneratedData: generatedData,
|
||||||
|
},
|
||||||
&common.StepProvision{},
|
&common.StepProvision{},
|
||||||
&common.StepCleanupTempKeys{
|
&common.StepCleanupTempKeys{
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
},
|
},
|
||||||
&awscommon.StepStopEBSBackedInstance{
|
&awscommon.StepStopEBSBackedInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
Skip: b.config.IsSpotInstance(),
|
Skip: b.config.IsSpotInstance(),
|
||||||
DisableStopInstance: b.config.DisableStopInstance,
|
DisableStopInstance: b.config.DisableStopInstance,
|
||||||
},
|
},
|
||||||
@@ -321,6 +328,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
EnableAMIENASupport: b.config.AMIENASupport,
|
EnableAMIENASupport: b.config.AMIENASupport,
|
||||||
},
|
},
|
||||||
&StepSnapshotVolumes{
|
&StepSnapshotVolumes{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
LaunchDevices: launchDevices,
|
LaunchDevices: launchDevices,
|
||||||
SnapshotOmitMap: b.config.LaunchMappings.GetOmissions(),
|
SnapshotOmitMap: b.config.LaunchMappings.GetOmissions(),
|
||||||
},
|
},
|
||||||
@@ -340,15 +348,17 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
Architecture: b.config.Architecture,
|
Architecture: b.config.Architecture,
|
||||||
LaunchOmitMap: b.config.LaunchMappings.GetOmissions(),
|
LaunchOmitMap: b.config.LaunchMappings.GetOmissions(),
|
||||||
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
},
|
},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
AccessConfig: &b.config.AccessConfig,
|
AccessConfig: &b.config.AccessConfig,
|
||||||
Regions: b.config.AMIRegions,
|
Regions: b.config.AMIRegions,
|
||||||
AMIKmsKeyId: b.config.AMIKmsKeyId,
|
AMIKmsKeyId: b.config.AMIKmsKeyId,
|
||||||
RegionKeyIds: b.config.AMIRegionKMSKeyIDs,
|
RegionKeyIds: b.config.AMIRegionKMSKeyIDs,
|
||||||
EncryptBootVolume: b.config.AMIEncryptBootVolume,
|
EncryptBootVolume: b.config.AMIEncryptBootVolume,
|
||||||
Name: b.config.AMIName,
|
Name: b.config.AMIName,
|
||||||
OriginalRegion: *ec2conn.Config.Region,
|
OriginalRegion: *ec2conn.Config.Region,
|
||||||
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
},
|
},
|
||||||
&awscommon.StepModifyAMIAttributes{
|
&awscommon.StepModifyAMIAttributes{
|
||||||
Description: b.config.AMIDescription,
|
Description: b.config.AMIDescription,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type FlatConfig struct {
|
|||||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||||
|
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||||
AssociatePublicIpAddress *bool `mapstructure:"associate_public_ip_address" required:"false" cty:"associate_public_ip_address" hcl:"associate_public_ip_address"`
|
AssociatePublicIpAddress *bool `mapstructure:"associate_public_ip_address" required:"false" cty:"associate_public_ip_address" hcl:"associate_public_ip_address"`
|
||||||
AvailabilityZone *string `mapstructure:"availability_zone" required:"false" cty:"availability_zone" hcl:"availability_zone"`
|
AvailabilityZone *string `mapstructure:"availability_zone" required:"false" cty:"availability_zone" hcl:"availability_zone"`
|
||||||
BlockDurationMinutes *int64 `mapstructure:"block_duration_minutes" required:"false" cty:"block_duration_minutes" hcl:"block_duration_minutes"`
|
BlockDurationMinutes *int64 `mapstructure:"block_duration_minutes" required:"false" cty:"block_duration_minutes" hcl:"block_duration_minutes"`
|
||||||
@@ -215,6 +216,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||||
|
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||||
"associate_public_ip_address": &hcldec.AttrSpec{Name: "associate_public_ip_address", Type: cty.Bool, Required: false},
|
"associate_public_ip_address": &hcldec.AttrSpec{Name: "associate_public_ip_address", Type: cty.Bool, Required: false},
|
||||||
"availability_zone": &hcldec.AttrSpec{Name: "availability_zone", Type: cty.String, Required: false},
|
"availability_zone": &hcldec.AttrSpec{Name: "availability_zone", Type: cty.String, Required: false},
|
||||||
"block_duration_minutes": &hcldec.AttrSpec{Name: "block_duration_minutes", Type: cty.Number, Required: false},
|
"block_duration_minutes": &hcldec.AttrSpec{Name: "block_duration_minutes", Type: cty.Number, Required: false},
|
||||||
|
|||||||
@@ -86,7 +86,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
|
|||||||
if len(generatedData) == 0 {
|
if len(generatedData) == 0 {
|
||||||
t.Fatalf("Generated data should not be empty")
|
t.Fatalf("Generated data should not be empty")
|
||||||
}
|
}
|
||||||
|
if len(generatedData) == 0 {
|
||||||
|
t.Fatalf("Generated data should not be empty")
|
||||||
|
}
|
||||||
if generatedData[0] != "SourceAMIName" {
|
if generatedData[0] != "SourceAMIName" {
|
||||||
t.Fatalf("Generated data should contain SourceAMIName")
|
t.Fatalf("Generated data should contain SourceAMIName")
|
||||||
}
|
}
|
||||||
|
if generatedData[1] != "BuildRegion" {
|
||||||
|
t.Fatalf("Generated data should contain BuildRegion")
|
||||||
|
}
|
||||||
|
if generatedData[2] != "SourceAMI" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMI")
|
||||||
|
}
|
||||||
|
if generatedData[3] != "SourceAMICreationDate" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMICreationDate")
|
||||||
|
}
|
||||||
|
if generatedData[4] != "SourceAMIOwner" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwner")
|
||||||
|
}
|
||||||
|
if generatedData[5] != "SourceAMIOwnerName" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwnerName")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
// StepRegisterAMI creates the AMI.
|
// StepRegisterAMI creates the AMI.
|
||||||
type StepRegisterAMI struct {
|
type StepRegisterAMI struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
RootDevice RootBlockDevice
|
RootDevice RootBlockDevice
|
||||||
AMIDevices []*ec2.BlockDeviceMapping
|
AMIDevices []*ec2.BlockDeviceMapping
|
||||||
LaunchDevices []*ec2.BlockDeviceMapping
|
LaunchDevices []*ec2.BlockDeviceMapping
|
||||||
@@ -86,7 +87,7 @@ func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) mul
|
|||||||
|
|
||||||
// Wait for the image to become ready
|
// Wait for the image to become ready
|
||||||
ui.Say("Waiting for AMI to become ready...")
|
ui.Say("Waiting for AMI to become ready...")
|
||||||
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
if err := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
||||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/hashicorp/packer/builder/amazon/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sourceDeviceName = "/dev/xvdf"
|
const sourceDeviceName = "/dev/xvdf"
|
||||||
@@ -23,6 +24,7 @@ func newStepRegisterAMI(amiDevices, launchDevices []*ec2.BlockDeviceMapping) *St
|
|||||||
},
|
},
|
||||||
AMIDevices: amiDevices,
|
AMIDevices: amiDevices,
|
||||||
LaunchDevices: launchDevices,
|
LaunchDevices: launchDevices,
|
||||||
|
PollingConfig: new(common.AWSPollingConfig),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
// Produces:
|
// Produces:
|
||||||
// snapshot_ids map[string]string - IDs of the created snapshots
|
// snapshot_ids map[string]string - IDs of the created snapshots
|
||||||
type StepSnapshotVolumes struct {
|
type StepSnapshotVolumes struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
LaunchDevices []*ec2.BlockDeviceMapping
|
LaunchDevices []*ec2.BlockDeviceMapping
|
||||||
snapshotIds map[string]string
|
snapshotIds map[string]string
|
||||||
snapshotMutex sync.Mutex
|
snapshotMutex sync.Mutex
|
||||||
@@ -56,7 +57,7 @@ func (s *StepSnapshotVolumes) snapshotVolume(ctx context.Context, deviceName str
|
|||||||
s.snapshotMutex.Unlock()
|
s.snapshotMutex.Unlock()
|
||||||
|
|
||||||
// Wait for snapshot to be created
|
// Wait for snapshot to be created
|
||||||
err = awscommon.WaitUntilSnapshotDone(ctx, ec2conn, *createSnapResp.SnapshotId)
|
err = s.PollingConfig.WaitUntilSnapshotDone(ctx, ec2conn, *createSnapResp.SnapshotId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go/service/iam"
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/hashicorp/packer/builder"
|
||||||
awscommon "github.com/hashicorp/packer/builder/amazon/common"
|
awscommon "github.com/hashicorp/packer/builder/amazon/common"
|
||||||
"github.com/hashicorp/packer/common"
|
"github.com/hashicorp/packer/common"
|
||||||
"github.com/hashicorp/packer/hcl2template"
|
"github.com/hashicorp/packer/hcl2template"
|
||||||
@@ -146,7 +147,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||||||
|
|
||||||
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
||||||
|
|
||||||
generatedData := []string{"SourceAMIName"}
|
generatedData := awscommon.GetGeneratedDataList()
|
||||||
return generatedData, warns, nil
|
return generatedData, warns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +167,13 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
state.Put("iam", iam)
|
state.Put("iam", iam)
|
||||||
state.Put("hook", hook)
|
state.Put("hook", hook)
|
||||||
state.Put("ui", ui)
|
state.Put("ui", ui)
|
||||||
|
generatedData := &builder.GeneratedData{State: state}
|
||||||
|
|
||||||
var instanceStep multistep.Step
|
var instanceStep multistep.Step
|
||||||
|
|
||||||
if b.config.IsSpotInstance() {
|
if b.config.IsSpotInstance() {
|
||||||
instanceStep = &awscommon.StepRunSpotInstance{
|
instanceStep = &awscommon.StepRunSpotInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.launchBlockDevices,
|
LaunchMappings: b.config.launchBlockDevices,
|
||||||
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
||||||
@@ -192,6 +195,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
instanceStep = &awscommon.StepRunSourceInstance{
|
instanceStep = &awscommon.StepRunSourceInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.launchBlockDevices,
|
LaunchMappings: b.config.launchBlockDevices,
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
@@ -276,11 +280,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
),
|
),
|
||||||
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
||||||
},
|
},
|
||||||
|
&awscommon.StepSetGeneratedData{
|
||||||
|
GeneratedData: generatedData,
|
||||||
|
},
|
||||||
&common.StepProvision{},
|
&common.StepProvision{},
|
||||||
&common.StepCleanupTempKeys{
|
&common.StepCleanupTempKeys{
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
},
|
},
|
||||||
&awscommon.StepStopEBSBackedInstance{
|
&awscommon.StepStopEBSBackedInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
Skip: b.config.IsSpotInstance(),
|
Skip: b.config.IsSpotInstance(),
|
||||||
DisableStopInstance: b.config.DisableStopInstance,
|
DisableStopInstance: b.config.DisableStopInstance,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ type FlatConfig struct {
|
|||||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||||
|
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||||
AssociatePublicIpAddress *bool `mapstructure:"associate_public_ip_address" required:"false" cty:"associate_public_ip_address" hcl:"associate_public_ip_address"`
|
AssociatePublicIpAddress *bool `mapstructure:"associate_public_ip_address" required:"false" cty:"associate_public_ip_address" hcl:"associate_public_ip_address"`
|
||||||
AvailabilityZone *string `mapstructure:"availability_zone" required:"false" cty:"availability_zone" hcl:"availability_zone"`
|
AvailabilityZone *string `mapstructure:"availability_zone" required:"false" cty:"availability_zone" hcl:"availability_zone"`
|
||||||
BlockDurationMinutes *int64 `mapstructure:"block_duration_minutes" required:"false" cty:"block_duration_minutes" hcl:"block_duration_minutes"`
|
BlockDurationMinutes *int64 `mapstructure:"block_duration_minutes" required:"false" cty:"block_duration_minutes" hcl:"block_duration_minutes"`
|
||||||
@@ -195,6 +196,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||||
|
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||||
"associate_public_ip_address": &hcldec.AttrSpec{Name: "associate_public_ip_address", Type: cty.Bool, Required: false},
|
"associate_public_ip_address": &hcldec.AttrSpec{Name: "associate_public_ip_address", Type: cty.Bool, Required: false},
|
||||||
"availability_zone": &hcldec.AttrSpec{Name: "availability_zone", Type: cty.String, Required: false},
|
"availability_zone": &hcldec.AttrSpec{Name: "availability_zone", Type: cty.String, Required: false},
|
||||||
"block_duration_minutes": &hcldec.AttrSpec{Name: "block_duration_minutes", Type: cty.Number, Required: false},
|
"block_duration_minutes": &hcldec.AttrSpec{Name: "block_duration_minutes", Type: cty.Number, Required: false},
|
||||||
|
|||||||
@@ -105,7 +105,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
|
|||||||
if len(generatedData) == 0 {
|
if len(generatedData) == 0 {
|
||||||
t.Fatalf("Generated data should not be empty")
|
t.Fatalf("Generated data should not be empty")
|
||||||
}
|
}
|
||||||
|
if len(generatedData) == 0 {
|
||||||
|
t.Fatalf("Generated data should not be empty")
|
||||||
|
}
|
||||||
if generatedData[0] != "SourceAMIName" {
|
if generatedData[0] != "SourceAMIName" {
|
||||||
t.Fatalf("Generated data should contain SourceAMIName")
|
t.Fatalf("Generated data should contain SourceAMIName")
|
||||||
}
|
}
|
||||||
|
if generatedData[1] != "BuildRegion" {
|
||||||
|
t.Fatalf("Generated data should contain BuildRegion")
|
||||||
|
}
|
||||||
|
if generatedData[2] != "SourceAMI" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMI")
|
||||||
|
}
|
||||||
|
if generatedData[3] != "SourceAMICreationDate" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMICreationDate")
|
||||||
|
}
|
||||||
|
if generatedData[4] != "SourceAMIOwner" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwner")
|
||||||
|
}
|
||||||
|
if generatedData[5] != "SourceAMIOwnerName" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwnerName")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||||||
}
|
}
|
||||||
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
packer.LogSecretFilter.Set(b.config.AccessKey, b.config.SecretKey, b.config.Token)
|
||||||
|
|
||||||
generatedData := []string{"SourceAMIName"}
|
generatedData := awscommon.GetGeneratedDataList()
|
||||||
return generatedData, warns, nil
|
return generatedData, warns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +255,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
|
|
||||||
if b.config.IsSpotInstance() {
|
if b.config.IsSpotInstance() {
|
||||||
instanceStep = &awscommon.StepRunSpotInstance{
|
instanceStep = &awscommon.StepRunSpotInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
BlockDurationMinutes: b.config.BlockDurationMinutes,
|
||||||
@@ -273,6 +274,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
instanceStep = &awscommon.StepRunSourceInstance{
|
instanceStep = &awscommon.StepRunSourceInstance{
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||||
LaunchMappings: b.config.LaunchMappings,
|
LaunchMappings: b.config.LaunchMappings,
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
@@ -360,6 +362,9 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
),
|
),
|
||||||
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
SSHConfig: b.config.RunConfig.Comm.SSHConfigFunc(),
|
||||||
},
|
},
|
||||||
|
&awscommon.StepSetGeneratedData{
|
||||||
|
GeneratedData: generatedData,
|
||||||
|
},
|
||||||
&common.StepProvision{},
|
&common.StepProvision{},
|
||||||
&common.StepCleanupTempKeys{
|
&common.StepCleanupTempKeys{
|
||||||
Comm: &b.config.RunConfig.Comm,
|
Comm: &b.config.RunConfig.Comm,
|
||||||
@@ -382,6 +387,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
|
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
|
||||||
EnableAMIENASupport: b.config.AMIENASupport,
|
EnableAMIENASupport: b.config.AMIENASupport,
|
||||||
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
|
||||||
|
PollingConfig: b.config.PollingConfig,
|
||||||
},
|
},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
AccessConfig: &b.config.AccessConfig,
|
AccessConfig: &b.config.AccessConfig,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type FlatConfig struct {
|
|||||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||||
|
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||||
AMIName *string `mapstructure:"ami_name" required:"true" cty:"ami_name" hcl:"ami_name"`
|
AMIName *string `mapstructure:"ami_name" required:"true" cty:"ami_name" hcl:"ami_name"`
|
||||||
AMIDescription *string `mapstructure:"ami_description" required:"false" cty:"ami_description" hcl:"ami_description"`
|
AMIDescription *string `mapstructure:"ami_description" required:"false" cty:"ami_description" hcl:"ami_description"`
|
||||||
AMIVirtType *string `mapstructure:"ami_virtualization_type" required:"false" cty:"ami_virtualization_type" hcl:"ami_virtualization_type"`
|
AMIVirtType *string `mapstructure:"ami_virtualization_type" required:"false" cty:"ami_virtualization_type" hcl:"ami_virtualization_type"`
|
||||||
@@ -177,6 +178,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||||
|
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||||
"ami_name": &hcldec.AttrSpec{Name: "ami_name", Type: cty.String, Required: false},
|
"ami_name": &hcldec.AttrSpec{Name: "ami_name", Type: cty.String, Required: false},
|
||||||
"ami_description": &hcldec.AttrSpec{Name: "ami_description", Type: cty.String, Required: false},
|
"ami_description": &hcldec.AttrSpec{Name: "ami_description", Type: cty.String, Required: false},
|
||||||
"ami_virtualization_type": &hcldec.AttrSpec{Name: "ami_virtualization_type", Type: cty.String, Required: false},
|
"ami_virtualization_type": &hcldec.AttrSpec{Name: "ami_virtualization_type", Type: cty.String, Required: false},
|
||||||
|
|||||||
@@ -319,7 +319,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
|
|||||||
if len(generatedData) == 0 {
|
if len(generatedData) == 0 {
|
||||||
t.Fatalf("Generated data should not be empty")
|
t.Fatalf("Generated data should not be empty")
|
||||||
}
|
}
|
||||||
|
if len(generatedData) == 0 {
|
||||||
|
t.Fatalf("Generated data should not be empty")
|
||||||
|
}
|
||||||
if generatedData[0] != "SourceAMIName" {
|
if generatedData[0] != "SourceAMIName" {
|
||||||
t.Fatalf("Generated data should contain SourceAMIName")
|
t.Fatalf("Generated data should contain SourceAMIName")
|
||||||
}
|
}
|
||||||
|
if generatedData[1] != "BuildRegion" {
|
||||||
|
t.Fatalf("Generated data should contain BuildRegion")
|
||||||
|
}
|
||||||
|
if generatedData[2] != "SourceAMI" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMI")
|
||||||
|
}
|
||||||
|
if generatedData[3] != "SourceAMICreationDate" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMICreationDate")
|
||||||
|
}
|
||||||
|
if generatedData[4] != "SourceAMIOwner" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwner")
|
||||||
|
}
|
||||||
|
if generatedData[5] != "SourceAMIOwnerName" {
|
||||||
|
t.Fatalf("Generated data should contain SourceAMIOwnerName")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StepRegisterAMI struct {
|
type StepRegisterAMI struct {
|
||||||
|
PollingConfig *awscommon.AWSPollingConfig
|
||||||
EnableAMIENASupport confighelper.Trilean
|
EnableAMIENASupport confighelper.Trilean
|
||||||
EnableAMISriovNetSupport bool
|
EnableAMISriovNetSupport bool
|
||||||
AMISkipBuildRegion bool
|
AMISkipBuildRegion bool
|
||||||
@@ -80,7 +81,7 @@ func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) mul
|
|||||||
|
|
||||||
// Wait for the image to become ready
|
// Wait for the image to become ready
|
||||||
ui.Say("Waiting for AMI to become ready...")
|
ui.Say("Waiting for AMI to become ready...")
|
||||||
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
if err := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
|
||||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
|
|||||||
@@ -221,8 +221,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
|
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
|
||||||
NewStepCaptureImage(azureClient, ui),
|
NewStepCaptureImage(azureClient, ui),
|
||||||
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
|
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
|
||||||
NewStepDeleteResourceGroup(azureClient, ui),
|
|
||||||
NewStepDeleteOSDisk(azureClient, ui),
|
|
||||||
NewStepDeleteAdditionalDisks(azureClient, ui),
|
NewStepDeleteAdditionalDisks(azureClient, ui),
|
||||||
}
|
}
|
||||||
} else if b.config.OSType == constants.Target_Windows {
|
} else if b.config.OSType == constants.Target_Windows {
|
||||||
@@ -264,8 +262,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
|
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
|
||||||
NewStepCaptureImage(azureClient, ui),
|
NewStepCaptureImage(azureClient, ui),
|
||||||
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
|
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
|
||||||
NewStepDeleteResourceGroup(azureClient, ui),
|
|
||||||
NewStepDeleteOSDisk(azureClient, ui),
|
|
||||||
NewStepDeleteAdditionalDisks(azureClient, ui),
|
NewStepDeleteAdditionalDisks(azureClient, ui),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ type Config struct {
|
|||||||
// Authentication via OAUTH
|
// Authentication via OAUTH
|
||||||
ClientConfig client.Config `mapstructure:",squash"`
|
ClientConfig client.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
// If set with one or more resource ids of user assigned managed identities, they will be configured on the VM.
|
// A list of one or more fully-qualified resource IDs of user assigned
|
||||||
|
// managed identities to be configured on the VM.
|
||||||
// See [documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
|
// See [documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
|
||||||
// for how to acquire tokens within the VM.
|
// for how to acquire tokens within the VM.
|
||||||
// To assign a user assigned managed identity to a VM, the provided account or service principal must have [Managed Identity Operator](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#managed-identity-operator)
|
// To assign a user assigned managed identity to a VM, the provided account or service principal must have [Managed Identity Operator](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#managed-identity-operator)
|
||||||
@@ -787,6 +788,10 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
|
|||||||
for _, rid := range c.UserAssignedManagedIdentities {
|
for _, rid := range c.UserAssignedManagedIdentities {
|
||||||
r, err := client.ParseResourceID(rid)
|
r, err := client.ParseResourceID(rid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error parsing resource ID from `user_assigned_managed_identities`; please make sure"+
|
||||||
|
" that this value follows the full resource id format: "+
|
||||||
|
"/subscriptions/<SUBSCRIPTON_ID>/resourcegroups/<RESOURCE_GROUP>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<USER_ASSIGNED_IDENTITY_NAME>.\n"+
|
||||||
|
" Original error: %s", err)
|
||||||
errs = packer.MultiErrorAppend(errs, err)
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
} else {
|
} else {
|
||||||
if !strings.EqualFold(r.Provider, "Microsoft.ManagedIdentity") {
|
if !strings.EqualFold(r.Provider, "Microsoft.ManagedIdentity") {
|
||||||
|
|||||||
@@ -111,27 +111,30 @@ func (s *StepCreateResourceGroup) Cleanup(state multistep.StateBag) {
|
|||||||
if state.Get(constants.ArmIsExistingResourceGroup).(bool) {
|
if state.Get(constants.ArmIsExistingResourceGroup).(bool) {
|
||||||
ui.Say("\nThe resource group was not created by Packer, not deleting ...")
|
ui.Say("\nThe resource group was not created by Packer, not deleting ...")
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
ui.Say("\nCleanup requested, deleting resource group ...")
|
|
||||||
|
|
||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
ctx := context.TODO()
|
||||||
ctx := context.TODO()
|
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
|
||||||
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
|
if exists, err := s.exists(ctx, resourceGroupName); !exists || err != nil {
|
||||||
if err == nil {
|
return
|
||||||
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
|
}
|
||||||
s.say(fmt.Sprintf("\n Not waiting for Resource Group delete as requested by user. Resource Group Name is %s", resourceGroupName))
|
|
||||||
} else {
|
ui.Say("\nCleanup requested, deleting resource group ...")
|
||||||
err = f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
|
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
|
||||||
}
|
if err == nil {
|
||||||
}
|
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
|
||||||
if err != nil {
|
s.say(fmt.Sprintf("\n Not waiting for Resource Group delete as requested by user. Resource Group Name is %s", resourceGroupName))
|
||||||
ui.Error(fmt.Sprintf("Error deleting resource group. Please delete it manually.\n\n"+
|
} else {
|
||||||
"Name: %s\n"+
|
err = f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
|
||||||
"Error: %s", resourceGroupName, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
|
|
||||||
ui.Say("Resource group has been deleted.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting resource group. Please delete it manually.\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", resourceGroupName, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
|
||||||
|
ui.Say("Resource group has been deleted.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,13 @@ func NewStepDeleteAdditionalDisks(client *AzureClient, ui packer.Ui) *StepDelete
|
|||||||
|
|
||||||
func (s *StepDeleteAdditionalDisk) deleteBlob(storageContainerName string, blobName string) error {
|
func (s *StepDeleteAdditionalDisk) deleteBlob(storageContainerName string, blobName string) error {
|
||||||
blob := s.client.BlobStorageClient.GetContainerReference(storageContainerName).GetBlobReference(blobName)
|
blob := s.client.BlobStorageClient.GetContainerReference(storageContainerName).GetBlobReference(blobName)
|
||||||
err := blob.Delete(nil)
|
_, err := blob.BreakLease(nil)
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "LeaseNotPresentWithLeaseOperation") {
|
||||||
|
s.say(s.client.LastError.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = blob.Delete(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.say(s.client.LastError.Error())
|
s.say(s.client.LastError.Error())
|
||||||
}
|
}
|
||||||
@@ -80,25 +85,26 @@ func (s *StepDeleteAdditionalDisk) Run(ctx context.Context, state multistep.Stat
|
|||||||
s.say("Failed to delete the managed Additional Disk!")
|
s.say("Failed to delete the managed Additional Disk!")
|
||||||
return processStepResult(err, s.error, state)
|
return processStepResult(err, s.error, state)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(additionaldisk)
|
||||||
|
if err != nil {
|
||||||
|
s.say("Failed to parse the Additional Disk's VHD URI!")
|
||||||
|
return processStepResult(err, s.error, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
xs := strings.Split(u.Path, "/")
|
||||||
|
if len(xs) < 3 {
|
||||||
|
err = errors.New("Failed to parse Additional Disk's VHD URI!")
|
||||||
} else {
|
} else {
|
||||||
u, err := url.Parse(additionaldisk)
|
var storageAccountName = xs[1]
|
||||||
if err != nil {
|
var blobName = strings.Join(xs[2:], "/")
|
||||||
s.say("Failed to parse the Additional Disk's VHD URI!")
|
|
||||||
return processStepResult(err, s.error, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
xs := strings.Split(u.Path, "/")
|
err = s.delete(storageAccountName, blobName)
|
||||||
if len(xs) < 3 {
|
}
|
||||||
err = errors.New("Failed to parse Additional Disk's VHD URI!")
|
if err != nil {
|
||||||
} else {
|
return processStepResult(err, s.error, state)
|
||||||
var storageAccountName = xs[1]
|
|
||||||
var blobName = strings.Join(xs[2:], "/")
|
|
||||||
|
|
||||||
err = s.delete(storageAccountName, blobName)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return processStepResult(err, s.error, state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package arm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/builder/azure/common/constants"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/helper/multistep"
|
|
||||||
"github.com/hashicorp/packer/packer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StepDeleteOSDisk struct {
|
|
||||||
client *AzureClient
|
|
||||||
delete func(string, string) error
|
|
||||||
deleteManaged func(context.Context, string, string) error
|
|
||||||
say func(message string)
|
|
||||||
error func(e error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStepDeleteOSDisk(client *AzureClient, ui packer.Ui) *StepDeleteOSDisk {
|
|
||||||
var step = &StepDeleteOSDisk{
|
|
||||||
client: client,
|
|
||||||
say: func(message string) { ui.Say(message) },
|
|
||||||
error: func(e error) { ui.Error(e.Error()) },
|
|
||||||
}
|
|
||||||
|
|
||||||
step.delete = step.deleteBlob
|
|
||||||
step.deleteManaged = step.deleteManagedDisk
|
|
||||||
return step
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteOSDisk) deleteBlob(storageContainerName string, blobName string) error {
|
|
||||||
blob := s.client.BlobStorageClient.GetContainerReference(storageContainerName).GetBlobReference(blobName)
|
|
||||||
err := blob.Delete(nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.say(s.client.LastError.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteOSDisk) deleteManagedDisk(ctx context.Context, resourceGroupName string, imageName string) error {
|
|
||||||
xs := strings.Split(imageName, "/")
|
|
||||||
diskName := xs[len(xs)-1]
|
|
||||||
f, err := s.client.DisksClient.Delete(ctx, resourceGroupName, diskName)
|
|
||||||
if err == nil {
|
|
||||||
err = f.WaitForCompletionRef(ctx, s.client.DisksClient.Client)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteOSDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
||||||
s.say("Deleting the temporary OS disk ...")
|
|
||||||
|
|
||||||
var osDisk = state.Get(constants.ArmOSDiskVhd).(string)
|
|
||||||
var isManagedDisk = state.Get(constants.ArmIsManagedImage).(bool)
|
|
||||||
var isExistingResourceGroup = state.Get(constants.ArmIsExistingResourceGroup).(bool)
|
|
||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
|
||||||
|
|
||||||
if isManagedDisk && !isExistingResourceGroup {
|
|
||||||
s.say(fmt.Sprintf(" -> OS Disk : skipping, managed disk was used..."))
|
|
||||||
return multistep.ActionContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.say(fmt.Sprintf(" -> OS Disk : '%s'", osDisk))
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if isManagedDisk {
|
|
||||||
err = s.deleteManaged(ctx, resourceGroupName, osDisk)
|
|
||||||
if err != nil {
|
|
||||||
s.say("Failed to delete the managed OS Disk!")
|
|
||||||
return processStepResult(err, s.error, state)
|
|
||||||
}
|
|
||||||
return multistep.ActionContinue
|
|
||||||
}
|
|
||||||
u, err := url.Parse(osDisk)
|
|
||||||
if err != nil {
|
|
||||||
s.say("Failed to parse the OS Disk's VHD URI!")
|
|
||||||
return processStepResult(err, s.error, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
xs := strings.Split(u.Path, "/")
|
|
||||||
if len(xs) < 3 {
|
|
||||||
err = errors.New("Failed to parse OS Disk's VHD URI!")
|
|
||||||
} else {
|
|
||||||
var storageAccountName = xs[1]
|
|
||||||
var blobName = strings.Join(xs[2:], "/")
|
|
||||||
|
|
||||||
err = s.delete(storageAccountName, blobName)
|
|
||||||
}
|
|
||||||
return processStepResult(err, s.error, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*StepDeleteOSDisk) Cleanup(multistep.StateBag) {
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
package arm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/builder/azure/common/constants"
|
|
||||||
"github.com/hashicorp/packer/helper/multistep"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldFailIfGetFails(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
|
|
||||||
deleteManaged: func(context.Context, string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionHalt {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
|
||||||
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldPassIfGetPasses(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionContinue {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == true {
|
|
||||||
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldTakeStepArgumentsFromStateBag(t *testing.T) {
|
|
||||||
var actualStorageContainerName string
|
|
||||||
var actualBlobName string
|
|
||||||
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(storageContainerName string, blobName string) error {
|
|
||||||
actualStorageContainerName = storageContainerName
|
|
||||||
actualBlobName = blobName
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd")
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
|
|
||||||
if result != multistep.ActionContinue {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualStorageContainerName != "images" {
|
|
||||||
t.Fatalf("Expected the storage container name to be 'images', but found '%s'.", actualStorageContainerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualBlobName != "pkrvm_os.vhd" {
|
|
||||||
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldHandleComplexStorageContainerNames(t *testing.T) {
|
|
||||||
var actualStorageContainerName string
|
|
||||||
var actualBlobName string
|
|
||||||
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(storageContainerName string, blobName string) error {
|
|
||||||
actualStorageContainerName = storageContainerName
|
|
||||||
actualBlobName = blobName
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/abc/def/pkrvm_os.vhd")
|
|
||||||
testSubject.Run(context.Background(), stateBag)
|
|
||||||
|
|
||||||
if actualStorageContainerName != "abc" {
|
|
||||||
t.Fatalf("Expected the storage container name to be 'abc/def', but found '%s'.", actualStorageContainerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualBlobName != "def/pkrvm_os.vhd" {
|
|
||||||
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldFailIfVHDNameCannotBeURLParsed(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
deleteManaged: func(context.Context, string, string) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid URL per https://golang.org/src/net/url/url_test.go
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("http://[fe80::1%en0]/")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionHalt {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%v'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
|
||||||
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestStepDeleteOSDiskShouldFailIfVHDNameIsTooShort(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
deleteManaged: func(context.Context, string, string) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteOSDisk("storage.blob.core.windows.net/abc")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionHalt {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
|
||||||
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldPassIfManagedDiskInTempResourceGroup(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := new(multistep.BasicStateBag)
|
|
||||||
stateBag.Put(constants.ArmOSDiskVhd, "subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk")
|
|
||||||
stateBag.Put(constants.ArmIsManagedImage, true)
|
|
||||||
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
|
|
||||||
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionContinue {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == true {
|
|
||||||
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldFailIfManagedDiskInExistingResourceGroupFailsToDelete(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
deleteManaged: func(context.Context, string, string) error { return errors.New("UNIT TEST FAIL!") },
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := new(multistep.BasicStateBag)
|
|
||||||
stateBag.Put(constants.ArmOSDiskVhd, "subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk")
|
|
||||||
stateBag.Put(constants.ArmIsManagedImage, true)
|
|
||||||
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
|
|
||||||
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionHalt {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
|
||||||
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteOSDiskShouldFailIfManagedDiskInExistingResourceGroupIsDeleted(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteOSDisk{
|
|
||||||
delete: func(string, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
deleteManaged: func(context.Context, string, string) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := new(multistep.BasicStateBag)
|
|
||||||
stateBag.Put(constants.ArmOSDiskVhd, "subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk")
|
|
||||||
stateBag.Put(constants.ArmIsManagedImage, true)
|
|
||||||
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
|
|
||||||
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionContinue {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == true {
|
|
||||||
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteTestStateBagStepDeleteOSDisk(osDiskVhd string) multistep.StateBag {
|
|
||||||
stateBag := new(multistep.BasicStateBag)
|
|
||||||
stateBag.Put(constants.ArmOSDiskVhd, osDiskVhd)
|
|
||||||
stateBag.Put(constants.ArmIsManagedImage, false)
|
|
||||||
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
|
|
||||||
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
|
|
||||||
|
|
||||||
return stateBag
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package arm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/builder/azure/common/constants"
|
|
||||||
"github.com/hashicorp/packer/common/retry"
|
|
||||||
"github.com/hashicorp/packer/helper/multistep"
|
|
||||||
"github.com/hashicorp/packer/packer"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxResourcesToDelete = 50
|
|
||||||
)
|
|
||||||
|
|
||||||
type StepDeleteResourceGroup struct {
|
|
||||||
client *AzureClient
|
|
||||||
delete func(ctx context.Context, state multistep.StateBag, resourceGroupName string) error
|
|
||||||
say func(message string)
|
|
||||||
error func(e error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStepDeleteResourceGroup(client *AzureClient, ui packer.Ui) *StepDeleteResourceGroup {
|
|
||||||
var step = &StepDeleteResourceGroup{
|
|
||||||
client: client,
|
|
||||||
say: func(message string) { ui.Say(message) },
|
|
||||||
error: func(e error) { ui.Error(e.Error()) },
|
|
||||||
}
|
|
||||||
|
|
||||||
step.delete = step.deleteResourceGroup
|
|
||||||
return step
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteResourceGroup) deleteResourceGroup(ctx context.Context, state multistep.StateBag, resourceGroupName string) error {
|
|
||||||
var err error
|
|
||||||
if state.Get(constants.ArmIsExistingResourceGroup).(bool) {
|
|
||||||
s.say("\nThe resource group was not created by Packer, only deleting individual resources ...")
|
|
||||||
var deploymentName = state.Get(constants.ArmDeploymentName).(string)
|
|
||||||
err = s.deleteDeploymentResources(ctx, deploymentName, resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyVaultDeploymentName, ok := state.GetOk(constants.ArmKeyVaultDeploymentName); ok {
|
|
||||||
// Only delete if custom keyvault was not provided.
|
|
||||||
if exists := state.Get(constants.ArmIsExistingKeyVault).(bool); !exists {
|
|
||||||
s.say("\n Deleting the keyvault deployment because it was created by Packer...")
|
|
||||||
err = s.deleteDeploymentResources(ctx, keyVaultDeploymentName.(string), resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
s.say("\nThe resource group was created by Packer, deleting ...")
|
|
||||||
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
|
|
||||||
if err == nil {
|
|
||||||
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
|
|
||||||
// No need to wait for the completion for delete if request is Accepted
|
|
||||||
s.say(fmt.Sprintf("\nResource Group is being deleted, not waiting for deletion due to config. Resource Group Name '%s'", resourceGroupName))
|
|
||||||
} else {
|
|
||||||
f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.say(s.client.LastError.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteResourceGroup) deleteDeploymentResources(ctx context.Context, deploymentName, resourceGroupName string) error {
|
|
||||||
maxResources := int32(maxResourcesToDelete)
|
|
||||||
|
|
||||||
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(ctx, resourceGroupName, deploymentName, &maxResources)
|
|
||||||
if err != nil {
|
|
||||||
s.reportIfError(err, resourceGroupName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for deploymentOperations.NotDone() {
|
|
||||||
deploymentOperation := deploymentOperations.Value()
|
|
||||||
// Sometimes an empty operation is added to the list by Azure
|
|
||||||
if deploymentOperation.Properties.TargetResource == nil {
|
|
||||||
deploymentOperations.Next()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceName := *deploymentOperation.Properties.TargetResource.ResourceName
|
|
||||||
resourceType := *deploymentOperation.Properties.TargetResource.ResourceType
|
|
||||||
|
|
||||||
s.say(fmt.Sprintf(" -> %s : '%s'",
|
|
||||||
resourceType,
|
|
||||||
resourceName))
|
|
||||||
|
|
||||||
retry.Config{
|
|
||||||
Tries: 10,
|
|
||||||
RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 600 * time.Second, Multiplier: 2}).Linear,
|
|
||||||
}.Run(ctx, func(ctx context.Context) error {
|
|
||||||
err := deleteResource(ctx, s.client,
|
|
||||||
resourceType,
|
|
||||||
resourceName,
|
|
||||||
resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
s.reportIfError(err, resourceName)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = deploymentOperations.Next(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteResourceGroup) reportIfError(err error, resourceName string) {
|
|
||||||
if err != nil {
|
|
||||||
s.say(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
|
|
||||||
"Name: %s\n"+
|
|
||||||
"Error: %s", resourceName, err.Error()))
|
|
||||||
s.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeleteResourceGroup) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
||||||
s.say("Deleting resource group ...")
|
|
||||||
|
|
||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
|
||||||
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
|
|
||||||
|
|
||||||
err := s.delete(ctx, state, resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
state.Put(constants.Error, err)
|
|
||||||
s.error(err)
|
|
||||||
|
|
||||||
return multistep.ActionHalt
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Put(constants.ArmIsResourceGroupCreated, false)
|
|
||||||
|
|
||||||
return multistep.ActionContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*StepDeleteResourceGroup) Cleanup(multistep.StateBag) {
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package arm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/builder/azure/common/constants"
|
|
||||||
"github.com/hashicorp/packer/helper/multistep"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStepDeleteResourceGroupShouldFailIfDeleteFails(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteResourceGroup{
|
|
||||||
delete: func(context.Context, multistep.StateBag, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteResourceGroup()
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionHalt {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
|
||||||
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteResourceGroupShouldPassIfDeletePasses(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteResourceGroup{
|
|
||||||
delete: func(context.Context, multistep.StateBag, string) error { return nil },
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteResourceGroup()
|
|
||||||
|
|
||||||
var result = testSubject.Run(context.Background(), stateBag)
|
|
||||||
if result != multistep.ActionContinue {
|
|
||||||
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := stateBag.GetOk(constants.Error); ok == true {
|
|
||||||
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStepDeleteResourceGroupShouldDeleteStateBagArmResourceGroupCreated(t *testing.T) {
|
|
||||||
var testSubject = &StepDeleteResourceGroup{
|
|
||||||
delete: func(context.Context, multistep.StateBag, string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
say: func(message string) {},
|
|
||||||
error: func(e error) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
stateBag := DeleteTestStateBagStepDeleteResourceGroup()
|
|
||||||
testSubject.Run(context.Background(), stateBag)
|
|
||||||
|
|
||||||
value, ok := stateBag.GetOk(constants.ArmIsResourceGroupCreated)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expected the resource bag value arm.IsResourceGroupCreated to exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.(bool) {
|
|
||||||
t.Fatalf("Expected arm.IsResourceGroupCreated to be false, but got %q", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteTestStateBagStepDeleteResourceGroup() multistep.StateBag {
|
|
||||||
stateBag := new(multistep.BasicStateBag)
|
|
||||||
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
|
|
||||||
stateBag.Put(constants.ArmIsResourceGroupCreated, "Unit Test: IsResourceGroupCreated")
|
|
||||||
|
|
||||||
return stateBag
|
|
||||||
}
|
|
||||||
@@ -42,6 +42,109 @@ func NewStepDeployTemplate(client *AzureClient, ui packer.Ui, config *Config, de
|
|||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StepDeployTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
s.say("Deploying deployment template ...")
|
||||||
|
|
||||||
|
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
||||||
|
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
|
||||||
|
s.say(fmt.Sprintf(" -> DeploymentName : '%s'", s.name))
|
||||||
|
|
||||||
|
return processStepResult(
|
||||||
|
s.deploy(ctx, resourceGroupName, s.name),
|
||||||
|
s.error, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
|
||||||
|
defer func() {
|
||||||
|
err := s.deleteTemplate(context.Background(), state)
|
||||||
|
if err != nil {
|
||||||
|
s.say(s.client.LastError.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
//Only clean up if this was an existing resource group and the resource group
|
||||||
|
//is marked as created
|
||||||
|
existingResourceGroup := state.Get(constants.ArmIsExistingResourceGroup).(bool)
|
||||||
|
resourceGroupCreated := state.Get(constants.ArmIsResourceGroupCreated).(bool)
|
||||||
|
if !existingResourceGroup || !resourceGroupCreated {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
ui.Say("\nThe resource group was not created by Packer, deleting individual resources ...")
|
||||||
|
|
||||||
|
deploymentName := s.name
|
||||||
|
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
|
||||||
|
|
||||||
|
// Get image disk details before deleting the image; otherwise we won't be able to
|
||||||
|
// delete the disk as the image request will return a 404
|
||||||
|
computeName := state.Get(constants.ArmComputeName).(string)
|
||||||
|
imageType, imageName, err := s.disk(context.TODO(), resourceGroupName, computeName)
|
||||||
|
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "ResourceNotFound") {
|
||||||
|
ui.Error(fmt.Sprintf("Could not retrieve OS Image details: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(" -> Deployment Resources within: " + deploymentName)
|
||||||
|
if deploymentName != "" {
|
||||||
|
maxResources := int32(50)
|
||||||
|
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(context.TODO(), resourceGroupName, deploymentName, &maxResources)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", resourceGroupName, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for deploymentOperations.NotDone() {
|
||||||
|
deploymentOperation := deploymentOperations.Value()
|
||||||
|
// Sometimes an empty operation is added to the list by Azure
|
||||||
|
if deploymentOperation.Properties.TargetResource == nil {
|
||||||
|
if err := deploymentOperations.Next(); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error moving to to next deployment operation ...\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", resourceGroupName, err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf(" -> %s : '%s'",
|
||||||
|
*deploymentOperation.Properties.TargetResource.ResourceType,
|
||||||
|
*deploymentOperation.Properties.TargetResource.ResourceName))
|
||||||
|
|
||||||
|
err = s.delete(context.TODO(), s.client,
|
||||||
|
*deploymentOperation.Properties.TargetResource.ResourceType,
|
||||||
|
*deploymentOperation.Properties.TargetResource.ResourceName,
|
||||||
|
resourceGroupName)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", *deploymentOperation.Properties.TargetResource.ResourceName, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = deploymentOperations.Next(); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", resourceGroupName, err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The disk is not defined as an operation in the template so it has to be deleted separately
|
||||||
|
if imageType == "" && imageName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf(" -> %s : '%s'", imageType, imageName))
|
||||||
|
err = s.deleteDisk(context.TODO(), imageType, imageName, resourceGroupName)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"Error: %s", imageName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StepDeployTemplate) deployTemplate(ctx context.Context, resourceGroupName string, deploymentName string) error {
|
func (s *StepDeployTemplate) deployTemplate(ctx context.Context, resourceGroupName string, deploymentName string) error {
|
||||||
deployment, err := s.factory(s.config)
|
deployment, err := s.factory(s.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -62,48 +165,32 @@ func (s *StepDeployTemplate) deleteTemplate(ctx context.Context, state multistep
|
|||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
||||||
var deploymentName = s.name
|
var deploymentName = s.name
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
ui.Say(fmt.Sprintf("Removing the created Deployment object: '%s'", deploymentName))
|
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Removing the created Deployment object: '%s'", deploymentName))
|
||||||
f, err := s.client.DeploymentsClient.Delete(ctx, resourceGroupName, deploymentName)
|
f, err := s.client.DeploymentsClient.Delete(ctx, resourceGroupName, deploymentName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
|
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
s.say(s.client.LastError.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepDeployTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
||||||
s.say("Deploying deployment template ...")
|
|
||||||
|
|
||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
|
||||||
|
|
||||||
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
|
|
||||||
s.say(fmt.Sprintf(" -> DeploymentName : '%s'", s.name))
|
|
||||||
|
|
||||||
return processStepResult(
|
|
||||||
s.deploy(ctx, resourceGroupName, s.name),
|
|
||||||
s.error, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepDeployTemplate) getImageDetails(ctx context.Context, resourceGroupName string, computeName string) (string, string, error) {
|
func (s *StepDeployTemplate) getImageDetails(ctx context.Context, resourceGroupName string, computeName string) (string, string, error) {
|
||||||
//We can't depend on constants.ArmOSDiskVhd being set
|
//We can't depend on constants.ArmOSDiskVhd being set
|
||||||
var imageName string
|
var imageName, imageType string
|
||||||
var imageType string
|
|
||||||
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
|
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imageName, imageType, err
|
return imageName, imageType, err
|
||||||
} else {
|
|
||||||
if vm.StorageProfile.OsDisk.Vhd != nil {
|
|
||||||
imageType = "image"
|
|
||||||
imageName = *vm.StorageProfile.OsDisk.Vhd.URI
|
|
||||||
} else {
|
|
||||||
imageType = "Microsoft.Compute/disks"
|
|
||||||
imageName = *vm.StorageProfile.OsDisk.ManagedDisk.ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vm.StorageProfile.OsDisk.Vhd != nil {
|
||||||
|
imageType = "image"
|
||||||
|
imageName = *vm.StorageProfile.OsDisk.Vhd.URI
|
||||||
|
} else {
|
||||||
|
imageType = "Microsoft.Compute/disks"
|
||||||
|
imageName = *vm.StorageProfile.OsDisk.ManagedDisk.ID
|
||||||
|
}
|
||||||
|
|
||||||
return imageType, imageName, nil
|
return imageType, imageName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +245,7 @@ func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string,
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// VHD image
|
// VHD image
|
||||||
u, err := url.Parse(imageName)
|
u, err := url.Parse(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,75 +259,11 @@ func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string,
|
|||||||
var blobName = strings.Join(xs[2:], "/")
|
var blobName = strings.Join(xs[2:], "/")
|
||||||
|
|
||||||
blob := s.client.BlobStorageClient.GetContainerReference(storageAccountName).GetBlobReference(blobName)
|
blob := s.client.BlobStorageClient.GetContainerReference(storageAccountName).GetBlobReference(blobName)
|
||||||
err = blob.Delete(nil)
|
_, err = blob.BreakLease(nil)
|
||||||
return err
|
if err != nil && !strings.Contains(err.Error(), "LeaseNotPresentWithLeaseOperation") {
|
||||||
}
|
s.say(s.client.LastError.Error())
|
||||||
|
return err
|
||||||
func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
|
}
|
||||||
defer s.deleteTemplate(context.Background(), state)
|
|
||||||
|
return blob.Delete(nil)
|
||||||
//Only clean up if this was an existing resource group and the resource group
|
|
||||||
//is marked as created
|
|
||||||
var existingResourceGroup = state.Get(constants.ArmIsExistingResourceGroup).(bool)
|
|
||||||
var resourceGroupCreated = state.Get(constants.ArmIsResourceGroupCreated).(bool)
|
|
||||||
if !existingResourceGroup || !resourceGroupCreated {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ui := state.Get("ui").(packer.Ui)
|
|
||||||
ui.Say("\nThe resource group was not created by Packer, deleting individual resources ...")
|
|
||||||
|
|
||||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
|
||||||
var computeName = state.Get(constants.ArmComputeName).(string)
|
|
||||||
var deploymentName = s.name
|
|
||||||
imageType, imageName, err := s.disk(context.TODO(), resourceGroupName, computeName)
|
|
||||||
if err != nil {
|
|
||||||
ui.Error("Could not retrieve OS Image details")
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Say(" -> Deployment Resources within: " + deploymentName)
|
|
||||||
if deploymentName != "" {
|
|
||||||
maxResources := int32(50)
|
|
||||||
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(context.TODO(), resourceGroupName, deploymentName, &maxResources)
|
|
||||||
if err != nil {
|
|
||||||
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
|
|
||||||
"Name: %s\n"+
|
|
||||||
"Error: %s", resourceGroupName, err))
|
|
||||||
}
|
|
||||||
for deploymentOperations.NotDone() {
|
|
||||||
deploymentOperation := deploymentOperations.Value()
|
|
||||||
// Sometimes an empty operation is added to the list by Azure
|
|
||||||
if deploymentOperation.Properties.TargetResource == nil {
|
|
||||||
deploymentOperations.Next()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ui.Say(fmt.Sprintf(" -> %s : '%s'",
|
|
||||||
*deploymentOperation.Properties.TargetResource.ResourceType,
|
|
||||||
*deploymentOperation.Properties.TargetResource.ResourceName))
|
|
||||||
err = s.delete(context.TODO(), s.client,
|
|
||||||
*deploymentOperation.Properties.TargetResource.ResourceType,
|
|
||||||
*deploymentOperation.Properties.TargetResource.ResourceName,
|
|
||||||
resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
|
|
||||||
"Name: %s\n"+
|
|
||||||
"Error: %s", *deploymentOperation.Properties.TargetResource.ResourceName, err))
|
|
||||||
}
|
|
||||||
if err = deploymentOperations.Next(); err != nil {
|
|
||||||
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
|
|
||||||
"Name: %s\n"+
|
|
||||||
"Error: %s", resourceGroupName, err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The disk is not defined as an operation in the template so has to be
|
|
||||||
// deleted separately
|
|
||||||
ui.Say(fmt.Sprintf(" -> %s : '%s'", imageType, imageName))
|
|
||||||
err = s.deleteDisk(context.TODO(), imageType, imageName, resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
|
|
||||||
"Name: %s\n"+
|
|
||||||
"Error: %s", imageName, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,8 +391,11 @@ func checkHyperVGeneration(s string) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||||
if runtime.GOOS != "linux" {
|
switch runtime.GOOS {
|
||||||
return nil, errors.New("the azure-chroot builder only works on Linux environments")
|
case "linux", "freebsd":
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return nil, errors.New("the azure-chroot builder only works on Linux and FreeBSD environments")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.config.ClientConfig.FillParameters()
|
err := b.config.ClientConfig.FillParameters()
|
||||||
|
|||||||
@@ -3,24 +3,18 @@ package chroot
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/packer/builder/azure/common/client"
|
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
"github.com/Azure/go-autorest/autorest/azure"
|
||||||
"github.com/Azure/go-autorest/autorest/to"
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
|
"github.com/hashicorp/packer/builder/azure/common/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiskAttacher interface {
|
type DiskAttacher interface {
|
||||||
AttachDisk(ctx context.Context, disk string) (lun int32, err error)
|
AttachDisk(ctx context.Context, disk string) (lun int32, err error)
|
||||||
DiskPathForLun(lun int32) string
|
|
||||||
WaitForDevice(ctx context.Context, i int32) (device string, err error)
|
WaitForDevice(ctx context.Context, i int32) (device string, err error)
|
||||||
DetachDisk(ctx context.Context, disk string) (err error)
|
DetachDisk(ctx context.Context, disk string) (err error)
|
||||||
WaitForDetach(ctx context.Context, diskID string) error
|
WaitForDetach(ctx context.Context, diskID string) error
|
||||||
@@ -38,31 +32,7 @@ type diskAttacher struct {
|
|||||||
vm *client.ComputeInfo // store info about this VM so that we don't have to ask metadata service on every call
|
vm *client.ComputeInfo // store info about this VM so that we don't have to ask metadata service on every call
|
||||||
}
|
}
|
||||||
|
|
||||||
func (diskAttacher) DiskPathForLun(lun int32) string {
|
var DiskNotFoundError = errors.New("Disk not found")
|
||||||
return fmt.Sprintf("/dev/disk/azure/scsi1/lun%d", lun)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
|
|
||||||
path := da.DiskPathForLun(lun)
|
|
||||||
|
|
||||||
for {
|
|
||||||
link, err := os.Readlink(path)
|
|
||||||
if err == nil {
|
|
||||||
return filepath.Abs("/dev/disk/azure/scsi1/" + link)
|
|
||||||
} else if err != os.ErrNotExist {
|
|
||||||
if pe, ok := err.(*os.PathError); ok && pe.Err != syscall.ENOENT {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
// continue
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (da *diskAttacher) DetachDisk(ctx context.Context, diskID string) error {
|
func (da *diskAttacher) DetachDisk(ctx context.Context, diskID string) error {
|
||||||
log.Println("Fetching list of disks currently attached to VM")
|
log.Println("Fetching list of disks currently attached to VM")
|
||||||
@@ -111,8 +81,6 @@ func (da *diskAttacher) WaitForDetach(ctx context.Context, diskID string) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var DiskNotFoundError = errors.New("Disk not found")
|
|
||||||
|
|
||||||
func (da *diskAttacher) AttachDisk(ctx context.Context, diskID string) (int32, error) {
|
func (da *diskAttacher) AttachDisk(ctx context.Context, diskID string) (int32, error) {
|
||||||
dataDisks, err := da.getDisks(ctx)
|
dataDisks, err := da.getDisks(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package chroot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
|
||||||
|
// This builder will always be running in Azure, where data disks show up
|
||||||
|
// on scbus5 target 0. The camcontrol command always outputs LUNs in
|
||||||
|
// unpadded hexadecimal format.
|
||||||
|
regexStr := fmt.Sprintf(`at scbus5 target 0 lun %x \(.*?da([\d]+)`, lun)
|
||||||
|
devRegex, err := regexp.Compile(regexStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
cmd := exec.Command("camcontrol", "devlist")
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
outString := out.String()
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(outString))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
// Check if this is the correct bus, target, and LUN.
|
||||||
|
if matches := devRegex.FindStringSubmatch(line); matches != nil {
|
||||||
|
// If this function immediately returns, devfs won't have
|
||||||
|
// created the device yet.
|
||||||
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
return fmt.Sprintf("/dev/da%s", matches[1]), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package chroot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func diskPathForLun(lun int32) string {
|
||||||
|
return fmt.Sprintf("/dev/disk/azure/scsi1/lun%d", lun)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
|
||||||
|
path := diskPathForLun(lun)
|
||||||
|
|
||||||
|
for {
|
||||||
|
link, err := os.Readlink(path)
|
||||||
|
if err == nil {
|
||||||
|
return filepath.Abs("/dev/disk/azure/scsi1/" + link)
|
||||||
|
} else if err != os.ErrNotExist {
|
||||||
|
if pe, ok := err.(*os.PathError); ok && pe.Err != syscall.ENOENT {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// +build !linux,!freebsd
|
||||||
|
|
||||||
|
package chroot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
|
||||||
|
panic("The azure-chroot builder does not work on this platform.")
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/packer/common"
|
"github.com/hashicorp/packer/common"
|
||||||
@@ -62,7 +63,13 @@ func (s *StepMountDevice) Run(ctx context.Context, state multistep.StateBag) mul
|
|||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceMount := fmt.Sprintf("%s%s", device, s.MountPartition)
|
var deviceMount string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
deviceMount = fmt.Sprintf("%sp%s", device, s.MountPartition)
|
||||||
|
default:
|
||||||
|
deviceMount = fmt.Sprintf("%s%s", device, s.MountPartition)
|
||||||
|
}
|
||||||
|
|
||||||
state.Put("deviceMount", deviceMount)
|
state.Put("deviceMount", deviceMount)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package chroot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStepMountDevice_Run(t *testing.T) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux", "freebsd":
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
t.Skip("Unsupported operating system")
|
||||||
|
}
|
||||||
|
mountPath, err := ioutil.TempDir("", "stepmountdevicetest")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to create a temporary directory: %q", err)
|
||||||
|
}
|
||||||
|
step := &StepMountDevice{
|
||||||
|
MountOptions: []string{"foo"},
|
||||||
|
MountPartition: "42",
|
||||||
|
MountPath: mountPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotCommand string
|
||||||
|
var wrapper common.CommandWrapper
|
||||||
|
wrapper = func(ran string) (string, error) {
|
||||||
|
gotCommand = ran
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("wrappedCommand", wrapper)
|
||||||
|
state.Put("device", "/dev/quux")
|
||||||
|
|
||||||
|
ui, getErrs := testUI()
|
||||||
|
state.Put("ui", ui)
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
state.Put("config", &config)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
got := step.Run(ctx, state)
|
||||||
|
if got != multistep.ActionContinue {
|
||||||
|
t.Errorf("Expected 'continue', but got '%v'", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedMountDevice string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
expectedMountDevice = "/dev/quuxp42"
|
||||||
|
default: // currently just Linux
|
||||||
|
expectedMountDevice = "/dev/quux42"
|
||||||
|
}
|
||||||
|
expectedCommand := fmt.Sprintf("mount -o foo %s %s", expectedMountDevice, mountPath)
|
||||||
|
if gotCommand != expectedCommand {
|
||||||
|
t.Errorf("Expected '%v', but got '%v'", expectedCommand, gotCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(mountPath)
|
||||||
|
_ = getErrs
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
|||||||
scanCodesToSendString := strings.Join(codes, " ")
|
scanCodesToSendString := strings.Join(codes, " ")
|
||||||
return driver.TypeScanCodes(vmName, scanCodesToSendString)
|
return driver.TypeScanCodes(vmName, scanCodesToSendString)
|
||||||
}
|
}
|
||||||
d := bootcommand.NewPCXTDriver(sendCodes, -1, s.GroupInterval)
|
d := bootcommand.NewPCXTDriver(sendCodes, 32, s.GroupInterval)
|
||||||
|
|
||||||
ui.Say("Typing the boot command...")
|
ui.Say("Typing the boot command...")
|
||||||
command, err := interpolate.Render(s.BootCommand, &s.Ctx)
|
command, err := interpolate.Render(s.BootCommand, &s.Ctx)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type ExportConfig struct {
|
|||||||
// This can be useful for passing product information to include in the
|
// This can be useful for passing product information to include in the
|
||||||
// resulting appliance file. Packer JSON configuration file example:
|
// resulting appliance file. Packer JSON configuration file example:
|
||||||
//
|
//
|
||||||
|
// In JSON:
|
||||||
// ```json
|
// ```json
|
||||||
// {
|
// {
|
||||||
// "type": "virtualbox-iso",
|
// "type": "virtualbox-iso",
|
||||||
@@ -31,6 +32,19 @@ type ExportConfig struct {
|
|||||||
// }
|
// }
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// source "virtualbox-iso" "basic-example" {
|
||||||
|
// export_opts = [
|
||||||
|
// "--manifest",
|
||||||
|
// "--vsys", "0",
|
||||||
|
// "--description", "{{user `vm_description`}}",
|
||||||
|
// "--version", "{{user `vm_version`}}"
|
||||||
|
// ]
|
||||||
|
// format = "ova"
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
// A VirtualBox [VM
|
// A VirtualBox [VM
|
||||||
// description](https://www.virtualbox.org/manual/ch09.html#vboxmanage-export-ovf)
|
// description](https://www.virtualbox.org/manual/ch09.html#vboxmanage-export-ovf)
|
||||||
// may contain arbitrary strings; the GUI interprets HTML formatting. However,
|
// may contain arbitrary strings; the GUI interprets HTML formatting. However,
|
||||||
|
|||||||
@@ -6,17 +6,40 @@ import (
|
|||||||
"github.com/hashicorp/packer/template/interpolate"
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//In order to perform extra customization of the virtual machine, a template can
|
||||||
|
//define extra calls to `VBoxManage` to perform.
|
||||||
|
//[VBoxManage](https://www.virtualbox.org/manual/ch09.html) is the command-line
|
||||||
|
//interface to VirtualBox where you can completely control VirtualBox. It can be
|
||||||
|
//used to do things such as set RAM, CPUs, etc.
|
||||||
type VBoxManageConfig struct {
|
type VBoxManageConfig struct {
|
||||||
// Custom `VBoxManage` commands to execute in order to further customize
|
// Custom `VBoxManage` commands to execute in order to further customize
|
||||||
// the virtual machine being created. The value of this is an array of
|
// the virtual machine being created. The example shown below sets the memory and number of CPUs
|
||||||
// commands to execute. The commands are executed in the order defined in
|
// within the virtual machine:
|
||||||
// the template. For each command, the command is defined itself as an
|
//
|
||||||
// array of strings, where each string represents a single argument on the
|
// In JSON:
|
||||||
// command-line to `VBoxManage` (but excluding `VBoxManage` itself). Each
|
// ```json
|
||||||
// arg is treated as a [configuration
|
// "vboxmanage": [
|
||||||
// template](/docs/templates/engine), where the `Name` variable is
|
// ["modifyvm", "{{.Name}}", "--memory", "1024"],
|
||||||
// replaced with the VM name. More details on how to use `VBoxManage` are
|
// ["modifyvm", "{{.Name}}", "--cpus", "2"]
|
||||||
// below.
|
// ]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// vboxmanage = [
|
||||||
|
// ["modifyvm", "{{.Name}}", "--memory", "1024"],
|
||||||
|
// ["modifyvm", "{{.Name}}", "--cpus", "2"],
|
||||||
|
// ]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The value of `vboxmanage` is an array of commands to execute. These commands are
|
||||||
|
// executed in the order defined. So in the above example, the memory will be set
|
||||||
|
// followed by the CPUs.
|
||||||
|
// Each command itself is an array of strings, where each string is an argument to
|
||||||
|
// `VBoxManage`. Each argument is treated as a [configuration
|
||||||
|
// template](/docs/templates/engine). The only available
|
||||||
|
// variable is `Name` which is replaced with the unique name of the VM, which is
|
||||||
|
// required for many VBoxManage calls.
|
||||||
VBoxManage [][]string `mapstructure:"vboxmanage" required:"false"`
|
VBoxManage [][]string `mapstructure:"vboxmanage" required:"false"`
|
||||||
// Identical to vboxmanage,
|
// Identical to vboxmanage,
|
||||||
// except that it is run after the virtual machine is shutdown, and before the
|
// except that it is run after the virtual machine is shutdown, and before the
|
||||||
|
|||||||
@@ -89,12 +89,20 @@ type Config struct {
|
|||||||
// pack](https://www.virtualbox.org/wiki/Downloads#VirtualBox6.0.14OracleVMVirtualBoxExtensionPack)
|
// pack](https://www.virtualbox.org/wiki/Downloads#VirtualBox6.0.14OracleVMVirtualBoxExtensionPack)
|
||||||
// and you will need to enable EFI mode for nvme to work, ex:
|
// and you will need to enable EFI mode for nvme to work, ex:
|
||||||
//
|
//
|
||||||
|
// In JSON:
|
||||||
// ```json
|
// ```json
|
||||||
// "vboxmanage": [
|
// "vboxmanage": [
|
||||||
// [ "modifyvm", "{{.Name}}", "--firmware", "EFI" ],
|
// [ "modifyvm", "{{.Name}}", "--firmware", "EFI" ],
|
||||||
// ]
|
// ]
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// vboxmanage = [
|
||||||
|
// [ "modifyvm", "{{.Name}}", "--firmware", "EFI" ],
|
||||||
|
// ]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
HardDriveInterface string `mapstructure:"hard_drive_interface" required:"false"`
|
HardDriveInterface string `mapstructure:"hard_drive_interface" required:"false"`
|
||||||
// The number of ports available on any SATA controller created, defaults
|
// The number of ports available on any SATA controller created, defaults
|
||||||
// to 1. VirtualBox supports up to 30 ports on a maximum of 1 SATA
|
// to 1. VirtualBox supports up to 30 ports on a maximum of 1 SATA
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func (d *ESX5Driver) Stop(vmxPathLocal string) error {
|
|||||||
|
|
||||||
func (d *ESX5Driver) Register(vmxPathLocal string) error {
|
func (d *ESX5Driver) Register(vmxPathLocal string) error {
|
||||||
vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal)))
|
vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal)))
|
||||||
if err := d.upload(vmxPath, vmxPathLocal); err != nil {
|
if err := d.upload(vmxPath, vmxPathLocal, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r, err := d.run(nil, "vim-cmd", "solo/registervm", strconv.Quote(vmxPath))
|
r, err := d.run(nil, "vim-cmd", "solo/registervm", strconv.Quote(vmxPath))
|
||||||
@@ -185,7 +185,7 @@ func (d *ESX5Driver) IsDestroyed() (bool, error) {
|
|||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ESX5Driver) UploadISO(localPath string, checksum string) (string, error) {
|
func (d *ESX5Driver) UploadISO(localPath string, checksum string, ui packer.Ui) (string, error) {
|
||||||
finalPath := d.CachePath(localPath)
|
finalPath := d.CachePath(localPath)
|
||||||
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
|
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -198,7 +198,7 @@ func (d *ESX5Driver) UploadISO(localPath string, checksum string) (string, error
|
|||||||
}
|
}
|
||||||
log.Println("Initial checksum did not match, uploading.")
|
log.Println("Initial checksum did not match, uploading.")
|
||||||
|
|
||||||
if err := d.upload(finalPath, localPath); err != nil {
|
if err := d.upload(finalPath, localPath, ui); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,12 +667,26 @@ func (d *ESX5Driver) mkdir(path string) error {
|
|||||||
return d.sh("mkdir", "-p", strconv.Quote(path))
|
return d.sh("mkdir", "-p", strconv.Quote(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ESX5Driver) upload(dst, src string) error {
|
func (d *ESX5Driver) upload(dst, src string, ui packer.Ui) error {
|
||||||
|
// Get size so we can set up progress tracker
|
||||||
|
info, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
f, err := os.Open(src)
|
f, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
if ui != nil {
|
||||||
|
pf := ui.TrackProgress(filepath.Base(src), 0, info.Size(), f)
|
||||||
|
defer pf.Close()
|
||||||
|
|
||||||
|
return d.comm.Upload(dst, pf, &info)
|
||||||
|
}
|
||||||
|
|
||||||
return d.comm.Upload(dst, f, nil)
|
return d.comm.Upload(dst, f, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type HWConfig struct {
|
|||||||
// machine](https://kb.vmware.com/s/article/1001805) for desktop VMware
|
// machine](https://kb.vmware.com/s/article/1001805) for desktop VMware
|
||||||
// clients. For ESXi, refer to the proper ESXi documentation.
|
// clients. For ESXi, refer to the proper ESXi documentation.
|
||||||
NetworkAdapterType string `mapstructure:"network_adapter_type" required:"false"`
|
NetworkAdapterType string `mapstructure:"network_adapter_type" required:"false"`
|
||||||
|
// The custom name of the network. Sets the vmx value "ethernet0.networkName"
|
||||||
|
NetworkName string `mapstructure:"network_name" required:"false"`
|
||||||
// Specify whether to enable VMware's virtual soundcard device when
|
// Specify whether to enable VMware's virtual soundcard device when
|
||||||
// building the VM. Defaults to false.
|
// building the VM. Defaults to false.
|
||||||
Sound bool `mapstructure:"sound" required:"false"`
|
Sound bool `mapstructure:"sound" required:"false"`
|
||||||
|
|||||||
@@ -10,13 +10,39 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type OutputConfig struct {
|
type OutputConfig struct {
|
||||||
// This is the path to the directory where the
|
// This is the path on your local machine (the one running Packer) to the
|
||||||
// resulting virtual machine will be created. This may be relative or absolute.
|
// directory where the resulting virtual machine will be created.
|
||||||
// If relative, the path is relative to the working directory when packer
|
// This may be relative or absolute. If relative, the path is relative to
|
||||||
// is executed. This directory must not exist or be empty prior to running
|
// the working directory when packer is executed.
|
||||||
// the builder. By default this is output-BUILDNAME where "BUILDNAME" is the
|
//
|
||||||
// name of the build.
|
// If you are running a remote esx build, the output_dir is the path on your
|
||||||
|
// local machine (the machine running Packer) to which Packer will export
|
||||||
|
// the vm if you have `"skip_export": false`. If you want to manage the
|
||||||
|
// virtual machine's path on the remote datastore, use `remote_output_dir`.
|
||||||
|
//
|
||||||
|
// This directory must not exist or be empty prior to running
|
||||||
|
// the builder.
|
||||||
|
//
|
||||||
|
// By default this is output-BUILDNAME where "BUILDNAME" is the name of the
|
||||||
|
// build.
|
||||||
OutputDir string `mapstructure:"output_directory" required:"false"`
|
OutputDir string `mapstructure:"output_directory" required:"false"`
|
||||||
|
// This is the directoy on your remote esx host where you will save your
|
||||||
|
// vm, relative to your remote_datastore.
|
||||||
|
//
|
||||||
|
// This option's default value is your `vm_name`, and the final path of your
|
||||||
|
// vm will be vmfs/volumes/$remote_datastore/$vm_name/$vm_name.vmx where
|
||||||
|
// `$remote_datastore` and `$vm_name` match their corresponding template
|
||||||
|
// options
|
||||||
|
//
|
||||||
|
// For example, setting `"remote_output_directory": "path/to/subdir`
|
||||||
|
// will create a directory `/vmfs/volumes/remote_datastore/path/to/subdir`.
|
||||||
|
//
|
||||||
|
// Packer will not create the remote datastore for you; it must already
|
||||||
|
// exist. However, Packer will create all directories defined in the option
|
||||||
|
// that do not currently exist.
|
||||||
|
//
|
||||||
|
// This option will be ignored unless you are building on a remote esx host.
|
||||||
|
RemoteOutputDir string `mapstructure:"remote_output_directory" required:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error {
|
func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
type RemoteDriver interface {
|
type RemoteDriver interface {
|
||||||
Driver
|
Driver
|
||||||
|
|
||||||
// UploadISO uploads a local ISO to the remote side and returns the
|
// UploadISO uploads a local ISO to the remote side and returns the
|
||||||
// new path that should be used in the VMX along with an error if it
|
// new path that should be used in the VMX along with an error if it
|
||||||
// exists.
|
// exists.
|
||||||
UploadISO(path string, checksum string) (string, error)
|
UploadISO(path string, checksum string, ui packer.Ui) (string, error)
|
||||||
|
|
||||||
// RemoveCache deletes localPath from the remote cache.
|
// RemoveCache deletes localPath from the remote cache.
|
||||||
RemoveCache(localPath string) error
|
RemoveCache(localPath string) error
|
||||||
@@ -24,7 +28,7 @@ type RemoteDriver interface {
|
|||||||
IsDestroyed() (bool, error)
|
IsDestroyed() (bool, error)
|
||||||
|
|
||||||
// Uploads a local file to remote side.
|
// Uploads a local file to remote side.
|
||||||
upload(dst, src string) error
|
upload(dst, src string, ui packer.Ui) error
|
||||||
|
|
||||||
// Download a remote file to a local file.
|
// Download a remote file to a local file.
|
||||||
Download(src, dst string) error
|
Download(src, dst string) error
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
type RemoteDriverMock struct {
|
type RemoteDriverMock struct {
|
||||||
DriverMock
|
DriverMock
|
||||||
|
|
||||||
@@ -29,10 +33,14 @@ type RemoteDriverMock struct {
|
|||||||
RemovedCachePath string
|
RemovedCachePath string
|
||||||
CacheRemoved bool
|
CacheRemoved bool
|
||||||
|
|
||||||
|
ReturnValDirExists bool
|
||||||
|
|
||||||
ReloadVMErr error
|
ReloadVMErr error
|
||||||
|
|
||||||
|
outputDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RemoteDriverMock) UploadISO(path string, checksum string) (string, error) {
|
func (d *RemoteDriverMock) UploadISO(path string, checksum string, ui packer.Ui) (string, error) {
|
||||||
d.UploadISOCalled = true
|
d.UploadISOCalled = true
|
||||||
d.UploadISOPath = path
|
d.UploadISOPath = path
|
||||||
return d.UploadISOResult, d.UploadISOErr
|
return d.UploadISOResult, d.UploadISOErr
|
||||||
@@ -60,7 +68,7 @@ func (d *RemoteDriverMock) IsDestroyed() (bool, error) {
|
|||||||
return d.IsDestroyedResult, d.IsDestroyedErr
|
return d.IsDestroyedResult, d.IsDestroyedErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RemoteDriverMock) upload(dst, src string) error {
|
func (d *RemoteDriverMock) upload(dst, src string, ui packer.Ui) error {
|
||||||
return d.UploadErr
|
return d.UploadErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,3 +85,33 @@ func (d *RemoteDriverMock) RemoveCache(localPath string) error {
|
|||||||
func (d *RemoteDriverMock) ReloadVM() error {
|
func (d *RemoteDriverMock) ReloadVM() error {
|
||||||
return d.ReloadVMErr
|
return d.ReloadVMErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the following functions satisfy the Outputdir interface
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) DirExists() (bool, error) {
|
||||||
|
return d.ReturnValDirExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) ListFiles() ([]string, error) {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) MkdirAll() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) Remove(string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) RemoveAll() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) SetOutputDir(s string) {
|
||||||
|
d.outputDir = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RemoteDriverMock) String() string {
|
||||||
|
return d.outputDir
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,13 +16,62 @@ import (
|
|||||||
type StepOutputDir struct {
|
type StepOutputDir struct {
|
||||||
Force bool
|
Force bool
|
||||||
|
|
||||||
|
OutputConfig *OutputConfig
|
||||||
|
VMName string
|
||||||
|
|
||||||
|
RemoteType string
|
||||||
|
|
||||||
success bool
|
success bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
func (s *StepOutputDir) SetOutputAndExportDirs(state multistep.StateBag) OutputDir {
|
||||||
dir := state.Get("dir").(OutputDir)
|
driver := state.Get("driver")
|
||||||
ui := state.Get("ui").(packer.Ui)
|
|
||||||
|
|
||||||
|
// Hold on to your pants. The output configuration is a little more complex
|
||||||
|
// than you'd expect because of all the moving parts between local and
|
||||||
|
// remote output, and exports, and legacy behavior.
|
||||||
|
var dir OutputDir
|
||||||
|
switch d := driver.(type) {
|
||||||
|
case OutputDir:
|
||||||
|
// The driver fulfils the OutputDir interface so that it can create
|
||||||
|
// output files on the remote instance.
|
||||||
|
dir = d
|
||||||
|
default:
|
||||||
|
// The driver will be running the build and creating the output
|
||||||
|
// directory locally
|
||||||
|
dir = new(LocalOutputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If remote type is esx, we need to track both the output dir on the remote
|
||||||
|
// instance and the output dir locally. exportOutputPath is where we track
|
||||||
|
// the local output dir.
|
||||||
|
exportOutputPath := s.OutputConfig.OutputDir
|
||||||
|
|
||||||
|
if s.RemoteType != "" {
|
||||||
|
if s.OutputConfig.RemoteOutputDir != "" {
|
||||||
|
// User set the remote output dir.
|
||||||
|
s.OutputConfig.OutputDir = s.OutputConfig.RemoteOutputDir
|
||||||
|
} else {
|
||||||
|
// Default output dir to vm name. On remote esx instance, this will
|
||||||
|
// become something like /vmfs/volumes/mydatastore/vmname/vmname.vmx
|
||||||
|
s.OutputConfig.OutputDir = s.VMName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remember, this one's either the output from a local build, or the remote
|
||||||
|
// output from a remote build. Not the local export path for a remote build.
|
||||||
|
dir.SetOutputDir(s.OutputConfig.OutputDir)
|
||||||
|
|
||||||
|
// Set dir in the state for use in file cleanup and artifact
|
||||||
|
state.Put("dir", dir)
|
||||||
|
state.Put("export_output_path", exportOutputPath)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
ui.Say("Configuring output and export directories...")
|
||||||
|
|
||||||
|
dir := s.SetOutputAndExportDirs(state)
|
||||||
exists, err := dir.DirExists()
|
exists, err := dir.DirExists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
@@ -31,7 +80,7 @@ func (s *StepOutputDir) Run(ctx context.Context, state multistep.StateBag) multi
|
|||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
if s.Force {
|
if s.Force {
|
||||||
ui.Say("Deleting previous output directory...")
|
ui.Message("Deleting previous output directory...")
|
||||||
dir.RemoveAll()
|
dir.RemoveAll()
|
||||||
} else {
|
} else {
|
||||||
state.Put("error", fmt.Errorf(
|
state.Put("error", fmt.Errorf(
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ import (
|
|||||||
"github.com/hashicorp/packer/helper/multistep"
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testOutputDir(t *testing.T) *LocalOutputDir {
|
func testOutputDir(t *testing.T) string {
|
||||||
td, err := ioutil.TempDir("", "packer")
|
td, err := ioutil.TempDir("", "packer")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
os.RemoveAll(td)
|
os.RemoveAll(td)
|
||||||
|
|
||||||
result := new(LocalOutputDir)
|
return td
|
||||||
result.SetOutputDir(td)
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_impl(t *testing.T) {
|
func TestStepOutputDir_impl(t *testing.T) {
|
||||||
@@ -27,12 +25,20 @@ func TestStepOutputDir_impl(t *testing.T) {
|
|||||||
|
|
||||||
func TestStepOutputDir(t *testing.T) {
|
func TestStepOutputDir(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepOutputDir)
|
driver := new(DriverMock)
|
||||||
|
state.Put("driver", driver)
|
||||||
|
|
||||||
dir := testOutputDir(t)
|
td := testOutputDir(t)
|
||||||
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
}
|
||||||
|
|
||||||
|
step := &StepOutputDir{
|
||||||
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
}
|
||||||
// Delete the test output directory when done
|
// Delete the test output directory when done
|
||||||
defer os.RemoveAll(dir.dir)
|
defer os.RemoveAll(td)
|
||||||
state.Put("dir", dir)
|
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
@@ -41,29 +47,37 @@ func TestStepOutputDir(t *testing.T) {
|
|||||||
if _, ok := state.GetOk("error"); ok {
|
if _, ok := state.GetOk("error"); ok {
|
||||||
t.Fatal("should NOT have error")
|
t.Fatal("should NOT have error")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the cleanup
|
// Test the cleanup
|
||||||
step.Cleanup(state)
|
step.Cleanup(state)
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_existsNoForce(t *testing.T) {
|
func TestStepOutputDir_existsNoForce(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepOutputDir)
|
|
||||||
|
|
||||||
dir := testOutputDir(t)
|
td := testOutputDir(t)
|
||||||
state.Put("dir", dir)
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
}
|
||||||
|
|
||||||
|
step := &StepOutputDir{
|
||||||
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
}
|
||||||
|
// Delete the test output directory when done
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
// Make sure the dir exists
|
// Make sure the dir exists
|
||||||
if err := os.MkdirAll(dir.dir, 0755); err != nil {
|
if err := os.MkdirAll(td, 0755); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(dir.dir)
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
@@ -75,24 +89,33 @@ func TestStepOutputDir_existsNoForce(t *testing.T) {
|
|||||||
|
|
||||||
// Test the cleanup
|
// Test the cleanup
|
||||||
step.Cleanup(state)
|
step.Cleanup(state)
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatal("should not delete dir")
|
t.Fatal("should not delete dir")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_existsForce(t *testing.T) {
|
func TestStepOutputDir_existsForce(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepOutputDir)
|
|
||||||
|
td := testOutputDir(t)
|
||||||
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
}
|
||||||
|
|
||||||
|
step := &StepOutputDir{
|
||||||
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
}
|
||||||
step.Force = true
|
step.Force = true
|
||||||
|
|
||||||
dir := testOutputDir(t)
|
// Delete the test output directory when done
|
||||||
state.Put("dir", dir)
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
// Make sure the dir exists
|
// Make sure the dir exists
|
||||||
if err := os.MkdirAll(dir.dir, 0755); err != nil {
|
if err := os.MkdirAll(td, 0755); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(dir.dir)
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
@@ -101,17 +124,22 @@ func TestStepOutputDir_existsForce(t *testing.T) {
|
|||||||
if _, ok := state.GetOk("error"); ok {
|
if _, ok := state.GetOk("error"); ok {
|
||||||
t.Fatal("should NOT have error")
|
t.Fatal("should NOT have error")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_cancel(t *testing.T) {
|
func TestStepOutputDir_cancel(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepOutputDir)
|
td := testOutputDir(t)
|
||||||
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
}
|
||||||
|
|
||||||
dir := testOutputDir(t)
|
step := &StepOutputDir{
|
||||||
state.Put("dir", dir)
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
}
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
@@ -120,24 +148,29 @@ func TestStepOutputDir_cancel(t *testing.T) {
|
|||||||
if _, ok := state.GetOk("error"); ok {
|
if _, ok := state.GetOk("error"); ok {
|
||||||
t.Fatal("should NOT have error")
|
t.Fatal("should NOT have error")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test cancel/halt
|
// Test cancel/halt
|
||||||
state.Put(multistep.StateCancelled, true)
|
state.Put(multistep.StateCancelled, true)
|
||||||
step.Cleanup(state)
|
step.Cleanup(state)
|
||||||
if _, err := os.Stat(dir.dir); err == nil {
|
if _, err := os.Stat(td); err == nil {
|
||||||
t.Fatal("directory should not exist")
|
t.Fatal("directory should not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_halt(t *testing.T) {
|
func TestStepOutputDir_halt(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepOutputDir)
|
td := testOutputDir(t)
|
||||||
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
}
|
||||||
|
|
||||||
dir := testOutputDir(t)
|
step := &StepOutputDir{
|
||||||
state.Put("dir", dir)
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
}
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
@@ -146,14 +179,46 @@ func TestStepOutputDir_halt(t *testing.T) {
|
|||||||
if _, ok := state.GetOk("error"); ok {
|
if _, ok := state.GetOk("error"); ok {
|
||||||
t.Fatal("should NOT have error")
|
t.Fatal("should NOT have error")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(dir.dir); err != nil {
|
if _, err := os.Stat(td); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test cancel/halt
|
// Test cancel/halt
|
||||||
state.Put(multistep.StateHalted, true)
|
state.Put(multistep.StateHalted, true)
|
||||||
step.Cleanup(state)
|
step.Cleanup(state)
|
||||||
if _, err := os.Stat(dir.dir); err == nil {
|
if _, err := os.Stat(td); err == nil {
|
||||||
t.Fatal("directory should not exist")
|
t.Fatal("directory should not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepOutputDir_Remote(t *testing.T) {
|
||||||
|
// Tests remote driver
|
||||||
|
state := testState(t)
|
||||||
|
driver := new(RemoteDriverMock)
|
||||||
|
state.Put("driver", driver)
|
||||||
|
|
||||||
|
td := testOutputDir(t)
|
||||||
|
outconfig := &OutputConfig{
|
||||||
|
OutputDir: td,
|
||||||
|
RemoteOutputDir: "remote_path",
|
||||||
|
}
|
||||||
|
|
||||||
|
step := &StepOutputDir{
|
||||||
|
OutputConfig: outconfig,
|
||||||
|
VMName: "testVM",
|
||||||
|
RemoteType: "esx5",
|
||||||
|
}
|
||||||
|
// Delete the test output directory when done
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
|
// Test the run
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't pre-create the output path for export but we do set it in state.
|
||||||
|
exportOutputPath := state.Get("export_output_path").(string)
|
||||||
|
if exportOutputPath != td {
|
||||||
|
t.Fatalf("err: should have set export_output_path!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (s *StepRegister) Cleanup(state multistep.StateBag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if remoteDriver, ok := driver.(RemoteDriver); ok {
|
if remoteDriver, ok := driver.(RemoteDriver); ok {
|
||||||
if s.SkipExport {
|
if s.SkipExport && !cancelled && !halted {
|
||||||
ui.Say("Unregistering virtual machine...")
|
ui.Say("Unregistering virtual machine...")
|
||||||
if err := remoteDriver.Unregister(s.registeredPath); err != nil {
|
if err := remoteDriver.Unregister(s.registeredPath); err != nil {
|
||||||
ui.Error(fmt.Sprintf("Error unregistering VM: %s", err))
|
ui.Error(fmt.Sprintf("Error unregistering VM: %s", err))
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (s *StepRemoteUpload) Run(ctx context.Context, state multistep.StateBag) mu
|
|||||||
|
|
||||||
ui.Say(s.Message)
|
ui.Say(s.Message)
|
||||||
log.Printf("Remote uploading: %s", path)
|
log.Printf("Remote uploading: %s", path)
|
||||||
newPath, err := remote.UploadISO(path, s.Checksum)
|
newPath, err := remote.UploadISO(path, s.Checksum, ui)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error uploading file: %s", err)
|
err := fmt.Errorf("Error uploading file: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
|||||||
@@ -12,8 +12,20 @@ import (
|
|||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testLocalOutputDir(t *testing.T) *LocalOutputDir {
|
||||||
|
td, err := ioutil.TempDir("", "packer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
os.RemoveAll(td)
|
||||||
|
|
||||||
|
result := new(LocalOutputDir)
|
||||||
|
result.SetOutputDir(td)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func testStepShutdownState(t *testing.T) multistep.StateBag {
|
func testStepShutdownState(t *testing.T) multistep.StateBag {
|
||||||
dir := testOutputDir(t)
|
dir := testLocalOutputDir(t)
|
||||||
if err := dir.MkdirAll(); err != nil {
|
if err := dir.MkdirAll(); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (c *StepUploadVMX) Run(ctx context.Context, state multistep.StateBag) multi
|
|||||||
remoteDriver, ok := driver.(RemoteDriver)
|
remoteDriver, ok := driver.(RemoteDriver)
|
||||||
if ok {
|
if ok {
|
||||||
remoteVmxPath := filepath.ToSlash(filepath.Join(fmt.Sprintf("%s", remoteDriver), filepath.Base(vmxPath)))
|
remoteVmxPath := filepath.ToSlash(filepath.Join(fmt.Sprintf("%s", remoteDriver), filepath.Base(vmxPath)))
|
||||||
if err := remoteDriver.upload(remoteVmxPath, vmxPath); err != nil {
|
if err := remoteDriver.upload(remoteVmxPath, vmxPath, nil); err != nil {
|
||||||
state.Put("error", fmt.Errorf("Error writing VMX: %s", err))
|
state.Put("error", fmt.Errorf("Error writing VMX: %s", err))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,29 +36,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the output dir implementation
|
|
||||||
var dir vmwcommon.OutputDir
|
|
||||||
switch d := driver.(type) {
|
|
||||||
case vmwcommon.OutputDir:
|
|
||||||
dir = d
|
|
||||||
default:
|
|
||||||
dir = new(vmwcommon.LocalOutputDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OutputDir will track remote esxi output; exportOutputPath preserves
|
|
||||||
// the path to the output on the machine running Packer.
|
|
||||||
exportOutputPath := b.config.OutputDir
|
|
||||||
|
|
||||||
if b.config.RemoteType != "" {
|
|
||||||
b.config.OutputDir = b.config.VMName
|
|
||||||
}
|
|
||||||
dir.SetOutputDir(b.config.OutputDir)
|
|
||||||
|
|
||||||
// Setup the state bag
|
// Setup the state bag
|
||||||
state := new(multistep.BasicStateBag)
|
state := new(multistep.BasicStateBag)
|
||||||
state.Put("config", &b.config)
|
state.Put("config", &b.config)
|
||||||
state.Put("debug", b.config.PackerDebug)
|
state.Put("debug", b.config.PackerDebug)
|
||||||
state.Put("dir", dir)
|
|
||||||
state.Put("driver", driver)
|
state.Put("driver", driver)
|
||||||
state.Put("hook", hook)
|
state.Put("hook", hook)
|
||||||
state.Put("ui", ui)
|
state.Put("ui", ui)
|
||||||
@@ -80,7 +61,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
Url: b.config.ISOUrls,
|
Url: b.config.ISOUrls,
|
||||||
},
|
},
|
||||||
&vmwcommon.StepOutputDir{
|
&vmwcommon.StepOutputDir{
|
||||||
Force: b.config.PackerForce,
|
Force: b.config.PackerForce,
|
||||||
|
OutputConfig: &b.config.OutputConfig,
|
||||||
|
RemoteType: b.config.RemoteType,
|
||||||
|
VMName: b.config.VMName,
|
||||||
},
|
},
|
||||||
&common.StepCreateFloppy{
|
&common.StepCreateFloppy{
|
||||||
Files: b.config.FloppyConfig.FloppyFiles,
|
Files: b.config.FloppyConfig.FloppyFiles,
|
||||||
@@ -179,7 +163,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
SkipExport: b.config.SkipExport,
|
SkipExport: b.config.SkipExport,
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
OVFToolOptions: b.config.OVFToolOptions,
|
OVFToolOptions: b.config.OVFToolOptions,
|
||||||
OutputDir: exportOutputPath,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +185,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile the artifact list
|
// Compile the artifact list
|
||||||
|
exportOutputPath := state.Get("export_output_path").(string) // set in StepOutputDir
|
||||||
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
|
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
|
||||||
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
|
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ type FlatConfig struct {
|
|||||||
CoreCount *int `mapstructure:"cores" required:"false" cty:"cores" hcl:"cores"`
|
CoreCount *int `mapstructure:"cores" required:"false" cty:"cores" hcl:"cores"`
|
||||||
Network *string `mapstructure:"network" required:"false" cty:"network" hcl:"network"`
|
Network *string `mapstructure:"network" required:"false" cty:"network" hcl:"network"`
|
||||||
NetworkAdapterType *string `mapstructure:"network_adapter_type" required:"false" cty:"network_adapter_type" hcl:"network_adapter_type"`
|
NetworkAdapterType *string `mapstructure:"network_adapter_type" required:"false" cty:"network_adapter_type" hcl:"network_adapter_type"`
|
||||||
|
NetworkName *string `mapstructure:"network_name" required:"false" cty:"network_name" hcl:"network_name"`
|
||||||
Sound *bool `mapstructure:"sound" required:"false" cty:"sound" hcl:"sound"`
|
Sound *bool `mapstructure:"sound" required:"false" cty:"sound" hcl:"sound"`
|
||||||
USB *bool `mapstructure:"usb" required:"false" cty:"usb" hcl:"usb"`
|
USB *bool `mapstructure:"usb" required:"false" cty:"usb" hcl:"usb"`
|
||||||
Serial *string `mapstructure:"serial" required:"false" cty:"serial" hcl:"serial"`
|
Serial *string `mapstructure:"serial" required:"false" cty:"serial" hcl:"serial"`
|
||||||
Parallel *string `mapstructure:"parallel" required:"false" cty:"parallel" hcl:"parallel"`
|
Parallel *string `mapstructure:"parallel" required:"false" cty:"parallel" hcl:"parallel"`
|
||||||
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
||||||
|
RemoteOutputDir *string `mapstructure:"remote_output_directory" required:"false" cty:"remote_output_directory" hcl:"remote_output_directory"`
|
||||||
Headless *bool `mapstructure:"headless" required:"false" cty:"headless" hcl:"headless"`
|
Headless *bool `mapstructure:"headless" required:"false" cty:"headless" hcl:"headless"`
|
||||||
VNCBindAddress *string `mapstructure:"vnc_bind_address" required:"false" cty:"vnc_bind_address" hcl:"vnc_bind_address"`
|
VNCBindAddress *string `mapstructure:"vnc_bind_address" required:"false" cty:"vnc_bind_address" hcl:"vnc_bind_address"`
|
||||||
VNCPortMin *int `mapstructure:"vnc_port_min" required:"false" cty:"vnc_port_min" hcl:"vnc_port_min"`
|
VNCPortMin *int `mapstructure:"vnc_port_min" required:"false" cty:"vnc_port_min" hcl:"vnc_port_min"`
|
||||||
@@ -187,11 +189,13 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false},
|
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false},
|
||||||
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
|
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
|
||||||
"network_adapter_type": &hcldec.AttrSpec{Name: "network_adapter_type", Type: cty.String, Required: false},
|
"network_adapter_type": &hcldec.AttrSpec{Name: "network_adapter_type", Type: cty.String, Required: false},
|
||||||
|
"network_name": &hcldec.AttrSpec{Name: "network_name", Type: cty.String, Required: false},
|
||||||
"sound": &hcldec.AttrSpec{Name: "sound", Type: cty.Bool, Required: false},
|
"sound": &hcldec.AttrSpec{Name: "sound", Type: cty.Bool, Required: false},
|
||||||
"usb": &hcldec.AttrSpec{Name: "usb", Type: cty.Bool, Required: false},
|
"usb": &hcldec.AttrSpec{Name: "usb", Type: cty.Bool, Required: false},
|
||||||
"serial": &hcldec.AttrSpec{Name: "serial", Type: cty.String, Required: false},
|
"serial": &hcldec.AttrSpec{Name: "serial", Type: cty.String, Required: false},
|
||||||
"parallel": &hcldec.AttrSpec{Name: "parallel", Type: cty.String, Required: false},
|
"parallel": &hcldec.AttrSpec{Name: "parallel", Type: cty.String, Required: false},
|
||||||
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
||||||
|
"remote_output_directory": &hcldec.AttrSpec{Name: "remote_output_directory", Type: cty.String, Required: false},
|
||||||
"headless": &hcldec.AttrSpec{Name: "headless", Type: cty.Bool, Required: false},
|
"headless": &hcldec.AttrSpec{Name: "headless", Type: cty.Bool, Required: false},
|
||||||
"vnc_bind_address": &hcldec.AttrSpec{Name: "vnc_bind_address", Type: cty.String, Required: false},
|
"vnc_bind_address": &hcldec.AttrSpec{Name: "vnc_bind_address", Type: cty.String, Required: false},
|
||||||
"vnc_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
|
"vnc_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type vmxTemplateData struct {
|
|||||||
Network_Type string
|
Network_Type string
|
||||||
Network_Device string
|
Network_Device string
|
||||||
Network_Adapter string
|
Network_Adapter string
|
||||||
|
Network_Name string
|
||||||
|
|
||||||
Sound_Present string
|
Sound_Present string
|
||||||
Usb_Present string
|
Usb_Present string
|
||||||
@@ -252,6 +253,9 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
|
|||||||
|
|
||||||
/// Check the network type that the user specified
|
/// Check the network type that the user specified
|
||||||
network := config.HWConfig.Network
|
network := config.HWConfig.Network
|
||||||
|
if config.HWConfig.NetworkName != "" {
|
||||||
|
templateData.Network_Name = config.HWConfig.NetworkName
|
||||||
|
}
|
||||||
driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver()
|
driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver()
|
||||||
|
|
||||||
// check to see if the driver implements a network mapper for mapping
|
// check to see if the driver implements a network mapper for mapping
|
||||||
@@ -542,6 +546,7 @@ ethernet0.pciSlotNumber = "33"
|
|||||||
ethernet0.present = "TRUE"
|
ethernet0.present = "TRUE"
|
||||||
ethernet0.virtualDev = "{{ .Network_Adapter }}"
|
ethernet0.virtualDev = "{{ .Network_Adapter }}"
|
||||||
ethernet0.wakeOnPcktRcv = "FALSE"
|
ethernet0.wakeOnPcktRcv = "FALSE"
|
||||||
|
{{if .Network_Name }}ethernet0.networkName = "{{ .Network_Name }}"{{end}}
|
||||||
|
|
||||||
// Hard disks
|
// Hard disks
|
||||||
scsi0.present = "{{ .SCSI_Present }}"
|
scsi0.present = "{{ .SCSI_Present }}"
|
||||||
|
|||||||
@@ -41,28 +41,9 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the output dir implementation
|
|
||||||
var dir vmwcommon.OutputDir
|
|
||||||
switch d := driver.(type) {
|
|
||||||
case vmwcommon.OutputDir:
|
|
||||||
dir = d
|
|
||||||
default:
|
|
||||||
dir = new(vmwcommon.LocalOutputDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OutputDir will track remote esxi output; exportOutputPath preserves
|
|
||||||
// the path to the output on the machine running Packer.
|
|
||||||
exportOutputPath := b.config.OutputDir
|
|
||||||
|
|
||||||
if b.config.RemoteType != "" {
|
|
||||||
b.config.OutputDir = b.config.VMName
|
|
||||||
}
|
|
||||||
dir.SetOutputDir(b.config.OutputDir)
|
|
||||||
|
|
||||||
// Set up the state.
|
// Set up the state.
|
||||||
state := new(multistep.BasicStateBag)
|
state := new(multistep.BasicStateBag)
|
||||||
state.Put("debug", b.config.PackerDebug)
|
state.Put("debug", b.config.PackerDebug)
|
||||||
state.Put("dir", dir)
|
|
||||||
state.Put("driver", driver)
|
state.Put("driver", driver)
|
||||||
state.Put("hook", hook)
|
state.Put("hook", hook)
|
||||||
state.Put("ui", ui)
|
state.Put("ui", ui)
|
||||||
@@ -77,7 +58,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
ToolsUploadFlavor: b.config.ToolsUploadFlavor,
|
ToolsUploadFlavor: b.config.ToolsUploadFlavor,
|
||||||
},
|
},
|
||||||
&vmwcommon.StepOutputDir{
|
&vmwcommon.StepOutputDir{
|
||||||
Force: b.config.PackerForce,
|
Force: b.config.PackerForce,
|
||||||
|
OutputConfig: &b.config.OutputConfig,
|
||||||
|
RemoteType: b.config.RemoteType,
|
||||||
|
VMName: b.config.VMName,
|
||||||
},
|
},
|
||||||
&common.StepCreateFloppy{
|
&common.StepCreateFloppy{
|
||||||
Files: b.config.FloppyConfig.FloppyFiles,
|
Files: b.config.FloppyConfig.FloppyFiles,
|
||||||
@@ -91,8 +75,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
Checksum: "none",
|
Checksum: "none",
|
||||||
},
|
},
|
||||||
&StepCloneVMX{
|
&StepCloneVMX{
|
||||||
OutputDir: b.config.OutputDir,
|
|
||||||
Path: b.config.SourcePath,
|
Path: b.config.SourcePath,
|
||||||
|
OutputDir: &b.config.OutputDir,
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
Linked: b.config.Linked,
|
Linked: b.config.Linked,
|
||||||
},
|
},
|
||||||
@@ -177,7 +161,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
SkipExport: b.config.SkipExport,
|
SkipExport: b.config.SkipExport,
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
OVFToolOptions: b.config.OVFToolOptions,
|
OVFToolOptions: b.config.OVFToolOptions,
|
||||||
OutputDir: exportOutputPath,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +184,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
|
|
||||||
// Artifact
|
// Artifact
|
||||||
log.Printf("Generating artifact...")
|
log.Printf("Generating artifact...")
|
||||||
|
exportOutputPath := state.Get("export_output_path").(string) // set in StepOutputDir
|
||||||
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
|
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
|
||||||
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
|
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type FlatConfig struct {
|
|||||||
RemotePrivateKey *string `mapstructure:"remote_private_key_file" required:"false" cty:"remote_private_key_file" hcl:"remote_private_key_file"`
|
RemotePrivateKey *string `mapstructure:"remote_private_key_file" required:"false" cty:"remote_private_key_file" hcl:"remote_private_key_file"`
|
||||||
SkipValidateCredentials *bool `mapstructure:"skip_validate_credentials" required:"false" cty:"skip_validate_credentials" hcl:"skip_validate_credentials"`
|
SkipValidateCredentials *bool `mapstructure:"skip_validate_credentials" required:"false" cty:"skip_validate_credentials" hcl:"skip_validate_credentials"`
|
||||||
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
||||||
|
RemoteOutputDir *string `mapstructure:"remote_output_directory" required:"false" cty:"remote_output_directory" hcl:"remote_output_directory"`
|
||||||
Headless *bool `mapstructure:"headless" required:"false" cty:"headless" hcl:"headless"`
|
Headless *bool `mapstructure:"headless" required:"false" cty:"headless" hcl:"headless"`
|
||||||
VNCBindAddress *string `mapstructure:"vnc_bind_address" required:"false" cty:"vnc_bind_address" hcl:"vnc_bind_address"`
|
VNCBindAddress *string `mapstructure:"vnc_bind_address" required:"false" cty:"vnc_bind_address" hcl:"vnc_bind_address"`
|
||||||
VNCPortMin *int `mapstructure:"vnc_port_min" required:"false" cty:"vnc_port_min" hcl:"vnc_port_min"`
|
VNCPortMin *int `mapstructure:"vnc_port_min" required:"false" cty:"vnc_port_min" hcl:"vnc_port_min"`
|
||||||
@@ -156,6 +157,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"remote_private_key_file": &hcldec.AttrSpec{Name: "remote_private_key_file", Type: cty.String, Required: false},
|
"remote_private_key_file": &hcldec.AttrSpec{Name: "remote_private_key_file", Type: cty.String, Required: false},
|
||||||
"skip_validate_credentials": &hcldec.AttrSpec{Name: "skip_validate_credentials", Type: cty.Bool, Required: false},
|
"skip_validate_credentials": &hcldec.AttrSpec{Name: "skip_validate_credentials", Type: cty.Bool, Required: false},
|
||||||
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
||||||
|
"remote_output_directory": &hcldec.AttrSpec{Name: "remote_output_directory", Type: cty.String, Required: false},
|
||||||
"headless": &hcldec.AttrSpec{Name: "headless", Type: cty.Bool, Required: false},
|
"headless": &hcldec.AttrSpec{Name: "headless", Type: cty.Bool, Required: false},
|
||||||
"vnc_bind_address": &hcldec.AttrSpec{Name: "vnc_bind_address", Type: cty.String, Required: false},
|
"vnc_bind_address": &hcldec.AttrSpec{Name: "vnc_bind_address", Type: cty.String, Required: false},
|
||||||
"vnc_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
|
"vnc_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// StepCloneVMX takes a VMX file and clones the VM into the output directory.
|
// StepCloneVMX takes a VMX file and clones the VM into the output directory.
|
||||||
type StepCloneVMX struct {
|
type StepCloneVMX struct {
|
||||||
OutputDir string
|
OutputDir *string
|
||||||
Path string
|
Path string
|
||||||
VMName string
|
VMName string
|
||||||
Linked bool
|
Linked bool
|
||||||
@@ -33,7 +33,7 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
|
|||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
// Set the path we want for the new .vmx file and clone
|
// Set the path we want for the new .vmx file and clone
|
||||||
vmxPath := filepath.Join(s.OutputDir, s.VMName+".vmx")
|
vmxPath := filepath.Join(*s.OutputDir, s.VMName+".vmx")
|
||||||
ui.Say("Cloning source VM...")
|
ui.Say("Cloning source VM...")
|
||||||
log.Printf("Cloning from: %s", s.Path)
|
log.Printf("Cloning from: %s", s.Path)
|
||||||
log.Printf("Cloning to: %s", vmxPath)
|
log.Printf("Cloning to: %s", vmxPath)
|
||||||
@@ -96,7 +96,7 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
|
|||||||
var diskFullPaths []string
|
var diskFullPaths []string
|
||||||
for _, diskFilename := range diskFilenames {
|
for _, diskFilename := range diskFilenames {
|
||||||
log.Printf("Found attached disk with filename: %s", diskFilename)
|
log.Printf("Found attached disk with filename: %s", diskFilename)
|
||||||
diskFullPaths = append(diskFullPaths, filepath.Join(s.OutputDir, diskFilename))
|
diskFullPaths = append(diskFullPaths, filepath.Join(*s.OutputDir, diskFilename))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(diskFullPaths) == 0 {
|
if len(diskFullPaths) == 0 {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func TestStepCloneVMX(t *testing.T) {
|
|||||||
|
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepCloneVMX)
|
step := new(StepCloneVMX)
|
||||||
step.OutputDir = td
|
step.OutputDir = &td
|
||||||
step.Path = sourcePath
|
step.Path = sourcePath
|
||||||
step.VMName = "foo"
|
step.VMName = "foo"
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if b.config.CustomizeConfig != nil {
|
||||||
|
steps = append(steps, &StepCustomize{
|
||||||
|
Config: b.config.CustomizeConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if b.config.Comm.Type != "none" {
|
if b.config.Comm.Type != "none" {
|
||||||
steps = append(steps,
|
steps = append(steps,
|
||||||
&common.StepHTTPIPDiscover{
|
&common.StepHTTPIPDiscover{
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ type Config struct {
|
|||||||
// Configuration for exporting VM to an ovf file.
|
// Configuration for exporting VM to an ovf file.
|
||||||
// The VM will not be exported if no [Export Configuration](#export-configuration) is specified.
|
// The VM will not be exported if no [Export Configuration](#export-configuration) is specified.
|
||||||
Export *common.ExportConfig `mapstructure:"export"`
|
Export *common.ExportConfig `mapstructure:"export"`
|
||||||
// Configuration for importing the VM template to a Content Library.
|
// Configuration for importing a VM template or OVF template to a Content Library.
|
||||||
// The VM template will not be imported if no [Content Library Import Configuration](#content-library-import-configuration) is specified.
|
// The template will not be imported if no [Content Library Import Configuration](#content-library-import-configuration) is specified.
|
||||||
// The import doesn't work if [convert_to_template](#convert_to_template) is set to true.
|
// The import doesn't work if [convert_to_template](#convert_to_template) is set to true.
|
||||||
ContentLibraryDestinationConfig *common.ContentLibraryDestinationConfig `mapstructure:"content_library_destination"`
|
ContentLibraryDestinationConfig *common.ContentLibraryDestinationConfig `mapstructure:"content_library_destination"`
|
||||||
|
// Customize the cloned VM to configure host, network, or licensing settings. See the [customization options](#customization).
|
||||||
|
CustomizeConfig *CustomizeConfig `mapstructure:"customize"`
|
||||||
|
|
||||||
ctx interpolate.Context
|
ctx interpolate.Context
|
||||||
}
|
}
|
||||||
@@ -75,6 +77,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||||||
if c.ContentLibraryDestinationConfig != nil {
|
if c.ContentLibraryDestinationConfig != nil {
|
||||||
errs = packer.MultiErrorAppend(errs, c.ContentLibraryDestinationConfig.Prepare(&c.LocationConfig)...)
|
errs = packer.MultiErrorAppend(errs, c.ContentLibraryDestinationConfig.Prepare(&c.LocationConfig)...)
|
||||||
}
|
}
|
||||||
|
if c.CustomizeConfig != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, c.CustomizeConfig.Prepare()...)
|
||||||
|
}
|
||||||
|
|
||||||
if len(errs.Errors) > 0 {
|
if len(errs.Errors) > 0 {
|
||||||
return nil, errs
|
return nil, errs
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ type FlatConfig struct {
|
|||||||
ConvertToTemplate *bool `mapstructure:"convert_to_template" cty:"convert_to_template" hcl:"convert_to_template"`
|
ConvertToTemplate *bool `mapstructure:"convert_to_template" cty:"convert_to_template" hcl:"convert_to_template"`
|
||||||
Export *common.FlatExportConfig `mapstructure:"export" cty:"export" hcl:"export"`
|
Export *common.FlatExportConfig `mapstructure:"export" cty:"export" hcl:"export"`
|
||||||
ContentLibraryDestinationConfig *common.FlatContentLibraryDestinationConfig `mapstructure:"content_library_destination" cty:"content_library_destination" hcl:"content_library_destination"`
|
ContentLibraryDestinationConfig *common.FlatContentLibraryDestinationConfig `mapstructure:"content_library_destination" cty:"content_library_destination" hcl:"content_library_destination"`
|
||||||
|
CustomizeConfig *FlatCustomizeConfig `mapstructure:"customize" cty:"customize" hcl:"customize"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlatMapstructure returns a new FlatConfig.
|
// FlatMapstructure returns a new FlatConfig.
|
||||||
@@ -240,6 +241,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"convert_to_template": &hcldec.AttrSpec{Name: "convert_to_template", Type: cty.Bool, Required: false},
|
"convert_to_template": &hcldec.AttrSpec{Name: "convert_to_template", Type: cty.Bool, Required: false},
|
||||||
"export": &hcldec.BlockSpec{TypeName: "export", Nested: hcldec.ObjectSpec((*common.FlatExportConfig)(nil).HCL2Spec())},
|
"export": &hcldec.BlockSpec{TypeName: "export", Nested: hcldec.ObjectSpec((*common.FlatExportConfig)(nil).HCL2Spec())},
|
||||||
"content_library_destination": &hcldec.BlockSpec{TypeName: "content_library_destination", Nested: hcldec.ObjectSpec((*common.FlatContentLibraryDestinationConfig)(nil).HCL2Spec())},
|
"content_library_destination": &hcldec.BlockSpec{TypeName: "content_library_destination", Nested: hcldec.ObjectSpec((*common.FlatContentLibraryDestinationConfig)(nil).HCL2Spec())},
|
||||||
|
"customize": &hcldec.BlockSpec{TypeName: "customize", Nested: hcldec.ObjectSpec((*FlatCustomizeConfig)(nil).HCL2Spec())},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ type CloneConfig struct {
|
|||||||
DiskSize int64 `mapstructure:"disk_size"`
|
DiskSize int64 `mapstructure:"disk_size"`
|
||||||
// Create VM as a linked clone from latest snapshot. Defaults to `false`.
|
// Create VM as a linked clone from latest snapshot. Defaults to `false`.
|
||||||
LinkedClone bool `mapstructure:"linked_clone"`
|
LinkedClone bool `mapstructure:"linked_clone"`
|
||||||
// Set the network in which the VM will be connected to. If no network is specified, `host`
|
// Set the network in which the VM will be connected to. If no network is
|
||||||
// must be specified to allow Packer to look for the available network.
|
// specified, `host` must be specified to allow Packer to look for the
|
||||||
|
// available network. If the network is inside a network folder in vCenter,
|
||||||
|
// you need to provide the full path to the network.
|
||||||
Network string `mapstructure:"network"`
|
Network string `mapstructure:"network"`
|
||||||
// VM notes.
|
// VM notes.
|
||||||
Notes string `mapstructure:"notes"`
|
Notes string `mapstructure:"notes"`
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//go:generate struct-markdown
|
||||||
|
//go:generate mapstructure-to-hcl2 -type CustomizeConfig,LinuxOptions,NetworkInterfaces,NetworkInterface,GlobalDnsSettings,GlobalRoutingSettings
|
||||||
|
package clone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/builder/vsphere/driver"
|
||||||
|
"github.com/hashicorp/packer/helper/config"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/vmware/govmomi/vim25/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A cloned virtual machine can be [customized](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-58E346FF-83AE-42B8-BE58-253641D257BC.html)
|
||||||
|
// to configure host, network, or licensing settings.
|
||||||
|
//
|
||||||
|
// To perform virtual machine customization as a part of the clone process, specify the customize block with the
|
||||||
|
// respective customization options. Windows guests are customized using Sysprep, which will result in the machine SID being reset.
|
||||||
|
// Before using customization, check that your source VM meets the [requirements](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-E63B6FAA-8D35-428D-B40C-744769845906.html)
|
||||||
|
// for guest OS customization on vSphere.
|
||||||
|
// See the [customization example](#customization-example) for a usage synopsis.
|
||||||
|
//
|
||||||
|
// The settings for customize are as follows:
|
||||||
|
type CustomizeConfig struct {
|
||||||
|
// Settings to Linux guest OS customization. See [Linux customization settings](#linux-customization-settings).
|
||||||
|
LinuxOptions *LinuxOptions `mapstructure:"linux_options"`
|
||||||
|
// Supply your own sysprep.xml file to allow full control of the customization process out-of-band of vSphere.
|
||||||
|
WindowsSysPrepFile string `mapstructure:"windows_sysprep_file"`
|
||||||
|
// Configure network interfaces on a per-interface basis that should matched up to the network adapters present in the VM.
|
||||||
|
// To use DHCP, declare an empty network_interface for each adapter being configured. This field is required.
|
||||||
|
// See [Network interface settings](#network-interface-settings).
|
||||||
|
NetworkInterfaces NetworkInterfaces `mapstructure:"network_interface"`
|
||||||
|
GlobalRoutingSettings `mapstructure:",squash"`
|
||||||
|
GlobalDnsSettings `mapstructure:",squash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinuxOptions struct {
|
||||||
|
// The domain name for this machine. This, along with [host_name](#host_name), make up the FQDN of this virtual machine.
|
||||||
|
Domain string `mapstructure:"domain"`
|
||||||
|
// The host name for this machine. This, along with [domain](#domain), make up the FQDN of this virtual machine.
|
||||||
|
Hostname string `mapstructure:"host_name"`
|
||||||
|
// Tells the operating system that the hardware clock is set to UTC. Default: true.
|
||||||
|
HWClockUTC config.Trilean `mapstructure:"hw_clock_utc"`
|
||||||
|
// Sets the time zone. The default is UTC.
|
||||||
|
Timezone string `mapstructure:"time_zone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkInterface struct {
|
||||||
|
// Network interface-specific DNS server settings for Windows operating systems.
|
||||||
|
// Ignored on Linux and possibly other operating systems - for those systems, please see the [global DNS settings](#global-dns-settings) section.
|
||||||
|
DnsServerList []string `mapstructure:"dns_server_list"`
|
||||||
|
// Network interface-specific DNS search domain for Windows operating systems.
|
||||||
|
// Ignored on Linux and possibly other operating systems - for those systems, please see the [global DNS settings](#global-dns-settings) section.
|
||||||
|
DnsDomain string `mapstructure:"dns_domain"`
|
||||||
|
// The IPv4 address assigned to this network adapter. If left blank or not included, DHCP is used.
|
||||||
|
Ipv4Address string `mapstructure:"ipv4_address"`
|
||||||
|
// The IPv4 subnet mask, in bits (example: 24 for 255.255.255.0).
|
||||||
|
Ipv4NetMask int `mapstructure:"ipv4_netmask"`
|
||||||
|
// The IPv6 address assigned to this network adapter. If left blank or not included, auto-configuration is used.
|
||||||
|
Ipv6Address string `mapstructure:"ipv6_address"`
|
||||||
|
// The IPv6 subnet mask, in bits (example: 32).
|
||||||
|
Ipv6NetMask int `mapstructure:"ipv6_netmask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkInterfaces []NetworkInterface
|
||||||
|
|
||||||
|
// The settings here must match the IP/mask of at least one network_interface supplied to customization.
|
||||||
|
type GlobalRoutingSettings struct {
|
||||||
|
// The IPv4 default gateway when using network_interface customization on the virtual machine.
|
||||||
|
Ipv4Gateway string `mapstructure:"ipv4_gateway"`
|
||||||
|
// The IPv6 default gateway when using network_interface customization on the virtual machine.
|
||||||
|
Ipv6Gateway string `mapstructure:"ipv6_gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following settings configure DNS globally, generally for Linux systems. For Windows systems,
|
||||||
|
// this is done per-interface, see [network interface](#network_interface) settings.
|
||||||
|
type GlobalDnsSettings struct {
|
||||||
|
// The list of DNS servers to configure on a virtual machine.
|
||||||
|
DnsServerList []string `mapstructure:"dns_server_list"`
|
||||||
|
// A list of DNS search domains to add to the DNS configuration on the virtual machine.
|
||||||
|
DnsSuffixList []string `mapstructure:"dns_suffix_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StepCustomize struct {
|
||||||
|
Config *CustomizeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomizeConfig) Prepare() []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if c.LinuxOptions == nil && c.WindowsSysPrepFile == "" {
|
||||||
|
errs = append(errs, fmt.Errorf("customize is empty"))
|
||||||
|
}
|
||||||
|
if c.LinuxOptions != nil && c.WindowsSysPrepFile != "" {
|
||||||
|
errs = append(errs, fmt.Errorf("`linux_options` and `windows_sysprep_text` both set - one must not be included if the other is specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LinuxOptions != nil {
|
||||||
|
if c.LinuxOptions.Hostname == "" {
|
||||||
|
errs = append(errs, fmt.Errorf("linux options `host_name` is empty"))
|
||||||
|
}
|
||||||
|
if c.LinuxOptions.Domain == "" {
|
||||||
|
errs = append(errs, fmt.Errorf("linux options `domain` is empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LinuxOptions.HWClockUTC == config.TriUnset {
|
||||||
|
c.LinuxOptions.HWClockUTC = config.TriTrue
|
||||||
|
}
|
||||||
|
if c.LinuxOptions.Timezone == "" {
|
||||||
|
c.LinuxOptions.Timezone = "UTC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.NetworkInterfaces) == 0 {
|
||||||
|
errs = append(errs, fmt.Errorf("one or more `network_interface` must be provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
identity, err := s.identitySettings()
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
nicSettingsMap := s.nicSettingsMap()
|
||||||
|
globalIpSettings := s.globalIpSettings()
|
||||||
|
|
||||||
|
spec := types.CustomizationSpec{
|
||||||
|
Identity: identity,
|
||||||
|
NicSettingMap: nicSettingsMap,
|
||||||
|
GlobalIPSettings: globalIpSettings,
|
||||||
|
}
|
||||||
|
ui.Say("Customizing VM...")
|
||||||
|
err = vm.Customize(spec)
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) identitySettings() (types.BaseCustomizationIdentitySettings, error) {
|
||||||
|
if s.Config.LinuxOptions != nil {
|
||||||
|
return &types.CustomizationLinuxPrep{
|
||||||
|
HostName: &types.CustomizationFixedName{
|
||||||
|
Name: s.Config.LinuxOptions.Hostname,
|
||||||
|
},
|
||||||
|
Domain: s.Config.LinuxOptions.Domain,
|
||||||
|
TimeZone: s.Config.LinuxOptions.Timezone,
|
||||||
|
HwClockUTC: s.Config.LinuxOptions.HWClockUTC.ToBoolPointer(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Config.WindowsSysPrepFile != "" {
|
||||||
|
sysPrep, err := ioutil.ReadFile(s.Config.WindowsSysPrepFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error on reading %s: %s", s.Config.WindowsSysPrepFile, err)
|
||||||
|
}
|
||||||
|
return &types.CustomizationSysprepText{
|
||||||
|
Value: string(sysPrep),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no customization identity found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) nicSettingsMap() []types.CustomizationAdapterMapping {
|
||||||
|
result := make([]types.CustomizationAdapterMapping, len(s.Config.NetworkInterfaces))
|
||||||
|
var ipv4gwFound, ipv6gwFound bool
|
||||||
|
for i := range s.Config.NetworkInterfaces {
|
||||||
|
var adapter types.CustomizationIPSettings
|
||||||
|
adapter, ipv4gwFound, ipv6gwFound = s.ipSettings(i, !ipv4gwFound, !ipv6gwFound)
|
||||||
|
obj := types.CustomizationAdapterMapping{
|
||||||
|
Adapter: adapter,
|
||||||
|
}
|
||||||
|
result[i] = obj
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) ipSettings(n int, ipv4gwAdd bool, ipv6gwAdd bool) (types.CustomizationIPSettings, bool, bool) {
|
||||||
|
var v4gwFound, v6gwFound bool
|
||||||
|
var obj types.CustomizationIPSettings
|
||||||
|
|
||||||
|
ipv4Address := s.Config.NetworkInterfaces[n].Ipv4Address
|
||||||
|
if ipv4Address != "" {
|
||||||
|
ipv4mask := s.Config.NetworkInterfaces[n].Ipv4NetMask
|
||||||
|
ipv4Gateway := s.Config.Ipv4Gateway
|
||||||
|
obj.Ip = &types.CustomizationFixedIp{
|
||||||
|
IpAddress: ipv4Address,
|
||||||
|
}
|
||||||
|
obj.SubnetMask = v4CIDRMaskToDotted(ipv4mask)
|
||||||
|
// Check for the gateway
|
||||||
|
if ipv4gwAdd && ipv4Gateway != "" && matchGateway(ipv4Address, ipv4mask, ipv4Gateway) {
|
||||||
|
obj.Gateway = []string{ipv4Gateway}
|
||||||
|
v4gwFound = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj.Ip = &types.CustomizationDhcpIpGenerator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.DnsServerList = s.Config.NetworkInterfaces[n].DnsServerList
|
||||||
|
obj.DnsDomain = s.Config.NetworkInterfaces[n].DnsDomain
|
||||||
|
obj.IpV6Spec, v6gwFound = s.IPSettingsIPV6Address(n, ipv6gwAdd)
|
||||||
|
|
||||||
|
return obj, v4gwFound, v6gwFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func v4CIDRMaskToDotted(mask int) string {
|
||||||
|
m := net.CIDRMask(mask, 32)
|
||||||
|
a := int(m[0])
|
||||||
|
b := int(m[1])
|
||||||
|
c := int(m[2])
|
||||||
|
d := int(m[3])
|
||||||
|
return fmt.Sprintf("%d.%d.%d.%d", a, b, c, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) IPSettingsIPV6Address(n int, gwAdd bool) (*types.CustomizationIPSettingsIpV6AddressSpec, bool) {
|
||||||
|
addr := s.Config.NetworkInterfaces[n].Ipv6Address
|
||||||
|
var gwFound bool
|
||||||
|
if addr == "" {
|
||||||
|
return nil, gwFound
|
||||||
|
}
|
||||||
|
mask := s.Config.NetworkInterfaces[n].Ipv6NetMask
|
||||||
|
gw := s.Config.Ipv6Gateway
|
||||||
|
obj := &types.CustomizationIPSettingsIpV6AddressSpec{
|
||||||
|
Ip: []types.BaseCustomizationIpV6Generator{
|
||||||
|
&types.CustomizationFixedIpV6{
|
||||||
|
IpAddress: addr,
|
||||||
|
SubnetMask: int32(mask),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if gwAdd && gw != "" && matchGateway(addr, mask, gw) {
|
||||||
|
obj.Gateway = []string{gw}
|
||||||
|
gwFound = true
|
||||||
|
}
|
||||||
|
return obj, gwFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchGateway take an IP, mask, and gateway, and checks to see if the gateway
|
||||||
|
// is reachable from the IP address.
|
||||||
|
func matchGateway(a string, m int, g string) bool {
|
||||||
|
ip := net.ParseIP(a)
|
||||||
|
gw := net.ParseIP(g)
|
||||||
|
var mask net.IPMask
|
||||||
|
if ip.To4() != nil {
|
||||||
|
mask = net.CIDRMask(m, 32)
|
||||||
|
} else {
|
||||||
|
mask = net.CIDRMask(m, 128)
|
||||||
|
}
|
||||||
|
if ip.Mask(mask).Equal(gw.Mask(mask)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) globalIpSettings() types.CustomizationGlobalIPSettings {
|
||||||
|
return types.CustomizationGlobalIPSettings{
|
||||||
|
DnsServerList: s.Config.DnsServerList,
|
||||||
|
DnsSuffixList: s.Config.DnsSuffixList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCustomize) Cleanup(_ multistep.StateBag) {}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Code generated by "mapstructure-to-hcl2 -type CustomizeConfig,LinuxOptions,NetworkInterfaces,NetworkInterface,GlobalDnsSettings,GlobalRoutingSettings"; DO NOT EDIT.
|
||||||
|
package clone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FlatCustomizeConfig is an auto-generated flat version of CustomizeConfig.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatCustomizeConfig struct {
|
||||||
|
LinuxOptions *FlatLinuxOptions `mapstructure:"linux_options" cty:"linux_options" hcl:"linux_options"`
|
||||||
|
WindowsSysPrepFile *string `mapstructure:"windows_sysprep_file" cty:"windows_sysprep_file" hcl:"windows_sysprep_file"`
|
||||||
|
NetworkInterfaces []FlatNetworkInterface `mapstructure:"network_interface" cty:"network_interface" hcl:"network_interface"`
|
||||||
|
Ipv4Gateway *string `mapstructure:"ipv4_gateway" cty:"ipv4_gateway" hcl:"ipv4_gateway"`
|
||||||
|
Ipv6Gateway *string `mapstructure:"ipv6_gateway" cty:"ipv6_gateway" hcl:"ipv6_gateway"`
|
||||||
|
DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"`
|
||||||
|
DnsSuffixList []string `mapstructure:"dns_suffix_list" cty:"dns_suffix_list" hcl:"dns_suffix_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatCustomizeConfig.
|
||||||
|
// FlatCustomizeConfig is an auto-generated flat version of CustomizeConfig.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*CustomizeConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatCustomizeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a CustomizeConfig.
|
||||||
|
// This spec is used by HCL to read the fields of CustomizeConfig.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatCustomizeConfig.
|
||||||
|
func (*FlatCustomizeConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"linux_options": &hcldec.BlockSpec{TypeName: "linux_options", Nested: hcldec.ObjectSpec((*FlatLinuxOptions)(nil).HCL2Spec())},
|
||||||
|
"windows_sysprep_file": &hcldec.AttrSpec{Name: "windows_sysprep_file", Type: cty.String, Required: false},
|
||||||
|
"network_interface": &hcldec.BlockListSpec{TypeName: "network_interface", Nested: hcldec.ObjectSpec((*FlatNetworkInterface)(nil).HCL2Spec())},
|
||||||
|
"ipv4_gateway": &hcldec.AttrSpec{Name: "ipv4_gateway", Type: cty.String, Required: false},
|
||||||
|
"ipv6_gateway": &hcldec.AttrSpec{Name: "ipv6_gateway", Type: cty.String, Required: false},
|
||||||
|
"dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false},
|
||||||
|
"dns_suffix_list": &hcldec.AttrSpec{Name: "dns_suffix_list", Type: cty.List(cty.String), Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatGlobalDnsSettings is an auto-generated flat version of GlobalDnsSettings.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatGlobalDnsSettings struct {
|
||||||
|
DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"`
|
||||||
|
DnsSuffixList []string `mapstructure:"dns_suffix_list" cty:"dns_suffix_list" hcl:"dns_suffix_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatGlobalDnsSettings.
|
||||||
|
// FlatGlobalDnsSettings is an auto-generated flat version of GlobalDnsSettings.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*GlobalDnsSettings) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatGlobalDnsSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a GlobalDnsSettings.
|
||||||
|
// This spec is used by HCL to read the fields of GlobalDnsSettings.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatGlobalDnsSettings.
|
||||||
|
func (*FlatGlobalDnsSettings) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false},
|
||||||
|
"dns_suffix_list": &hcldec.AttrSpec{Name: "dns_suffix_list", Type: cty.List(cty.String), Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatGlobalRoutingSettings is an auto-generated flat version of GlobalRoutingSettings.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatGlobalRoutingSettings struct {
|
||||||
|
Ipv4Gateway *string `mapstructure:"ipv4_gateway" cty:"ipv4_gateway" hcl:"ipv4_gateway"`
|
||||||
|
Ipv6Gateway *string `mapstructure:"ipv6_gateway" cty:"ipv6_gateway" hcl:"ipv6_gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatGlobalRoutingSettings.
|
||||||
|
// FlatGlobalRoutingSettings is an auto-generated flat version of GlobalRoutingSettings.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*GlobalRoutingSettings) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatGlobalRoutingSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a GlobalRoutingSettings.
|
||||||
|
// This spec is used by HCL to read the fields of GlobalRoutingSettings.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatGlobalRoutingSettings.
|
||||||
|
func (*FlatGlobalRoutingSettings) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"ipv4_gateway": &hcldec.AttrSpec{Name: "ipv4_gateway", Type: cty.String, Required: false},
|
||||||
|
"ipv6_gateway": &hcldec.AttrSpec{Name: "ipv6_gateway", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatLinuxOptions is an auto-generated flat version of LinuxOptions.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatLinuxOptions struct {
|
||||||
|
Domain *string `mapstructure:"domain" cty:"domain" hcl:"domain"`
|
||||||
|
Hostname *string `mapstructure:"host_name" cty:"host_name" hcl:"host_name"`
|
||||||
|
HWClockUTC *bool `mapstructure:"hw_clock_utc" cty:"hw_clock_utc" hcl:"hw_clock_utc"`
|
||||||
|
Timezone *string `mapstructure:"time_zone" cty:"time_zone" hcl:"time_zone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatLinuxOptions.
|
||||||
|
// FlatLinuxOptions is an auto-generated flat version of LinuxOptions.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*LinuxOptions) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatLinuxOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a LinuxOptions.
|
||||||
|
// This spec is used by HCL to read the fields of LinuxOptions.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatLinuxOptions.
|
||||||
|
func (*FlatLinuxOptions) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"domain": &hcldec.AttrSpec{Name: "domain", Type: cty.String, Required: false},
|
||||||
|
"host_name": &hcldec.AttrSpec{Name: "host_name", Type: cty.String, Required: false},
|
||||||
|
"hw_clock_utc": &hcldec.AttrSpec{Name: "hw_clock_utc", Type: cty.Bool, Required: false},
|
||||||
|
"time_zone": &hcldec.AttrSpec{Name: "time_zone", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatNetworkInterface is an auto-generated flat version of NetworkInterface.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatNetworkInterface struct {
|
||||||
|
DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"`
|
||||||
|
DnsDomain *string `mapstructure:"dns_domain" cty:"dns_domain" hcl:"dns_domain"`
|
||||||
|
Ipv4Address *string `mapstructure:"ipv4_address" cty:"ipv4_address" hcl:"ipv4_address"`
|
||||||
|
Ipv4NetMask *int `mapstructure:"ipv4_netmask" cty:"ipv4_netmask" hcl:"ipv4_netmask"`
|
||||||
|
Ipv6Address *string `mapstructure:"ipv6_address" cty:"ipv6_address" hcl:"ipv6_address"`
|
||||||
|
Ipv6NetMask *int `mapstructure:"ipv6_netmask" cty:"ipv6_netmask" hcl:"ipv6_netmask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatNetworkInterface.
|
||||||
|
// FlatNetworkInterface is an auto-generated flat version of NetworkInterface.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*NetworkInterface) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatNetworkInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a NetworkInterface.
|
||||||
|
// This spec is used by HCL to read the fields of NetworkInterface.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatNetworkInterface.
|
||||||
|
func (*FlatNetworkInterface) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false},
|
||||||
|
"dns_domain": &hcldec.AttrSpec{Name: "dns_domain", Type: cty.String, Required: false},
|
||||||
|
"ipv4_address": &hcldec.AttrSpec{Name: "ipv4_address", Type: cty.String, Required: false},
|
||||||
|
"ipv4_netmask": &hcldec.AttrSpec{Name: "ipv4_netmask", Type: cty.Number, Required: false},
|
||||||
|
"ipv6_address": &hcldec.AttrSpec{Name: "ipv6_address", Type: cty.String, Required: false},
|
||||||
|
"ipv6_netmask": &hcldec.AttrSpec{Name: "ipv6_netmask", Type: cty.Number, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -21,12 +21,22 @@ type OutputConfig struct {
|
|||||||
// created, must be empty prior to running the builder. By default this is
|
// created, must be empty prior to running the builder. By default this is
|
||||||
// "output-BUILDNAME" where "BUILDNAME" is the name of the build.
|
// "output-BUILDNAME" where "BUILDNAME" is the name of the build.
|
||||||
OutputDir string `mapstructure:"output_directory" required:"false"`
|
OutputDir string `mapstructure:"output_directory" required:"false"`
|
||||||
|
// The permissions to apply to the "output_directory", and to any parent
|
||||||
|
// directories that get created for output_directory. By default this is
|
||||||
|
// "0750". You should express the permission as quoted string with a
|
||||||
|
// leading zero such as "0755" in JSON file, because JSON does not support
|
||||||
|
// octal value. In Unix-like OS, the actual permission may differ from
|
||||||
|
// this value because of umask.
|
||||||
|
DirPerm os.FileMode `mapstructure:"directory_permission" required:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error {
|
func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error {
|
||||||
if c.OutputDir == "" {
|
if c.OutputDir == "" {
|
||||||
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
|
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
|
||||||
}
|
}
|
||||||
|
if c.DirPerm == 0 {
|
||||||
|
c.DirPerm = 0750
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
@@ -9,7 +11,8 @@ import (
|
|||||||
// FlatOutputConfig is an auto-generated flat version of OutputConfig.
|
// FlatOutputConfig is an auto-generated flat version of OutputConfig.
|
||||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
type FlatOutputConfig struct {
|
type FlatOutputConfig struct {
|
||||||
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
||||||
|
DirPerm *os.FileMode `mapstructure:"directory_permission" required:"false" cty:"directory_permission" hcl:"directory_permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlatMapstructure returns a new FlatOutputConfig.
|
// FlatMapstructure returns a new FlatOutputConfig.
|
||||||
@@ -24,7 +27,8 @@ func (*OutputConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.
|
|||||||
// The decoded values from this spec will then be applied to a FlatOutputConfig.
|
// The decoded values from this spec will then be applied to a FlatOutputConfig.
|
||||||
func (*FlatOutputConfig) HCL2Spec() map[string]hcldec.Spec {
|
func (*FlatOutputConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
s := map[string]hcldec.Spec{
|
s := map[string]hcldec.Spec{
|
||||||
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
||||||
|
"directory_permission": &hcldec.AttrSpec{Name: "directory_permission", Type: cty.Number, Required: false},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,23 +76,27 @@ func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) mul
|
|||||||
ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port))
|
ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keyAlt, keyCtrl, keyShift bool
|
||||||
sendCodes := func(code key.Code, down bool) error {
|
sendCodes := func(code key.Code, down bool) error {
|
||||||
var keyAlt, keyCtrl, keyShift bool
|
|
||||||
|
|
||||||
switch code {
|
switch code {
|
||||||
case key.CodeLeftAlt:
|
case key.CodeLeftAlt:
|
||||||
keyAlt = down
|
keyAlt = down
|
||||||
case key.CodeLeftControl:
|
case key.CodeLeftControl:
|
||||||
keyCtrl = down
|
keyCtrl = down
|
||||||
default:
|
case key.CodeLeftShift:
|
||||||
keyShift = down
|
keyShift = down
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shift := down
|
||||||
|
if keyShift {
|
||||||
|
shift = keyShift
|
||||||
|
}
|
||||||
|
|
||||||
_, err := vm.TypeOnKeyboard(driver.KeyInput{
|
_, err := vm.TypeOnKeyboard(driver.KeyInput{
|
||||||
Scancode: code,
|
Scancode: code,
|
||||||
Ctrl: keyCtrl,
|
Ctrl: keyCtrl,
|
||||||
Alt: keyAlt,
|
Alt: keyAlt,
|
||||||
Shift: keyShift,
|
Shift: shift,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
|
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ func (s *StepConfigParams) Run(_ context.Context, state multistep.StateBag) mult
|
|||||||
if s.Config.ToolsUpgradePolicy {
|
if s.Config.ToolsUpgradePolicy {
|
||||||
info.ToolsUpgradePolicy = "UpgradeAtPowerCycle"
|
info.ToolsUpgradePolicy = "UpgradeAtPowerCycle"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Adding configuration parameters...")
|
ui.Say("Adding configuration parameters...")
|
||||||
if err := vm.AddConfigParams(configParams, info); err != nil {
|
if err := vm.AddConfigParams(configParams, info); err != nil {
|
||||||
state.Put("error", fmt.Errorf("error adding configuration parameters: %v", err))
|
state.Put("error", fmt.Errorf("error adding configuration parameters: %v", err))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
//
|
//
|
||||||
// Example usage:
|
// Example usage:
|
||||||
//
|
//
|
||||||
|
// In JSON:
|
||||||
// ```json
|
// ```json
|
||||||
// ...
|
// ...
|
||||||
// "vm_name": "example-ubuntu",
|
// "vm_name": "example-ubuntu",
|
||||||
@@ -40,6 +41,16 @@ import (
|
|||||||
// "output_directory": "./output_vsphere"
|
// "output_directory": "./output_vsphere"
|
||||||
// },
|
// },
|
||||||
// ```
|
// ```
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// # ...
|
||||||
|
// vm_name = "example-ubuntu"
|
||||||
|
// # ...
|
||||||
|
// export {
|
||||||
|
// force = true
|
||||||
|
// output_directory = "./output_vsphere"
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
// The above configuration would create the following files:
|
// The above configuration would create the following files:
|
||||||
//
|
//
|
||||||
// ```text
|
// ```text
|
||||||
@@ -66,12 +77,20 @@ type ExportConfig struct {
|
|||||||
//
|
//
|
||||||
// For example, adding the following export config option would output the mac addresses for all Ethernet devices in the ovf file:
|
// For example, adding the following export config option would output the mac addresses for all Ethernet devices in the ovf file:
|
||||||
//
|
//
|
||||||
|
// In JSON:
|
||||||
// ```json
|
// ```json
|
||||||
// ...
|
// ...
|
||||||
// "export": {
|
// "export": {
|
||||||
// "options": ["mac"]
|
// "options": ["mac"]
|
||||||
// },
|
// },
|
||||||
// ```
|
// ```
|
||||||
|
// In HCL2:
|
||||||
|
// ```hcl
|
||||||
|
// ...
|
||||||
|
// export {
|
||||||
|
// options = ["mac"]
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
Options []string `mapstructure:"options"`
|
Options []string `mapstructure:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +124,7 @@ func (c *ExportConfig) Prepare(ctx *interpolate.Context, lc *LocationConfig, pc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(c.OutputDir.OutputDir, 0750); err != nil {
|
if err := os.MkdirAll(c.OutputDir.OutputDir, c.OutputDir.DirPerm); err != nil {
|
||||||
errs = packer.MultiErrorAppend(errs, errors.Wrap(err, "unable to make directory for export"))
|
errs = packer.MultiErrorAppend(errs, errors.Wrap(err, "unable to make directory for export"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
@@ -9,12 +11,13 @@ import (
|
|||||||
// FlatExportConfig is an auto-generated flat version of ExportConfig.
|
// FlatExportConfig is an auto-generated flat version of ExportConfig.
|
||||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
type FlatExportConfig struct {
|
type FlatExportConfig struct {
|
||||||
Name *string `mapstructure:"name" cty:"name" hcl:"name"`
|
Name *string `mapstructure:"name" cty:"name" hcl:"name"`
|
||||||
Force *bool `mapstructure:"force" cty:"force" hcl:"force"`
|
Force *bool `mapstructure:"force" cty:"force" hcl:"force"`
|
||||||
Images *bool `mapstructure:"images" cty:"images" hcl:"images"`
|
Images *bool `mapstructure:"images" cty:"images" hcl:"images"`
|
||||||
Manifest *string `mapstructure:"manifest" cty:"manifest" hcl:"manifest"`
|
Manifest *string `mapstructure:"manifest" cty:"manifest" hcl:"manifest"`
|
||||||
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
||||||
Options []string `mapstructure:"options" cty:"options" hcl:"options"`
|
DirPerm *os.FileMode `mapstructure:"directory_permission" required:"false" cty:"directory_permission" hcl:"directory_permission"`
|
||||||
|
Options []string `mapstructure:"options" cty:"options" hcl:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlatMapstructure returns a new FlatExportConfig.
|
// FlatMapstructure returns a new FlatExportConfig.
|
||||||
@@ -29,12 +32,13 @@ func (*ExportConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.
|
|||||||
// The decoded values from this spec will then be applied to a FlatExportConfig.
|
// The decoded values from this spec will then be applied to a FlatExportConfig.
|
||||||
func (*FlatExportConfig) HCL2Spec() map[string]hcldec.Spec {
|
func (*FlatExportConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
s := map[string]hcldec.Spec{
|
s := map[string]hcldec.Spec{
|
||||||
"name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
|
"name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
|
||||||
"force": &hcldec.AttrSpec{Name: "force", Type: cty.Bool, Required: false},
|
"force": &hcldec.AttrSpec{Name: "force", Type: cty.Bool, Required: false},
|
||||||
"images": &hcldec.AttrSpec{Name: "images", Type: cty.Bool, Required: false},
|
"images": &hcldec.AttrSpec{Name: "images", Type: cty.Bool, Required: false},
|
||||||
"manifest": &hcldec.AttrSpec{Name: "manifest", Type: cty.String, Required: false},
|
"manifest": &hcldec.AttrSpec{Name: "manifest", Type: cty.String, Required: false},
|
||||||
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
||||||
"options": &hcldec.AttrSpec{Name: "options", Type: cty.List(cty.String), Required: false},
|
"directory_permission": &hcldec.AttrSpec{Name: "directory_permission", Type: cty.Number, Required: false},
|
||||||
|
"options": &hcldec.AttrSpec{Name: "options", Type: cty.List(cty.String), Required: false},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,28 +13,41 @@ import (
|
|||||||
"github.com/vmware/govmomi/vapi/vcenter"
|
"github.com/vmware/govmomi/vapi/vcenter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// With this configuration Packer creates a library item in a content library whose content is a virtual machine template created from the just built VM.
|
// With this configuration Packer creates a library item in a content library whose content is a VM template
|
||||||
// The virtual machine template is stored in a newly created library item.
|
// or an OVF template created from the just built VM.
|
||||||
|
// The template is stored in a existing or newly created library item.
|
||||||
type ContentLibraryDestinationConfig struct {
|
type ContentLibraryDestinationConfig struct {
|
||||||
// Name of the library in which the new library item containing the VM template should be created.
|
// Name of the library in which the new library item containing the template should be created/updated.
|
||||||
// The Content Library should be of type Local to allow deploying virtual machines.
|
// The Content Library should be of type Local to allow deploying virtual machines.
|
||||||
Library string `mapstructure:"library"`
|
Library string `mapstructure:"library"`
|
||||||
// Name of the library item that will be created. The name of the item should be different from [vm_name](#vm_name).
|
// Name of the library item that will be created or updated.
|
||||||
// Defaults to [vm_name](#vm_name) + timestamp.
|
// For VM templates, the name of the item should be different from [vm_name](#vm_name) and
|
||||||
|
// the default is [vm_name](#vm_name) + timestamp when not set. VM templates will be always imported to a new library item.
|
||||||
|
// For OVF templates, the name defaults to [vm_name](#vm_name) when not set, and if an item with the same name already
|
||||||
|
// exists it will be then updated with the new OVF template, otherwise a new item will be created.
|
||||||
|
//
|
||||||
|
// ~> **Note**: It's not possible to update existing library items with a new VM template. If updating an existing library
|
||||||
|
// item is necessary, use an OVF template instead by setting the [ovf](#ovf) option as `true`.
|
||||||
|
//
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
// Description of the library item that will be created. Defaults to "Packer imported [vm_name](#vm_name) VM template".
|
// Description of the library item that will be created.
|
||||||
|
// This option is not used when importing OVF templates.
|
||||||
|
// Defaults to "Packer imported [vm_name](#vm_name) VM template".
|
||||||
Description string `mapstructure:"description"`
|
Description string `mapstructure:"description"`
|
||||||
// Cluster onto which the virtual machine template should be placed.
|
// Cluster onto which the virtual machine template should be placed.
|
||||||
// If cluster and resource_pool are both specified, resource_pool must belong to cluster.
|
// If cluster and resource_pool are both specified, resource_pool must belong to cluster.
|
||||||
// If cluster and host are both specified, host must be a member of cluster.
|
// If cluster and host are both specified, host must be a member of cluster.
|
||||||
|
// This option is not used when importing OVF templates.
|
||||||
// Defaults to [cluster](#cluster).
|
// Defaults to [cluster](#cluster).
|
||||||
Cluster string `mapstructure:"cluster"`
|
Cluster string `mapstructure:"cluster"`
|
||||||
// Virtual machine folder into which the virtual machine template should be placed.
|
// Virtual machine folder into which the virtual machine template should be placed.
|
||||||
|
// This option is not used when importing OVF templates.
|
||||||
// Defaults to the same folder as the source virtual machine.
|
// Defaults to the same folder as the source virtual machine.
|
||||||
Folder string `mapstructure:"folder"`
|
Folder string `mapstructure:"folder"`
|
||||||
// Host onto which the virtual machine template should be placed.
|
// Host onto which the virtual machine template should be placed.
|
||||||
// If host and resource_pool are both specified, resource_pool must belong to host.
|
// If host and resource_pool are both specified, resource_pool must belong to host.
|
||||||
// If host and cluster are both specified, host must be a member of cluster.
|
// If host and cluster are both specified, host must be a member of cluster.
|
||||||
|
// This option is not used when importing OVF templates.
|
||||||
// Defaults to [host](#host).
|
// Defaults to [host](#host).
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
// Resource pool into which the virtual machine template should be placed.
|
// Resource pool into which the virtual machine template should be placed.
|
||||||
@@ -42,10 +55,14 @@ type ContentLibraryDestinationConfig struct {
|
|||||||
// the system will attempt to choose a suitable resource pool for the virtual machine template.
|
// the system will attempt to choose a suitable resource pool for the virtual machine template.
|
||||||
ResourcePool string `mapstructure:"resource_pool"`
|
ResourcePool string `mapstructure:"resource_pool"`
|
||||||
// The datastore for the virtual machine template's configuration and log files.
|
// The datastore for the virtual machine template's configuration and log files.
|
||||||
|
// This option is not used when importing OVF templates.
|
||||||
// Defaults to the storage backing associated with the library specified by library.
|
// Defaults to the storage backing associated with the library specified by library.
|
||||||
Datastore string `mapstructure:"datastore"`
|
Datastore string `mapstructure:"datastore"`
|
||||||
// If set to true, the VM will be destroyed after deploying the template to the Content Library. Defaults to `false`.
|
// If set to true, the VM will be destroyed after deploying the template to the Content Library.
|
||||||
|
// Defaults to `false`.
|
||||||
Destroy bool `mapstructure:"destroy"`
|
Destroy bool `mapstructure:"destroy"`
|
||||||
|
// When set to true, Packer will import and OVF template to the content library item. Defaults to `false`.
|
||||||
|
Ovf bool `mapstructure:"ovf"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContentLibraryDestinationConfig) Prepare(lc *LocationConfig) []error {
|
func (c *ContentLibraryDestinationConfig) Prepare(lc *LocationConfig) []error {
|
||||||
@@ -54,31 +71,38 @@ func (c *ContentLibraryDestinationConfig) Prepare(lc *LocationConfig) []error {
|
|||||||
if c.Library == "" {
|
if c.Library == "" {
|
||||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("a library name must be provided"))
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("a library name must be provided"))
|
||||||
}
|
}
|
||||||
if c.Name == lc.VMName {
|
|
||||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("the content library destination name must be different from the VM name"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Name == "" {
|
if c.Ovf {
|
||||||
// Add timestamp to the the name to differentiate from the original VM
|
if c.Name == "" {
|
||||||
// otherwise vSphere won't be able to create the template which will be imported
|
c.Name = lc.VMName
|
||||||
name, err := interpolate.Render(lc.VMName+"{{timestamp}}", nil)
|
}
|
||||||
if err != nil {
|
} else {
|
||||||
errs = packer.MultiErrorAppend(errs,
|
if c.Name == lc.VMName {
|
||||||
fmt.Errorf("unable to parse content library VM template name: %s", err))
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("the content library destination name must be different from the VM name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Name == "" {
|
||||||
|
// Add timestamp to the name to differentiate from the original VM
|
||||||
|
// otherwise vSphere won't be able to create the template which will be imported
|
||||||
|
name, err := interpolate.Render(lc.VMName+"{{timestamp}}", nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("unable to parse content library VM template name: %s", err))
|
||||||
|
}
|
||||||
|
c.Name = name
|
||||||
|
}
|
||||||
|
if c.Cluster == "" {
|
||||||
|
c.Cluster = lc.Cluster
|
||||||
|
}
|
||||||
|
if c.Host == "" {
|
||||||
|
c.Host = lc.Host
|
||||||
|
}
|
||||||
|
if c.ResourcePool == "" {
|
||||||
|
c.ResourcePool = lc.ResourcePool
|
||||||
|
}
|
||||||
|
if c.Description == "" {
|
||||||
|
c.Description = fmt.Sprintf("Packer imported %s VM template", lc.VMName)
|
||||||
}
|
}
|
||||||
c.Name = name
|
|
||||||
}
|
|
||||||
if c.Cluster == "" {
|
|
||||||
c.Cluster = lc.Cluster
|
|
||||||
}
|
|
||||||
if c.Host == "" {
|
|
||||||
c.Host = lc.Host
|
|
||||||
}
|
|
||||||
if c.ResourcePool == "" {
|
|
||||||
c.ResourcePool = lc.ResourcePool
|
|
||||||
}
|
|
||||||
if c.Description == "" {
|
|
||||||
c.Description = fmt.Sprintf("Packer imported %s VM template", lc.VMName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs != nil && len(errs.Errors) > 0 {
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
@@ -95,7 +119,42 @@ type StepImportToContentLibrary struct {
|
|||||||
func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if s.ContentLibConfig.Ovf {
|
||||||
|
ui.Say(fmt.Sprintf("Importing VM OVF template %s to Content Library...", s.ContentLibConfig.Name))
|
||||||
|
err = s.importOvfTemplate(vm)
|
||||||
|
} else {
|
||||||
|
ui.Say(fmt.Sprintf("Importing VM template %s to Content Library...", s.ContentLibConfig.Name))
|
||||||
|
err = s.importVmTemplate(vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Failed to import template %s: %s", s.ContentLibConfig.Name, err.Error()))
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ContentLibConfig.Destroy {
|
||||||
|
state.Put("destroy_vm", s.ContentLibConfig.Destroy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachine) error {
|
||||||
|
ovf := vcenter.OVF{
|
||||||
|
Spec: vcenter.CreateSpec{
|
||||||
|
Name: s.ContentLibConfig.Name,
|
||||||
|
},
|
||||||
|
Target: vcenter.LibraryTarget{
|
||||||
|
LibraryID: s.ContentLibConfig.Library,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return vm.ImportOvfToContentLibrary(ovf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepImportToContentLibrary) importVmTemplate(vm *driver.VirtualMachine) error {
|
||||||
template := vcenter.Template{
|
template := vcenter.Template{
|
||||||
Name: s.ContentLibConfig.Name,
|
Name: s.ContentLibConfig.Name,
|
||||||
Description: s.ContentLibConfig.Description,
|
Description: s.ContentLibConfig.Description,
|
||||||
@@ -114,19 +173,7 @@ func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.Stat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Importing VM template %s to Content Library...", s.ContentLibConfig.Name))
|
return vm.ImportToContentLibrary(template)
|
||||||
err := vm.ImportToContentLibrary(template)
|
|
||||||
if err != nil {
|
|
||||||
ui.Error(fmt.Sprintf("Failed to import VM template %s: %s", s.ContentLibConfig.Name, err.Error()))
|
|
||||||
state.Put("error", err)
|
|
||||||
return multistep.ActionHalt
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ContentLibConfig.Destroy {
|
|
||||||
state.Put("destroy_vm", s.ContentLibConfig.Destroy)
|
|
||||||
}
|
|
||||||
|
|
||||||
return multistep.ActionContinue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepImportToContentLibrary) Cleanup(multistep.StateBag) {
|
func (s *StepImportToContentLibrary) Cleanup(multistep.StateBag) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type FlatContentLibraryDestinationConfig struct {
|
|||||||
ResourcePool *string `mapstructure:"resource_pool" cty:"resource_pool" hcl:"resource_pool"`
|
ResourcePool *string `mapstructure:"resource_pool" cty:"resource_pool" hcl:"resource_pool"`
|
||||||
Datastore *string `mapstructure:"datastore" cty:"datastore" hcl:"datastore"`
|
Datastore *string `mapstructure:"datastore" cty:"datastore" hcl:"datastore"`
|
||||||
Destroy *bool `mapstructure:"destroy" cty:"destroy" hcl:"destroy"`
|
Destroy *bool `mapstructure:"destroy" cty:"destroy" hcl:"destroy"`
|
||||||
|
Ovf *bool `mapstructure:"ovf" cty:"ovf" hcl:"ovf"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlatMapstructure returns a new FlatContentLibraryDestinationConfig.
|
// FlatMapstructure returns a new FlatContentLibraryDestinationConfig.
|
||||||
@@ -41,6 +42,7 @@ func (*FlatContentLibraryDestinationConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"resource_pool": &hcldec.AttrSpec{Name: "resource_pool", Type: cty.String, Required: false},
|
"resource_pool": &hcldec.AttrSpec{Name: "resource_pool", Type: cty.String, Required: false},
|
||||||
"datastore": &hcldec.AttrSpec{Name: "datastore", Type: cty.String, Required: false},
|
"datastore": &hcldec.AttrSpec{Name: "datastore", Type: cty.String, Required: false},
|
||||||
"destroy": &hcldec.AttrSpec{Name: "destroy", Type: cty.Bool, Required: false},
|
"destroy": &hcldec.AttrSpec{Name: "destroy", Type: cty.Bool, Required: false},
|
||||||
|
"ovf": &hcldec.AttrSpec{Name: "ovf", Type: cty.Bool, Required: false},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,10 +97,16 @@ func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multi
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
|
cancel()
|
||||||
|
<-waitDone
|
||||||
|
if ip != "" {
|
||||||
|
state.Put("ip", ip)
|
||||||
|
log.Printf("[WARN] API timeout waiting for IP but one IP was found. Using IP: %s", ip)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
err := fmt.Errorf("Timeout waiting for IP.")
|
err := fmt.Errorf("Timeout waiting for IP.")
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
cancel()
|
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
cancel()
|
cancel()
|
||||||
@@ -140,6 +146,14 @@ loop:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for ctx cancellation to avoid printing any IP logs at the timeout
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ip, fmt.Errorf("IP wait cancelled.")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
if prevIp == "" || prevIp != ip {
|
if prevIp == "" || prevIp != ip {
|
||||||
if prevIp == "" {
|
if prevIp == "" {
|
||||||
log.Printf("VM IP aquired: %s", ip)
|
log.Printf("VM IP aquired: %s", ip)
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package driver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/vmware/govmomi/object"
|
"github.com/vmware/govmomi/object"
|
||||||
|
"github.com/vmware/govmomi/property"
|
||||||
"github.com/vmware/govmomi/vim25/mo"
|
"github.com/vmware/govmomi/vim25/mo"
|
||||||
"github.com/vmware/govmomi/vim25/soap"
|
"github.com/vmware/govmomi/vim25/soap"
|
||||||
"github.com/vmware/govmomi/vim25/types"
|
"github.com/vmware/govmomi/vim25/types"
|
||||||
@@ -57,6 +61,21 @@ func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetDatastoreName(id string) (string, error) {
|
||||||
|
obj := types.ManagedObjectReference{
|
||||||
|
Type: "Datastore",
|
||||||
|
Value: id,
|
||||||
|
}
|
||||||
|
pc := property.DefaultCollector(d.vimClient)
|
||||||
|
var me mo.ManagedEntity
|
||||||
|
|
||||||
|
err := pc.RetrieveOne(d.ctx, obj, []string{"name"}, &me)
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
return me.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
|
func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
|
||||||
var p []string
|
var p []string
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
@@ -85,6 +104,42 @@ func (ds *Datastore) ResolvePath(path string) string {
|
|||||||
return ds.ds.Path(path)
|
return ds.ds.Path(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The file ID isn't available via the API, so we use DatastoreBrowser to search
|
||||||
|
func (d *Driver) GetDatastoreFilePath(datastoreID, dir, filename string) (string, error) {
|
||||||
|
ref := types.ManagedObjectReference{Type: "Datastore", Value: datastoreID}
|
||||||
|
ds := object.NewDatastore(d.vimClient, ref)
|
||||||
|
|
||||||
|
b, err := ds.Browser(d.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return filename, err
|
||||||
|
}
|
||||||
|
ext := path.Ext(filename)
|
||||||
|
pat := strings.Replace(filename, ext, "*"+ext, 1)
|
||||||
|
spec := types.HostDatastoreBrowserSearchSpec{
|
||||||
|
MatchPattern: []string{pat},
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := b.SearchDatastore(d.ctx, dir, &spec)
|
||||||
|
if err != nil {
|
||||||
|
return filename, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := task.WaitForResult(d.ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return filename, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, ok := info.Result.(types.HostDatastoreBrowserSearchResults)
|
||||||
|
if !ok {
|
||||||
|
return filename, fmt.Errorf("search(%s) result type=%T", pat, info.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.File) != 1 {
|
||||||
|
return filename, fmt.Errorf("search(%s) result files=%d", pat, len(res.File))
|
||||||
|
}
|
||||||
|
return res.File[0].GetFileInfo().Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_uploads bool) error {
|
func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_uploads bool) error {
|
||||||
p := soap.DefaultUpload
|
p := soap.DefaultUpload
|
||||||
ctx := ds.driver.ctx
|
ctx := ds.driver.ctx
|
||||||
@@ -128,3 +183,28 @@ func RemoveDatastorePrefix(path string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DatastoreIsoPath struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DatastoreIsoPath) Validate() bool {
|
||||||
|
// Matches:
|
||||||
|
// [datastore] /dir/subdir/file
|
||||||
|
// [datastore] dir/subdir/file
|
||||||
|
// [] /dir/subdir/file
|
||||||
|
// /dir/subdir/file or dir/subdir/file
|
||||||
|
matched, _ := regexp.MatchString(`^((\[\w*\])?\s*([^\[\]]+))$`, d.path)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DatastoreIsoPath) GetFilePath() string {
|
||||||
|
filePath := d.path
|
||||||
|
parts := strings.Split(d.path, "]")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
// removes datastore name from path
|
||||||
|
filePath = parts[1]
|
||||||
|
filePath = strings.TrimLeft(filePath, " ")
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package driver
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDatastoreIsoPath(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
isoPath string
|
||||||
|
filePath string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
isoPath: "[datastore] dir/subdir/file",
|
||||||
|
filePath: "dir/subdir/file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isoPath: "[] dir/subdir/file",
|
||||||
|
filePath: "dir/subdir/file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isoPath: "dir/subdir/file",
|
||||||
|
filePath: "dir/subdir/file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isoPath: "[datastore] /dir/subdir/file",
|
||||||
|
filePath: "/dir/subdir/file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isoPath: "/dir/subdir/file [datastore] ",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isoPath: "[datastore][] /dir/subdir/file",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc {
|
||||||
|
dsIsoPath := &DatastoreIsoPath{path: c.isoPath}
|
||||||
|
if dsIsoPath.Validate() != c.valid {
|
||||||
|
t.Fatalf("Expecting %s to be %t but was %t", c.isoPath, c.valid, !c.valid)
|
||||||
|
}
|
||||||
|
if !c.valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filePath := dsIsoPath.GetFilePath()
|
||||||
|
if filePath != c.filePath {
|
||||||
|
t.Fatalf("Expecting %s but got %s", c.filePath, filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ type Driver struct {
|
|||||||
// context that controls the authenticated sessions used to run the VM commands
|
// context that controls the authenticated sessions used to run the VM commands
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
client *govmomi.Client
|
client *govmomi.Client
|
||||||
restClient *rest.Client
|
vimClient *vim25.Client
|
||||||
|
restClient *RestClient
|
||||||
finder *find.Finder
|
finder *find.Finder
|
||||||
datacenter *object.Datacenter
|
datacenter *object.Datacenter
|
||||||
}
|
}
|
||||||
@@ -59,12 +60,6 @@ func NewDriver(config *ConnectConfig) (*Driver, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
restClient := rest.NewClient(vimClient)
|
|
||||||
err = restClient.Login(ctx, credentials)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
finder := find.NewFinder(client.Client, false)
|
finder := find.NewFinder(client.Client, false)
|
||||||
datacenter, err := finder.DatacenterOrDefault(ctx, config.Datacenter)
|
datacenter, err := finder.DatacenterOrDefault(ctx, config.Datacenter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,11 +68,32 @@ func NewDriver(config *ConnectConfig) (*Driver, error) {
|
|||||||
finder.SetDatacenter(datacenter)
|
finder.SetDatacenter(datacenter)
|
||||||
|
|
||||||
d := Driver{
|
d := Driver{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
client: client,
|
client: client,
|
||||||
restClient: restClient,
|
vimClient: vimClient,
|
||||||
|
restClient: &RestClient{
|
||||||
|
client: rest.NewClient(vimClient),
|
||||||
|
credentials: credentials,
|
||||||
|
},
|
||||||
datacenter: datacenter,
|
datacenter: datacenter,
|
||||||
finder: finder,
|
finder: finder,
|
||||||
}
|
}
|
||||||
return &d, nil
|
return &d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rest.Client requires vCenter.
|
||||||
|
// RestClient is to modularize the rest.Client session and use it only when is necessary.
|
||||||
|
// This will allow users without vCenter to use the other features that doesn't use the rest.Client.
|
||||||
|
// To use the client login/logout must be done to create an authenticated session.
|
||||||
|
type RestClient struct {
|
||||||
|
client *rest.Client
|
||||||
|
credentials *url.Userinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RestClient) Login(ctx context.Context) error {
|
||||||
|
return r.client.Login(ctx, r.credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RestClient) Logout(ctx context.Context) error {
|
||||||
|
return r.client.Logout(ctx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
package driver
|
package driver
|
||||||
|
|
||||||
import "github.com/vmware/govmomi/vapi/library"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vmware/govmomi/vapi/library"
|
||||||
|
)
|
||||||
|
|
||||||
type Library struct {
|
type Library struct {
|
||||||
driver *Driver
|
driver *Driver
|
||||||
library *library.Library
|
library *library.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) FindContentLibrary(name string) (*Library, error) {
|
func (d *Driver) FindContentLibraryByName(name string) (*Library, error) {
|
||||||
lm := library.NewManager(d.restClient)
|
lm := library.NewManager(d.restClient.client)
|
||||||
l, err := lm.GetLibraryByName(d.ctx, name)
|
l, err := lm.GetLibraryByName(d.ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -18,3 +25,89 @@ func (d *Driver) FindContentLibrary(name string) (*Library, error) {
|
|||||||
driver: d,
|
driver: d,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FindContentLibraryItem(libraryId string, name string) (*library.Item, error) {
|
||||||
|
lm := library.NewManager(d.restClient.client)
|
||||||
|
items, err := lm.GetLibraryItems(d.ctx, libraryId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Name == name {
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Item %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FindContentLibraryFileDatastorePath(isoPath string) (string, error) {
|
||||||
|
log.Printf("Check if ISO path is a Content Library path")
|
||||||
|
err := d.restClient.Login(d.ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("vCenter client not available. ISO path not identified as a Content Library path")
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryFilePath := &LibraryFilePath{path: isoPath}
|
||||||
|
err = libraryFilePath.Validate()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ISO path not identified as a Content Library path")
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
libraryName := libraryFilePath.GetLibraryName()
|
||||||
|
itemName := libraryFilePath.GetLibraryItemName()
|
||||||
|
isoFile := libraryFilePath.GetFileName()
|
||||||
|
|
||||||
|
lib, err := d.FindContentLibraryByName(libraryName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ISO path not identified as a Content Library path")
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
log.Printf("ISO path identified as a Content Library path")
|
||||||
|
log.Printf("Finding the equivalent datastore path for the Content Library ISO file path")
|
||||||
|
libItem, err := d.FindContentLibraryItem(lib.library.ID, itemName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Couldn't find item %s: %s", itemName, err.Error())
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
datastoreName, err := d.GetDatastoreName(lib.library.Storage[0].DatastoreID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Couldn't find datastore name for library %s", libraryName)
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
libItemDir := fmt.Sprintf("[%s] contentlib-%s/%s", datastoreName, lib.library.ID, libItem.ID)
|
||||||
|
|
||||||
|
isoFilePath, err := d.GetDatastoreFilePath(lib.library.Storage[0].DatastoreID, libItemDir, isoFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Couldn't find datastore ID path for %s", isoFile)
|
||||||
|
return isoPath, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = d.restClient.Logout(d.ctx)
|
||||||
|
return path.Join(libItemDir, isoFilePath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LibraryFilePath struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LibraryFilePath) Validate() error {
|
||||||
|
l.path = strings.TrimLeft(l.path, "/")
|
||||||
|
parts := strings.Split(l.path, "/")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return fmt.Errorf("Not a valid Content Library File path. The path must contain the nanmes for the library, item and file.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LibraryFilePath) GetLibraryName() string {
|
||||||
|
return strings.Split(l.path, "/")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LibraryFilePath) GetLibraryItemName() string {
|
||||||
|
return strings.Split(l.path, "/")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LibraryFilePath) GetFileName() string {
|
||||||
|
return strings.Split(l.path, "/")[2]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package driver
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLibraryFilePath(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
filePath string
|
||||||
|
libraryName string
|
||||||
|
libraryItemName string
|
||||||
|
fileName string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
filePath: "lib/item/file",
|
||||||
|
libraryName: "lib",
|
||||||
|
libraryItemName: "item",
|
||||||
|
fileName: "file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: "/lib/item/file",
|
||||||
|
libraryName: "lib",
|
||||||
|
libraryItemName: "item",
|
||||||
|
fileName: "file",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: "/lib/item/filedir/file",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: "/lib/item",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: "/lib",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc {
|
||||||
|
libraryFilePath := &LibraryFilePath{path: c.filePath}
|
||||||
|
if err := libraryFilePath.Validate(); err != nil {
|
||||||
|
if c.valid {
|
||||||
|
t.Fatalf("Expecting %s to be valid", c.filePath)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
libraryName := libraryFilePath.GetLibraryName()
|
||||||
|
if libraryName != c.libraryName {
|
||||||
|
t.Fatalf("Expecting %s but got %s", c.libraryName, libraryName)
|
||||||
|
}
|
||||||
|
libraryItemName := libraryFilePath.GetLibraryItemName()
|
||||||
|
if libraryItemName != c.libraryItemName {
|
||||||
|
t.Fatalf("Expecting %s but got %s", c.libraryItemName, libraryItemName)
|
||||||
|
}
|
||||||
|
fileName := libraryFilePath.GetFileName()
|
||||||
|
if fileName != c.fileName {
|
||||||
|
t.Fatalf("Expecting %s but got %s", c.fileName, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -524,6 +524,14 @@ func (vm *VirtualMachine) Configure(config *HardwareConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vm *VirtualMachine) Customize(spec types.CustomizationSpec) error {
|
||||||
|
task, err := vm.vm.Customize(vm.driver.ctx, spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return task.Wait(vm.driver.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (vm *VirtualMachine) ResizeDisk(diskSize int64) error {
|
func (vm *VirtualMachine) ResizeDisk(diskSize int64) error {
|
||||||
var confSpec types.VirtualMachineConfigSpec
|
var confSpec types.VirtualMachineConfigSpec
|
||||||
|
|
||||||
@@ -675,17 +683,57 @@ func (vm *VirtualMachine) ConvertToTemplate() error {
|
|||||||
return vm.vm.MarkAsTemplate(vm.driver.ctx)
|
return vm.vm.MarkAsTemplate(vm.driver.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vm *VirtualMachine) ImportToContentLibrary(template vcenter.Template) error {
|
func (vm *VirtualMachine) ImportOvfToContentLibrary(ovf vcenter.OVF) error {
|
||||||
template.SourceVM = vm.vm.Reference().Value
|
err := vm.driver.restClient.Login(vm.driver.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
l, err := vm.driver.FindContentLibrary(template.Library)
|
l, err := vm.driver.FindContentLibraryByName(ovf.Target.LibraryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if l.library.Type != "LOCAL" {
|
if l.library.Type != "LOCAL" {
|
||||||
return fmt.Errorf("can not deploy a VM to the content library %s of type %s; the content library must be of type LOCAL", template.Library, l.library.Type)
|
return fmt.Errorf("can not deploy a VM to the content library %s of type %s; "+
|
||||||
|
"the content library must be of type LOCAL", ovf.Target.LibraryID, l.library.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item, err := vm.driver.FindContentLibraryItem(l.library.ID, ovf.Spec.Name)
|
||||||
|
if err == nil {
|
||||||
|
// Updates existing library item
|
||||||
|
ovf.Target.LibraryItemID = item.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
ovf.Target.LibraryID = l.library.ID
|
||||||
|
ovf.Source.Value = vm.vm.Reference().Value
|
||||||
|
ovf.Source.Type = "VirtualMachine"
|
||||||
|
|
||||||
|
vcm := vcenter.NewManager(vm.driver.restClient.client)
|
||||||
|
_, err = vcm.CreateOVF(vm.driver.ctx, ovf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm.driver.restClient.Logout(vm.driver.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VirtualMachine) ImportToContentLibrary(template vcenter.Template) error {
|
||||||
|
err := vm.driver.restClient.Login(vm.driver.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := vm.driver.FindContentLibraryByName(template.Library)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if l.library.Type != "LOCAL" {
|
||||||
|
return fmt.Errorf("can not deploy a VM to the content library %s of type %s; "+
|
||||||
|
"the content library must be of type LOCAL", template.Library, l.library.Type)
|
||||||
|
}
|
||||||
|
|
||||||
template.Library = l.library.ID
|
template.Library = l.library.ID
|
||||||
|
template.SourceVM = vm.vm.Reference().Value
|
||||||
|
|
||||||
if template.Placement.Cluster != "" {
|
if template.Placement.Cluster != "" {
|
||||||
c, err := vm.driver.FindCluster(template.Placement.Cluster)
|
c, err := vm.driver.FindCluster(template.Placement.Cluster)
|
||||||
@@ -724,9 +772,13 @@ func (vm *VirtualMachine) ImportToContentLibrary(template vcenter.Template) erro
|
|||||||
template.VMHomeStorage.Datastore = d.ds.Reference().Value
|
template.VMHomeStorage.Datastore = d.ds.Reference().Value
|
||||||
}
|
}
|
||||||
|
|
||||||
vcm := vcenter.NewManager(vm.driver.restClient)
|
vcm := vcenter.NewManager(vm.driver.restClient.client)
|
||||||
_, err = vcm.CreateTemplate(vm.driver.ctx, template)
|
_, err = vcm.CreateTemplate(vm.driver.ctx, template)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm.driver.restClient.Logout(vm.driver.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vm *VirtualMachine) GetDir() (string, error) {
|
func (vm *VirtualMachine) GetDir() (string, error) {
|
||||||
@@ -889,7 +941,7 @@ func newVGPUProfile(vGPUProfile string) types.VirtualPCIPassthrough {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vm *VirtualMachine) AddCdrom(controllerType string, isoPath string) error {
|
func (vm *VirtualMachine) AddCdrom(controllerType string, datastoreIsoPath string) error {
|
||||||
devices, err := vm.vm.Device(vm.driver.ctx)
|
devices, err := vm.vm.Device(vm.driver.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -915,11 +967,21 @@ func (vm *VirtualMachine) AddCdrom(controllerType string, isoPath string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isoPath != "" {
|
if datastoreIsoPath != "" {
|
||||||
devices.InsertIso(cdrom, isoPath)
|
ds := &DatastoreIsoPath{path: datastoreIsoPath}
|
||||||
|
if !ds.Validate() {
|
||||||
|
return fmt.Errorf("%s is not a valid iso path", datastoreIsoPath)
|
||||||
|
}
|
||||||
|
if libPath, err := vm.driver.FindContentLibraryFileDatastorePath(ds.GetFilePath()); err == nil {
|
||||||
|
datastoreIsoPath = libPath
|
||||||
|
} else {
|
||||||
|
log.Printf("Using %s as the datastore path", datastoreIsoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.InsertIso(cdrom, datastoreIsoPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Creating CD-ROM on controller '%v' with iso '%v'", controller, isoPath)
|
log.Printf("Creating CD-ROM on controller '%v' with iso '%v'", controller, datastoreIsoPath)
|
||||||
return vm.addDevice(cdrom)
|
return vm.addDevice(cdrom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,14 @@ import (
|
|||||||
type CDRomConfig struct {
|
type CDRomConfig struct {
|
||||||
// Which controller to use. Example: `sata`. Defaults to `ide`.
|
// Which controller to use. Example: `sata`. Defaults to `ide`.
|
||||||
CdromType string `mapstructure:"cdrom_type"`
|
CdromType string `mapstructure:"cdrom_type"`
|
||||||
// List of datastore paths to ISO files that will be mounted to the VM.
|
// List of Datastore or Content Library paths to ISO files that will be mounted to the VM.
|
||||||
// Example: `"[datastore1] ISO/ubuntu.iso"`.
|
// Here's an HCL2 example:
|
||||||
|
// ```hcl
|
||||||
|
// iso_paths = [
|
||||||
|
// "[datastore1] ISO/ubuntu.iso",
|
||||||
|
// "Packer Library Test/ubuntu-16.04.6-server-amd64/ubuntu-16.04.6-server-amd64.iso"
|
||||||
|
// ]
|
||||||
|
// ```
|
||||||
ISOPaths []string `mapstructure:"iso_paths"`
|
ISOPaths []string `mapstructure:"iso_paths"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user