Compare commits

...

151 Commits

Author SHA1 Message Date
Wilken Rivera 44eda339d3 docs/powershell: Add struct-markdown to provisioner config 2020-08-24 16:27:29 -04:00
Sylvia Moss 2152fa3313 Accept content library path in iso_paths (#9801) 2020-08-24 16:54:30 +02:00
Adrien Delorme fbfe31ceaf update flock + go mod tidy (#9805) 2020-08-24 10:49:43 +02:00
Adrien Delorme 7de6c21a8a HCL: document contextual source variable usage (#9799)
It is possible to use ${source.name} and ${source.type}, this was not documented before
2020-08-21 13:37:08 +02:00
Adrien Delorme f28c06a861 HCL: re-add the option to set a command for inspec (#9800) 2020-08-21 13:32:21 +02:00
Sylvia Moss ada2a431ef Modularise vCenter rest client (#9793) 2020-08-20 17:55:25 +02:00
Sylvia Moss 99800619b7 Add HCL2 examples to the virtualbox builders (#9788) 2020-08-20 15:43:38 +02:00
Megan Marsh 5b27fc7d61 update changelog 2020-08-19 07:43:03 -07:00
Sylvia Moss a1524bf96a Add option to also import OVF templates to the Content Library (#9755) 2020-08-19 13:20:25 +02:00
Megan Marsh 01d45d67c8 Merge pull request #9785 from hashicorp/document_7067
document workaround for upstream bug
2020-08-18 14:27:51 -07:00
Megan Marsh 6307710485 document workaround for upstream bug 2020-08-18 14:12:14 -07:00
Megan Marsh 229e80bcba Merge pull request #9784 from hashicorp/cleanup_local_vs_remote_output_directories
implement a remote_output_directory option, and heavily comment outpu…
2020-08-18 14:11:32 -07:00
Megan Marsh a2cfaace59 extract output dir configuration and defaulting into the step_output_dir, using pointers to make sure they get set properly back on the main config 2020-08-18 12:50:06 -07:00
Megan Marsh 3b2bedf794 implement a remote_output_directory option, and heavily comment output code 2020-08-18 12:50:00 -07:00
Wilken Rivera 20eeffee0d integration/secretsmanager: Add support for AWS SharedConfig file (#9781)
This changes updates the AWS Secrets manager session authentication
logic to support reading the AWS configuration file for default
credentials and region settings, if they are not provided via
environment variables.

* Modify error output a little to remove stutter "error ... : error ...`

Results before change
```
unset AWS_REGION
⇶  ~/pkg/packer build amazon-ebs_secretsmanager_shell-local.json
template: root:1:3: executing "root" at <aws_secretsmanager `packer/test/keys`
`shell`>: error calling aws_secretsmanager: Error getting secret: MissingRegion:
could not find region configuration
```

Results after change
```
unset AWS_REGION
⇶  ~/pkg/packer build amazon-ebs_secretsmanager_shell-local.json
null: output will be in this color.

==> null: Running local shell script: /tmp/packer-shell721444992
    null: powershell
Build 'null' finished after 4 milliseconds 121 microseconds.

==> Wait completed after 4 milliseconds 192 microseconds

==> Builds finished. The artifacts of successful builds are:
```
2020-08-18 10:51:48 -04:00
Megan Marsh b0d2201d55 Merge pull request #9782 from hashicorp/fix_9151
fully destroy vm if it was cancelled or errored
2020-08-18 07:22:18 -07:00
Megan Marsh 0a5cd953fc fully destroy vm if it was cancelled or errored 2020-08-17 14:07:58 -07:00
Megan Marsh aa2418cf01 add progress tracker to vmware-esx iso upload. Add colored prefix to … (#9779)
* add progress tracker to vmware-esx iso upload. Add colored prefix to tracker to make it clear which build a tracker belongs to.

* fix mock; fix tests
2020-08-17 14:35:42 -04:00
Megan Marsh c88ff4ec45 add colored prefix to progress tracker bar so it's clearer what build an uploading file belongs to (#9780) 2020-08-17 14:32:13 -04:00
Megan Marsh d74b47b734 Merge pull request #9777 from hashicorp/fix_9756
Add aws_polling config option to override env variables
2020-08-17 10:49:38 -07:00
sylviamoss 3cdf1d6234 trigger netlify 2020-08-17 18:09:16 +02:00
sylviamoss 1a0d5a93ee fix max_attempts typo 2020-08-17 17:58:22 +02:00
sylviamoss 788dc32598 add aws_polling config option to override env variables 2020-08-17 17:15:32 +02:00
Megan Marsh d20b34e71a Merge pull request #9772 from hashicorp/fix_9635
change pause to run connect again afterwards
2020-08-17 07:38:28 -07:00
Isaac Buckman 46a26a3d9e Fix Hyper-V boot command refs #5291 (#9765) 2020-08-17 16:00:43 +02:00
Wilken Rivera 56f6a976de integrations/secretsmanager: Add support for plaintext, non key/pair, secrets (#9773) 2020-08-17 13:41:21 +02:00
Wilken Rivera 156b6ac2b4 Update GitHub action (#9774)
Ignore issue events triggered by pull_requests
2020-08-17 13:35:19 +02:00
Sathyajith Bhat 9c8daed65e Fix typo in error message (#9775)
If a variable has not been passed, the error message shown has a tiny typo (varaibles instead of variables)
2020-08-16 21:39:08 -04:00
Marco Molteni 1a248e4868 Fix: build.sh: give validateToolPresence a chance to inform the user (#9776)
Since the build.sh script runs with `set -e` (exit _immediately_ in case
of error), we cannot first call the `which` command and, on a susequent
line, check its exit status with $?, it would be too late. Instead, we
idiomatically check on the same line of the invocation of `which`.

From the confusing:

    $ make bin
    ==> Checking for necessary tools...
    make: *** [bin] Error 1

To the informative:

    $ make bin
    ==> Checking for necessary tools...
    realpath is not on the path. Exiting...
    make: *** [bin] Error 1
2020-08-16 21:37:12 -04:00
Megan Marsh 2dcf950441 change pause to run connect again afterwards 2020-08-14 11:23:43 -07:00
Megan Marsh d486459a00 Merge pull request #9764 from hashicorp/hcl2_provisioner_override
Add override provisioner's option for HCL2
2020-08-14 09:49:34 -07:00
Wilken Rivera 89d57b8635 post-processor/googlecompute-import: Update documentation (#9767) 2020-08-14 12:04:44 +02:00
Megan Marsh 086141f9a9 add an hcl example to the docs pipeline (#9763) 2020-08-14 11:42:06 +02:00
Megan Marsh 8b4993e44c fix docs for kms key ids (#9766) 2020-08-14 11:35:35 +02:00
sylviamoss 39a8dee4ea add tests and update override example 2020-08-14 11:22:51 +02:00
Megan Marsh 99f503a22b update changelog 2020-08-13 13:11:50 -07:00
sylviamoss 62c3743890 implemet override provisioner's option for hcl2 2020-08-13 19:27:04 +02:00
Megan Marsh acba3ea3e7 Document network path (#9762) 2020-08-13 11:26:40 -04:00
Sylvia Moss a2277e312f add inspec provisioner hcl2 examples (#9761) 2020-08-13 11:23:56 -04:00
Megan Marsh 29b08e54bc Merge pull request #9760 from hashicorp/pipeline_with_hcl_docs
add hcl docs to pipeline build docs
2020-08-13 07:19:14 -07:00
Megan Marsh 321aa090df Merge pull request #9746 from jsmcnair/f-vault-function
Add vault function for HCL2 and documentation
2020-08-13 07:18:28 -07:00
Megan Marsh 2a80bff8c2 Update website/pages/guides/packer-on-cicd/pipelineing-builds.mdx
Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>
2020-08-13 07:05:26 -07:00
Megan Marsh b5b28c55fb update powershell provisioner docs to include HCL examples (#9759) 2020-08-13 13:45:30 +02:00
Megan Marsh 87b290ae67 add hcl docs to pipeline build docs 2020-08-12 15:24:47 -07:00
jsmcnair 78c280b1b7 Use common funcs / fix panic / clarify docs 2020-08-12 22:34:13 +01:00
Megan Marsh e97c56dc72 Merge pull request #9559 from hashicorp/fix_9482
builder/azure-arm: Remove duplicate resource deletion steps
2020-08-12 12:43:09 -07:00
Wilken Rivera 55812fe8a5 Handle error of deferred deletion 2020-08-12 11:30:15 -04:00
Wilken Rivera 5700f74c8c Handle lease not present error 2020-08-12 11:27:07 -04:00
Wilken Rivera d91839b93e Update deletion logic for OS disks
* Add an ResourceNotFound error check
* Add logic to break temporary disk lease before deleting it
2020-08-12 11:03:51 -04:00
Wilken Rivera ba6d11d518 Remove StepDeleteOSDisk in favor of StepDeployTemplate#Cleanup 2020-08-12 11:03:03 -04:00
Megan Marsh 54469c4728 Add all of the custom AWS template engines to GeneratedData for use b… (#9751) 2020-08-12 10:43:15 +02:00
Sylvia Moss e475db6a66 check for ip after timeout (#9748) 2020-08-12 10:39:12 +02:00
Wilken Rivera 9d453624ff Merge pull request #9752 from hashicorp/ansible_local_docs
ansible-local hcl docs
2020-08-11 21:11:05 -04:00
Wilken Rivera 956651e711 Merge pull request #9753 from hashicorp/breakpoint_hcl_docs
hcl example for breakpoint provisioner
2020-08-11 21:07:38 -04:00
Wilken Rivera 7abeafaa04 Merge pull request #9754 from jfpanisset/remove_iso_checksum_type_2
Remove more examples of deprecated iso_checksum_type in docs
2020-08-11 21:04:02 -04:00
Jean-Francois Panisset 6f8a909cef Remove more examples of deprecated iso_checksum_type in docs
Follow up to remove_iso_checksum_type

Signed-off-by: Jean-Francois Panisset <panisset@gmail.com>
2020-08-11 17:29:21 -07:00
Megan Marsh 0aa4d7117d hcl example for breakpoint provisioner 2020-08-11 16:28:02 -07:00
Megan Marsh 0a3ff3b190 ansible-local hcl docs 2020-08-11 16:20:56 -07:00
Megan Marsh 3a1fc8c6f4 Merge pull request #9749 from chrisgilmerproj/cg_timing
Pretty print build durations
2020-08-11 15:45:01 -07:00
Chris Gilmer 6e918c4bd3 Update test for builder output 2020-08-11 15:25:33 -07:00
Megan Marsh 5016df92b8 fix vendoring 2020-08-11 15:04:47 -07:00
Chris Gilmer 04f952a2f4 Pretty print build durations 2020-08-11 15:04:42 -07:00
Wilken Rivera aed97e1a8b Ensure any active leases are brokend before deleting 2020-08-11 16:41:28 -04:00
Wilken Rivera 920da9d0a4 Fix up code to remove some additional steps 2020-08-11 16:40:51 -04:00
Megan Marsh 703023d47e update changelog 2020-08-11 08:34:45 -07:00
Javier Ruiz Jiménez 13d1b50d8a Remove double-quoted in variable type (#9747)
The type parameter in a variable definition can't be double-quoted as it is a primitive type keyword or a complex type constructor
2020-08-11 16:33:02 +02:00
Megan Marsh 2ffbe8240f document that interactive login is not an option for gov clouds (#9744) 2020-08-11 15:55:24 +02:00
jsmcnair 8666ebceca Fix broken header 2020-08-11 09:48:00 +01:00
jsmcnair 33e3f62ff7 Add vault function for HCL2 and document 2020-08-11 09:18:15 +01:00
Megan Marsh 027eb40a97 soften the declaration that vmware and virtualbox require bare metal. (#9745) 2020-08-10 20:09:57 -04:00
Megan Marsh 676c3a21bf Merge pull request #9743 from hashicorp/document_9168
add docs note about quoting string variables on the command line
2020-08-10 14:51:56 -07:00
Megan Marsh 4c3912bfdc add docs note about quoting string variables on the command line 2020-08-10 14:07:20 -07:00
Megan Marsh c3ba818893 Merge pull request #9741 from hashicorp/on_error_docs
add docs to make it explicit that on-error=abort etc are only relevan…
2020-08-10 13:57:34 -07:00
Megan Marsh 05c46635a3 Merge pull request #9742 from hashicorp/document_4658
document vsphere role permissions needed to run post-processors.
2020-08-10 13:57:19 -07:00
Megan Marsh f8bb9008c0 Merge pull request #9697 from backerman/personal/backerman/9695
Add FreeBSD support to azure/chroot
2020-08-10 12:47:48 -07:00
Megan Marsh 5f119a5464 document vsphere role permissions needed to run post-processors. 2020-08-10 11:48:18 -07:00
Megan Marsh 0f399ace57 add docs to make it explicit that on-error=abort etc are only relevant to provisioners, not post-processors 2020-08-10 11:10:37 -07:00
Wilken Rivera 7c28d5590c Remove StepDeleteResourceGroup in favor of StepDeploymentTemplate#Cleanup 2020-08-10 12:57:11 -04:00
Megan Marsh c0757a1bba Merge pull request #9738 from hashicorp/community_pp
Add community supported post-processors
2020-08-10 09:24:04 -07:00
Megan Marsh d3f48622a3 Merge pull request #9726 from acornies/feature/salt-masterless-formulas
Feature: salt-masterless formulas
2020-08-10 09:19:49 -07:00
Sylvia Moss 80960d7a64 Fix vsphere boot command and check for context cancelation when waiting for ip (#9739) 2020-08-10 18:16:19 +02:00
Andrew Cornies bde75c9a96 Updated implementation to go-getter/v2
go.mod update and go mod vendor

Signed-off-by: Andrew Cornies <acornies@gmail.com>
2020-08-10 09:41:44 -04:00
sylviamoss a447d151bb add community supported post-processor 2020-08-10 14:09:58 +02:00
Megan Marsh d826711e7a File provisioner docs (#9735) 2020-08-10 13:15:27 +02:00
Megan Marsh daccfc42cf Fix ReregisterVM to default to true instead of false. (#9736) 2020-08-10 13:13:14 +02:00
Brad Ackerman c43d99ccfd Add tests for StepCopyFiles. 2020-08-09 15:44:50 -07:00
Brad Ackerman 6cb0a381a4 Remove removed extraneous import from vendor dir. 2020-08-09 14:41:31 -07:00
Brad Ackerman 23d5432333 Revert inadvertently-changed import. 2020-08-09 14:31:54 -07:00
Brad Ackerman 109b2a0ca9 Build constraints don't seem to work on test. 2020-08-09 14:12:58 -07:00
Brad Ackerman 94c9b87431 Only run on platforms supported by chroot. 2020-08-09 13:53:21 -07:00
Brad Ackerman 33eb8483ba Get some tests in here. 2020-08-09 13:39:29 -07:00
Brad Ackerman f824cbb79d Don't deprovision in chroot. 2020-08-08 18:44:39 -07:00
Brad Ackerman 67479f0b23 Add FreeBSD example for azure-chroot. 2020-08-08 18:06:05 -07:00
Andrew Cornies a704e1b6b1 Updated formula regexp to support ?ref= syntax.
Added formulas docs for salt-masterless provisioner

Signed-off-by: Andrew Cornies <acornies@gmail.com>
2020-08-07 18:38:41 -04:00
Megan Marsh b72a75278a Merge pull request #9737 from hashicorp/document_5049
add microsoft daemon docs to hyperv docs
2020-08-07 15:08:03 -07:00
Wilken Rivera ae23f891e1 post-processor/googlecompute-export: Update documentation (#9727)
* Add markdown generator to post-processor config
* Add HCL2 example
2020-08-07 16:34:38 -04:00
Megan Marsh 69d951d34f add microsoft daemon docs to hyperv docs 2020-08-07 12:41:59 -07:00
Megan Marsh 15a008a408 Merge pull request #9734 from hashicorp/googlecompute-export-hcl-iap-fix
post-processor/googlecompute-export: Remove IAP configuration from HCL schema
2020-08-07 11:07:11 -07:00
Megan Marsh ecb825ee7a update vendor directory 2020-08-07 10:58:15 -07:00
Megan Marsh 03220c0b94 fix linting. 2020-08-07 10:53:07 -07:00
Megan Marsh bf7cc2587e update changelog 2020-08-07 10:00:55 -07:00
Megan Marsh 4c627c57ff Merge pull request #9720 from hashicorp/skip_pp_on_cancel
Skip post-processors when build is cancelled
2020-08-07 09:13:37 -07:00
Megan Marsh fc8346bd27 Merge pull request #9731 from hashicorp/manifast_doc_note
Add keep_input_artifact note to the manifest post-processor
2020-08-07 09:12:41 -07:00
Megan Marsh 46364ddf34 Merge pull request #9730 from hashicorp/better_errors_9705
improve error handling when waiting for image so that the resourcenot…
2020-08-07 09:11:39 -07:00
Megan Marsh 97acdf2374 Merge pull request #9725 from maxim-lobanov/file-provisioner-sources
Bug: "file" provisioner only uses first entry from Sources if processing directories
2020-08-07 08:06:35 -07:00
Megan Marsh 8f362656ee Update builder/amazon/ebs/step_create_ami.go
Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>
2020-08-07 08:02:57 -07:00
Megan Marsh 8ea6414969 Merge pull request #9729 from hashicorp/document_9125
document issue with ansible macros
2020-08-07 08:02:00 -07:00
Wilken Rivera 759d2451a4 post-processor/googlecompute-export: Remove IAP configuration from HCL schema
Build results before change
```
[go-1.14.2] [2] wilken@automaton in
~/Development/packer-templates/googlecompute/googlecompute-startup-scripts/
on master
⇶  packer build -var project=$GOOGLE_PROJECT_ID
googlecompute-ubuntu-startup-script-export.pkr.hcl
Error: Failed preparing post-processor-block "googlecompute-export" ""

  on googlecompute-ubuntu-startup-script-export.pkr.hcl line 28:
    (source code not available)

    1 error occurred:
            * unknown configuration key: '"iap"'

==> Builds finished but no artifacts were created.
```

Build results after change
```
⇶  packer build -var project=$GOOGLE_PROJECT_ID
googlecompute-ubuntu-startup-script-export.pkr.hcl
googlecompute.example: output will be in this color.

==> googlecompute.example: Checking image does not exist...
==> googlecompute.example: Creating temporary SSH key for instance...
```
2020-08-07 11:00:07 -04:00
Megan Marsh 643d6d165b Merge pull request #9728 from hashicorp/fix_9722
fix generator to work even in nested packer/packer dirs
2020-08-07 07:25:56 -07:00
sylviamoss ddd85da1a6 add keep_input_artifact note 2020-08-07 09:00:34 +02:00
Maxim Lobanov dd2927f871 fix bug with copying a few files in order 2020-08-07 09:38:30 +03:00
Megan Marsh 484dbfe6ad improve error handling when waiting for image so that the resourcenotready error is bubbled up more clearly without verbose logging turned on 2020-08-06 14:58:05 -07:00
Megan Marsh 961d9b4f08 document issue with ansible macros 2020-08-06 14:00:26 -07:00
Megan Marsh fa163c2514 fix generator to work even in nested packer/packer dirs 2020-08-06 13:24:15 -07:00
Luke Swithenbank 6d1a146143 Fix Google Compute Export Post-Processor (#9708)
* Fix Google Compute Export Post-Processor

The current Post-Processor hangs on waiting for the Startup Script to finish. The startup script doesn't update the metadata (StartupScriptStatusKey) so this change aims to fix that but adding the `SetMetadata` functionality to this script.

* Update startup.go
2020-08-06 16:18:23 -04:00
Andrew Cornies 24739270cf Added Prepare tests around formula URLs
Signed-off-by: Andrew Cornies <acornies@gmail.com>
2020-08-06 14:48:25 -04:00
Maxim Lobanov cf999e07db fix formatting 2020-08-06 21:14:53 +03:00
Maxim Lobanov cdd33d3ff2 proceed with next files when copy directory 2020-08-06 21:04:00 +03:00
Andrew Cornies a2ea308881 URL format error checking
Signed-off-by: Andrew Cornies <acornies@gmail.com>
2020-08-06 14:02:45 -04:00
Megan Marsh 04d7a10aa0 Merge pull request #9714 from hashicorp/fix_9688
provide more helpful error message than the one returned by the clien…
2020-08-06 10:54:21 -07:00
Sylvia Moss 78ce1dbde9 Add manifest hcl2 examples (#9721) 2020-08-06 13:43:35 -04:00
Megan Marsh 4964c54f60 update docs to state resource id must be fully qualified 2020-08-06 10:41:18 -07:00
sylviamoss 6d0b5a6957 skip post-processors when build is cancelled 2020-08-06 14:40:02 +02:00
Megan Marsh 25f2ec48d3 add network_name option to vmware builder to make it match the network option on the vsphere builder, so vmware users needn't update vmx data just for the network name (#9718) 2020-08-06 13:19:55 +02:00
Megan Marsh 09852b89a7 make note about deprecation warning (#9717) 2020-08-06 13:14:30 +02:00
Andrew Cornies ab52d83fb0 Functional download of formulas using gogetter
Signed-off-by: Andrew Cornies <acornies@gmail.com>
2020-08-06 01:22:19 -04:00
Megan Marsh de5c5873a2 Merge pull request #9715 from hashicorp/checksum_pp_hcl_example
Update checksum post-processor docs with hcl2 example
2020-08-05 11:07:21 -07:00
Megan Marsh 3f61498704 Merge pull request #9704 from ntoofu/f-vsphere-export-permission
Add `directory_permission` config option to `vsphere` builder
2020-08-05 10:57:30 -07:00
Megan Marsh 171d725425 Merge pull request #9707 from hashicorp/fix_9471
update docs for command and make sure they get generated properly so …
2020-08-05 10:49:16 -07:00
sylviamoss fd683daa22 update checksum post-processor docs 2020-08-05 19:46:17 +02:00
Megan Marsh b52143f528 update docs for command and make sure they get generated properly so they appear in the docs 2020-08-05 10:30:16 -07:00
Sylvia Moss dcacf7ad4c Add vSphere HCL examples to the docs (#9674) 2020-08-05 19:23:52 +02:00
Megan Marsh aede34079d provide more helpful error message than the one returned by the client, without context 2020-08-05 09:52:11 -07:00
ntoofu a53e81d9a2 Make docs more clear
https://github.com/hashicorp/packer/pull/9704#discussion_r465393240
2020-08-06 01:11:02 +09:00
Sylvia Moss 9181a435aa Make max_retries a string to allow variable interpolation (#9673) 2020-08-05 17:41:20 +02:00
jhawk28 6b8d086e94 alt, ctrl, and shift should be outside variables (#9702) 2020-08-05 17:36:40 +02:00
Megan Marsh 825deef43d Merge pull request #9665 from hashicorp/vsphere_clone_customization
Add step to customize cloned VM
2020-08-05 08:30:31 -07:00
Sylvia Moss dd37a10355 fix configuration_parameters (#9713) 2020-08-05 16:57:12 +02:00
Megan Marsh f295b6d2e5 change default target to just make the dev binaries (#9706) 2020-08-05 13:32:24 +02:00
Megan Marsh 6a8f49ad1d Merge pull request #9698 from VladRassokhin/fixup-8883
Fixup for #8883: interpolate in file provisioner
2020-08-04 17:19:12 -07:00
Megan Marsh c63e260d7d Merge pull request #9696 from jfpanisset/remove_iso_checksum_type
Remove iso_checksum_type from documentation
2020-08-04 16:24:15 -07:00
Vladislav Rassokhin 2ac9f5a276 Fixup for #8883: interpolate in file provisioner
* `UploadDir` should receive interpolated `Destination`
* Render `Destination` only once
2020-08-02 21:09:47 +03:00
ntoofu 5493285f65 Add directory_permission config option to vsphere builder 2020-08-02 15:02:31 +09:00
Brad Ackerman 133c8a68b0 Add git mod vendor changes. 2020-08-01 22:24:56 -07:00
Brad Ackerman fb1a4bd86f Add placeholder diskattacher for other OSes. 2020-08-01 21:59:43 -07:00
Brad Ackerman 5ce6c8564c Fix ordering to match master for better diffs. 2020-08-01 21:30:44 -07:00
Brad Ackerman 13a4e47efe Increase wait time for device to be created. 2020-08-01 21:22:50 -07:00
Brad Ackerman 364c415294 Add FreeBSD support. 2020-08-01 21:22:50 -07:00
Jean-Francois Panisset 3ce148110c iso_checksum_type was removed from 1.6.0, checksum type is now
inferred from iso_checksum string length or prefix.

Signed-off-by: Jean-Francois Panisset <panisset@gmail.com>
2020-08-01 21:02:31 -07:00
Etan Reisner a28edbaa0b Fix skip_save_build_region in the ebssurrogate builder. (#9666) 2020-07-31 16:01:46 -04:00
sylviamoss c6e9bde97b add step customize and docs 2020-07-29 15:07:44 +02:00
299 changed files with 10195 additions and 2464 deletions
+1
View File
@@ -8,6 +8,7 @@ jobs:
steps:
- name: Add track-internal
uses: andymckay/labeler@1.0.2
if: github.event.issue.pull_request == null
with:
repo-token: ${{ secrets.Github_Token }}
add-labels: "track-internal"
+62
View File
@@ -1,5 +1,67 @@
## 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)
### BACKWARDS INCOMPATABILITIES:
+1 -1
View File
@@ -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 \
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
+14 -3
View File
@@ -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)
generatedData := []string{"SourceAMIName", "Device", "MountPath"}
generatedData := awscommon.GetGeneratedDataList()
generatedData = append(generatedData, "Device", "MountPath")
return generatedData, warns, nil
}
@@ -396,12 +398,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
GeneratedData: generatedData,
},
&StepCreateVolume{
PollingConfig: b.config.PollingConfig,
RootVolumeType: b.config.RootVolumeType,
RootVolumeSize: b.config.RootVolumeSize,
RootVolumeTags: b.config.RootVolumeTags,
Ctx: b.config.ctx,
},
&StepAttachVolume{},
&StepAttachVolume{
PollingConfig: b.config.PollingConfig,
},
&StepEarlyUnflock{},
&chroot.StepPreMountCommands{
Commands: b.config.PreMountCommands,
@@ -420,9 +425,14 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&chroot.StepCopyFiles{
Files: b.config.CopyFiles,
},
&awscommon.StepSetGeneratedData{
GeneratedData: generatedData,
},
&chroot.StepChrootProvision{},
&chroot.StepEarlyCleanup{},
&StepSnapshot{},
&StepSnapshot{
PollingConfig: b.config.PollingConfig,
},
&awscommon.StepDeregisterAMI{
AccessConfig: &b.config.AccessConfig,
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,
EnableAMIENASupport: b.config.AMIENASupport,
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
PollingConfig: b.config.PollingConfig,
},
&awscommon.StepAMIRegionCopy{
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"`
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"`
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"`
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"`
@@ -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},
"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())},
"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())},
"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},
+17 -2
View File
@@ -227,10 +227,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
if generatedData[0] != "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")
}
if generatedData[2] != "MountPath" {
if generatedData[7] != "MountPath" {
t.Fatalf("Generated data should contain MountPath")
}
}
+5 -4
View File
@@ -19,8 +19,9 @@ import (
// device string - The location where the volume was attached.
// attach_cleanup CleanupFunc
type StepAttachVolume struct {
attached bool
volumeId string
PollingConfig *awscommon.AWSPollingConfig
attached bool
volumeId string
}
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
// 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 {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
@@ -87,7 +88,7 @@ func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
s.attached = false
// 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 {
return fmt.Errorf("Error waiting for volume: %s", err)
}
+2 -1
View File
@@ -20,6 +20,7 @@ import (
// Produces:
// volume_id string - The ID of the created volume
type StepCreateVolume struct {
PollingConfig *awscommon.AWSPollingConfig
volumeId string
RootVolumeSize int64
RootVolumeType string
@@ -110,7 +111,7 @@ func (s *StepCreateVolume) Run(ctx context.Context, state multistep.StateBag) mu
log.Printf("Volume ID: %s", s.volumeId)
// 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 {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
+2 -1
View File
@@ -15,6 +15,7 @@ import (
// StepRegisterAMI creates the AMI.
type StepRegisterAMI struct {
PollingConfig *awscommon.AWSPollingConfig
RootVolumeSize int64
EnableAMIENASupport confighelper.Trilean
EnableAMISriovNetSupport bool
@@ -81,7 +82,7 @@ func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) mul
state.Put("amis", amis)
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)
state.Put("error", err)
ui.Error(err.Error())
+3 -2
View File
@@ -16,7 +16,8 @@ import (
// Produces:
// snapshot_id string - ID of the created snapshot
type StepSnapshot struct {
snapshotId string
PollingConfig *awscommon.AWSPollingConfig
snapshotId string
}
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))
// 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 {
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state.Put("error", err)
+8 -1
View File
@@ -138,6 +138,9 @@ type AccessConfig struct {
// }
// ```
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
}
@@ -215,7 +218,6 @@ func (c *AccessConfig) Session() (*session.Session, error) {
if c.DecodeAuthZMessages {
DecodeAuthZMessages(c.session)
}
LogEnvOverrideWarnings()
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."))
}
if c.PollingConfig == nil {
c.PollingConfig = new(AWSPollingConfig)
}
c.PollingConfig.LogEnvOverrideWarnings()
return errs
}
@@ -13,6 +13,7 @@ func testAccessConfig() *AccessConfig {
getEC2Connection: func() ec2iface.EC2API {
return &mockEC2Client{}
},
PollingConfig: new(AWSPollingConfig),
}
}
+21 -7
View File
@@ -79,18 +79,32 @@ type AMIConfig struct {
// Default false.
AMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot" required:"false"`
// Whether or not to encrypt the resulting AMI when
// copying a provisioned instance to an AMI. By default, Packer will keep the
// encryption setting to what it was in the source image. Setting false will
// result in an unencrypted image, and true will result in an encrypted one.
// copying a provisioned instance to an AMI. By default, Packer will keep
// the encryption setting to what it was in the source image. Setting false
// 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
// 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
// prevent an unnecessary extra copy step and save you some time.
AMIEncryptBootVolume config.Trilean `mapstructure:"encrypt_boot" required:"false"`
// ID, alias or ARN of the KMS key to use for boot volume encryption. This
// only applies to the main `region`, other regions where the AMI will be
// copied will be encrypted by the default EBS KMS key. For valid formats
// see *KmsKeyId* in the [AWS API docs -
// ID, alias or ARN of the KMS key to use for AMI encryption. This
// only applies to the main `region` -- any regions the AMI gets copied to
// copied will be encrypted by the default EBS KMS key for that region,
// 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).
// This field is validated by Packer, when using an alias, you will have to
// prefix `kms_key_id` with `alias/`.
+13 -8
View File
@@ -13,7 +13,7 @@ import (
"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.
//
// Example use case:
@@ -24,7 +24,7 @@ import (
// JSON example:
//
// ```json
// ami_block_device_mappings: [
// launch_block_device_mappings: [
// {
// "device_name": "/dev/sda1",
// "encrypted": true,
@@ -36,13 +36,16 @@ import (
// HCL2 example:
//
// ```hcl
// ami_block_device_mappings {
// launch_block_device_mappings {
// device_name = "/dev/sda1"
// encrypted = true
// 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:
// 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
// snapshot_id.
VolumeSize int64 `mapstructure:"volume_size" required:"false"`
// ID, alias or ARN of the KMS key to use for boot volume encryption. This
// only applies to the main region, other regions where the AMI will be
// copied will be encrypted by the default EBS KMS key. For valid formats
// see KmsKeyId in the [AWS API docs -
// ID, alias or ARN of the KMS key to use for boot volume encryption.
// This option exists for launch_block_device_mappings but not
// ami_block_device_mappings. The kms key id defined here only applies to
// 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)
// 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/.
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),
SourceAMITags: sourceAMITags,
}
generatedData.Put("BuildRegion", buildInfoTemplate.BuildRegion)
generatedData.Put("SourceAMI", buildInfoTemplate.SourceAMI)
generatedData.Put("SourceAMICreationDate", buildInfoTemplate.SourceAMICreationDate)
generatedData.Put("SourceAMIName", buildInfoTemplate.SourceAMIName)
generatedData.Put("SourceAMIOwner", buildInfoTemplate.SourceAMIOwner)
generatedData.Put("SourceAMIOwnerName", buildInfoTemplate.SourceAMIOwnerName)
return buildInfoTemplate
}
func GetGeneratedDataList() []string {
return []string{
"SourceAMIName",
"BuildRegion",
"SourceAMI",
"SourceAMICreationDate",
"SourceAMIOwner",
"SourceAMIOwnerName",
}
}
+82 -33
View File
@@ -1,3 +1,5 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type AWSPollingConfig
package common
import (
@@ -38,12 +40,45 @@ type StateChangeConf struct {
// Following are wrapper functions that use Packer's environment-variables to
// 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{
ImageIds: []*string{&imageId},
}
waitOpts := getWaiterOptions()
waitOpts := w.getWaiterOptions()
if len(waitOpts) == 0 {
// Bump this default to 30 minutes because the aws default
// 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
}
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{
InstanceIds: []*string{&instanceId},
@@ -75,12 +110,11 @@ func WaitUntilInstanceRunning(ctx aws.Context, conn *ec2.EC2, instanceId string)
err := conn.WaitUntilInstanceRunningWithContext(
ctx,
&instanceInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
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{
InstanceIds: []*string{&instanceId},
}
@@ -88,12 +122,12 @@ func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId stri
err := conn.WaitUntilInstanceTerminatedWithContext(
ctx,
&instanceInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
return err
}
// 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{
SpotInstanceRequestIds: []*string{&spotRequestId},
}
@@ -101,11 +135,11 @@ func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId
err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext(
ctx,
&spotRequestInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
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{
VolumeIds: []*string{&volumeId},
}
@@ -113,11 +147,11 @@ func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) e
err := conn.WaitUntilVolumeAvailableWithContext(
ctx,
&volumeInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
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{
SnapshotIds: []*string{&snapshotID},
}
@@ -125,13 +159,13 @@ func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) er
err := conn.WaitUntilSnapshotCompletedWithContext(
ctx,
&snapInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
return err
}
// 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{
VolumeIds: []*string{&volumeId},
}
@@ -139,11 +173,11 @@ func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) er
err := WaitForVolumeToBeAttached(conn,
ctx,
&volumeInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
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{
VolumeIds: []*string{&volumeId},
}
@@ -151,11 +185,11 @@ func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) er
err := WaitForVolumeToBeDetached(conn,
ctx,
&volumeInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
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{
ImportTaskIds: []*string{&taskID},
}
@@ -163,7 +197,7 @@ func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error
err := WaitForImageToBeImported(conn,
ctx,
&importInput,
getWaiterOptions()...)
w.getWaiterOptions()...)
return err
}
@@ -298,8 +332,18 @@ type overridableWaitVars struct {
awsTimeoutSeconds envInfo
}
func getWaiterOptions() []request.WaiterOption {
func (w *AWSPollingConfig) getWaiterOptions() []request.WaiterOption {
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)
return waitOpts
}
@@ -333,33 +377,38 @@ func getEnvOverrides() overridableWaitVars {
return envValues
}
func LogEnvOverrideWarnings() {
pollDelay := os.Getenv("AWS_POLL_DELAY_SECONDS")
timeoutSeconds := os.Getenv("AWS_TIMEOUT_SECONDS")
maxAttempts := os.Getenv("AWS_MAX_ATTEMPTS")
func (w *AWSPollingConfig) LogEnvOverrideWarnings() {
pollDelayEnv := os.Getenv("AWS_POLL_DELAY_SECONDS")
timeoutSecondsEnv := os.Getenv("AWS_TIMEOUT_SECONDS")
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 " +
"AWS_TIMEOUT_SECONDS are both set. Packer will use " +
"AWS_MAX_ATTEMPTS and discard AWS_TIMEOUT_SECONDS.")
if pollDelay == "" {
if !pollDelayIsSet {
warning = fmt.Sprintf("%s Since you have not set the poll delay, "+
"Packer will default to a 2-second delay.", warning)
}
log.Printf(warning)
} else if timeoutSeconds != "" {
} else if timeoutSecondsIsSet {
log.Printf("[WARNING] (aws): env var AWS_TIMEOUT_SECONDS is " +
"deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " +
"explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " +
"poll delay of 2 seconds, regardless of the AWS waiter's default.")
"deprecated in favor of AWS_MAX_ATTEMPTS env or aws_polling_max_attempts config option. " +
"If you have not explicitly set AWS_POLL_DELAY_SECONDS env or aws_polling_delay_seconds config option, " +
"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. " +
"Packer will default to waiter-specific delays and timeouts. If you would " +
"like to customize the length of time between retries and max " +
"number of retries you may do so by setting the environment " +
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " +
"desired values.")
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS or the " +
"configuration options aws_polling_delay_seconds and aws_polling_max_attempts " +
"to your desired values.")
}
}
+32
View File
@@ -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
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",
*resp.ImageId, target, err)
}
@@ -19,6 +19,7 @@ import (
)
type StepRunSourceInstance struct {
PollingConfig *AWSPollingConfig
AssociatePublicIpAddress bool
LaunchMappings EC2BlockDeviceMappingsBuilder
Comm *communicator.Config
@@ -234,7 +235,7 @@ func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBa
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)
state.Put("error", err)
ui.Error(err.Error())
@@ -364,7 +365,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
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())
}
}
@@ -23,6 +23,7 @@ type EC2BlockDeviceMappingsBuilder interface {
}
type StepRunSpotInstance struct {
PollingConfig *AWSPollingConfig
AssociatePublicIpAddress bool
LaunchMappings EC2BlockDeviceMappingsBuilder
BlockDurationMinutes int64
@@ -466,7 +467,7 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
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())
}
}
@@ -29,6 +29,7 @@ func tStateSpot() multistep.StateBag {
func getBasicStep() *StepRunSpotInstance {
stepRunSpotInstance := StepRunSpotInstance{
PollingConfig: new(AWSPollingConfig),
AssociatePublicIpAddress: false,
LaunchMappings: BlockDevices{},
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 {
PollingConfig *AWSPollingConfig
Skip bool
DisableStopInstance bool
}
@@ -74,7 +75,7 @@ func (s *StepStopEBSBackedInstance) Run(ctx context.Context, state multistep.Sta
&ec2.DescribeInstancesInput{
InstanceIds: []*string{instance.InstanceId},
},
getWaiterOptions()...)
s.PollingConfig.getWaiterOptions()...)
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
+8 -1
View File
@@ -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)
generatedData := []string{"SourceAMIName"}
generatedData := awscommon.GetGeneratedDataList()
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() {
instanceStep = &awscommon.StepRunSpotInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
BlockDurationMinutes: b.config.BlockDurationMinutes,
@@ -191,6 +192,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
} else {
instanceStep = &awscommon.StepRunSourceInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
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(),
},
&awscommon.StepSetGeneratedData{
GeneratedData: generatedData,
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.RunConfig.Comm,
},
&awscommon.StepStopEBSBackedInstance{
PollingConfig: b.config.PollingConfig,
Skip: b.config.IsSpotInstance(),
DisableStopInstance: b.config.DisableStopInstance,
},
@@ -305,6 +311,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
},
&stepCreateAMI{
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
PollingConfig: b.config.PollingConfig,
},
&awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig,
+2
View File
@@ -31,6 +31,7 @@ type FlatConfig struct {
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"`
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"`
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"`
@@ -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},
"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())},
"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_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},
+18
View File
@@ -144,7 +144,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if generatedData[0] != "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")
}
}
+11 -5
View File
@@ -16,6 +16,7 @@ import (
)
type stepCreateAMI struct {
PollingConfig *awscommon.AWSPollingConfig
image *ec2.Image
AMISkipBuildRegion bool
}
@@ -85,18 +86,23 @@ func (s *stepCreateAMI) Run(ctx context.Context, state multistep.StateBag) multi
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); err != nil {
log.Printf("Error waiting for AMI: %s", err)
if waitErr := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); waitErr != nil {
// 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}})
if imerr != nil {
log.Printf("Unable to determine reason waiting for AMI failed: %s", err)
err = fmt.Errorf("Unknown error waiting for AMI; %s", err)
// If there's a failure describing images, bubble that error up too, but don't erase the waitErr.
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 {
// Finally, if there's a stateReason, store that with the wait err
image := imResp.Images[0]
if image != nil {
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)
+23 -13
View File
@@ -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)
generatedData := []string{"SourceAMIName"}
generatedData := awscommon.GetGeneratedDataList()
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() {
instanceStep = &awscommon.StepRunSpotInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
BlockDurationMinutes: b.config.BlockDurationMinutes,
@@ -213,6 +214,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
} else {
instanceStep = &awscommon.StepRunSourceInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
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
steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
VpcId: b.config.VpcId,
SubnetId: b.config.SubnetId,
HasSubnetFilter: !b.config.SubnetFilter.Empty(),
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
VpcId: b.config.VpcId,
SubnetId: b.config.SubnetId,
HasSubnetFilter: !b.config.SubnetFilter.Empty(),
},
&awscommon.StepSourceAMIInfo{
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(),
},
&awscommon.StepSetGeneratedData{
GeneratedData: generatedData,
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.RunConfig.Comm,
},
&awscommon.StepStopEBSBackedInstance{
PollingConfig: b.config.PollingConfig,
Skip: b.config.IsSpotInstance(),
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,
},
&StepSnapshotVolumes{
PollingConfig: b.config.PollingConfig,
LaunchDevices: launchDevices,
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,
LaunchOmitMap: b.config.LaunchMappings.GetOmissions(),
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
PollingConfig: b.config.PollingConfig,
},
&awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
AMIKmsKeyId: b.config.AMIKmsKeyId,
RegionKeyIds: b.config.AMIRegionKMSKeyIDs,
EncryptBootVolume: b.config.AMIEncryptBootVolume,
Name: b.config.AMIName,
OriginalRegion: *ec2conn.Config.Region,
AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
AMIKmsKeyId: b.config.AMIKmsKeyId,
RegionKeyIds: b.config.AMIRegionKMSKeyIDs,
EncryptBootVolume: b.config.AMIEncryptBootVolume,
Name: b.config.AMIName,
OriginalRegion: *ec2conn.Config.Region,
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
},
&awscommon.StepModifyAMIAttributes{
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"`
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"`
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"`
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"`
@@ -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},
"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())},
"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},
"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},
@@ -86,7 +86,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if generatedData[0] != "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.
type StepRegisterAMI struct {
PollingConfig *awscommon.AWSPollingConfig
RootDevice RootBlockDevice
AMIDevices []*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
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)
state.Put("error", err)
ui.Error(err.Error())
@@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/builder/amazon/common"
)
const sourceDeviceName = "/dev/xvdf"
@@ -23,6 +24,7 @@ func newStepRegisterAMI(amiDevices, launchDevices []*ec2.BlockDeviceMapping) *St
},
AMIDevices: amiDevices,
LaunchDevices: launchDevices,
PollingConfig: new(common.AWSPollingConfig),
}
}
@@ -18,6 +18,7 @@ import (
// Produces:
// snapshot_ids map[string]string - IDs of the created snapshots
type StepSnapshotVolumes struct {
PollingConfig *awscommon.AWSPollingConfig
LaunchDevices []*ec2.BlockDeviceMapping
snapshotIds map[string]string
snapshotMutex sync.Mutex
@@ -56,7 +57,7 @@ func (s *StepSnapshotVolumes) snapshotVolume(ctx context.Context, deviceName str
s.snapshotMutex.Unlock()
// Wait for snapshot to be created
err = awscommon.WaitUntilSnapshotDone(ctx, ec2conn, *createSnapResp.SnapshotId)
err = s.PollingConfig.WaitUntilSnapshotDone(ctx, ec2conn, *createSnapResp.SnapshotId)
return err
}
+9 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/builder"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/common"
"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)
generatedData := []string{"SourceAMIName"}
generatedData := awscommon.GetGeneratedDataList()
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("hook", hook)
state.Put("ui", ui)
generatedData := &builder.GeneratedData{State: state}
var instanceStep multistep.Step
if b.config.IsSpotInstance() {
instanceStep = &awscommon.StepRunSpotInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.launchBlockDevices,
BlockDurationMinutes: b.config.BlockDurationMinutes,
@@ -192,6 +195,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
} else {
instanceStep = &awscommon.StepRunSourceInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.launchBlockDevices,
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(),
},
&awscommon.StepSetGeneratedData{
GeneratedData: generatedData,
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.RunConfig.Comm,
},
&awscommon.StepStopEBSBackedInstance{
PollingConfig: b.config.PollingConfig,
Skip: b.config.IsSpotInstance(),
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"`
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"`
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"`
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"`
@@ -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},
"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())},
"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},
"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},
+18
View File
@@ -105,7 +105,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if generatedData[0] != "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")
}
}
+7 -1
View File
@@ -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)
generatedData := []string{"SourceAMIName"}
generatedData := awscommon.GetGeneratedDataList()
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() {
instanceStep = &awscommon.StepRunSpotInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
BlockDurationMinutes: b.config.BlockDurationMinutes,
@@ -273,6 +274,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
} else {
instanceStep = &awscommon.StepRunSourceInstance{
PollingConfig: b.config.PollingConfig,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
LaunchMappings: b.config.LaunchMappings,
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(),
},
&awscommon.StepSetGeneratedData{
GeneratedData: generatedData,
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
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,
EnableAMIENASupport: b.config.AMIENASupport,
AMISkipBuildRegion: b.config.AMISkipBuildRegion,
PollingConfig: b.config.PollingConfig,
},
&awscommon.StepAMIRegionCopy{
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"`
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"`
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"`
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"`
@@ -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},
"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())},
"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_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},
+18
View File
@@ -319,7 +319,25 @@ func TestBuilderPrepare_ReturnGeneratedData(t *testing.T) {
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if len(generatedData) == 0 {
t.Fatalf("Generated data should not be empty")
}
if generatedData[0] != "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")
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ import (
)
type StepRegisterAMI struct {
PollingConfig *awscommon.AWSPollingConfig
EnableAMIENASupport confighelper.Trilean
EnableAMISriovNetSupport 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
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)
state.Put("error", err)
ui.Error(err.Error())
-4
View File
@@ -221,8 +221,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
NewStepDeleteResourceGroup(azureClient, ui),
NewStepDeleteOSDisk(azureClient, ui),
NewStepDeleteAdditionalDisks(azureClient, ui),
}
} 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),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
NewStepDeleteResourceGroup(azureClient, ui),
NewStepDeleteOSDisk(azureClient, ui),
NewStepDeleteAdditionalDisks(azureClient, ui),
)
} else {
+6 -1
View File
@@ -103,7 +103,8 @@ type Config struct {
// Authentication via OAUTH
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)
// 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)
@@ -787,6 +788,10 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
for _, rid := range c.UserAssignedManagedIdentities {
r, err := client.ParseResourceID(rid)
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)
} else {
if !strings.EqualFold(r.Provider, "Microsoft.ManagedIdentity") {
+23 -20
View File
@@ -111,27 +111,30 @@ func (s *StepCreateResourceGroup) Cleanup(state multistep.StateBag) {
if state.Get(constants.ArmIsExistingResourceGroup).(bool) {
ui.Say("\nThe resource group was not created by Packer, not deleting ...")
return
} else {
ui.Say("\nCleanup requested, deleting resource group ...")
}
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
ctx := context.TODO()
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
if err == nil {
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 {
err = f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
}
}
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.")
ctx := context.TODO()
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
if exists, err := s.exists(ctx, resourceGroupName); !exists || err != nil {
return
}
ui.Say("\nCleanup requested, deleting resource group ...")
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
if err == nil {
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 {
err = f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
}
}
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 {
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 {
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!")
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 {
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)
}
var storageAccountName = xs[1]
var blobName = strings.Join(xs[2:], "/")
xs := strings.Split(u.Path, "/")
if len(xs) < 3 {
err = errors.New("Failed to parse Additional Disk's VHD URI!")
} else {
var storageAccountName = xs[1]
var blobName = strings.Join(xs[2:], "/")
err = s.delete(storageAccountName, blobName)
}
if err != nil {
return processStepResult(err, s.error, state)
}
err = s.delete(storageAccountName, blobName)
}
if err != nil {
return processStepResult(err, s.error, state)
}
}
return multistep.ActionContinue
-99
View File
@@ -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
}
+122 -98
View File
@@ -42,6 +42,109 @@ func NewStepDeployTemplate(client *AzureClient, ui packer.Ui, config *Config, de
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 {
deployment, err := s.factory(s.config)
if err != nil {
@@ -62,48 +165,32 @@ func (s *StepDeployTemplate) deleteTemplate(ctx context.Context, state multistep
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var deploymentName = s.name
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)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
}
if err != nil {
s.say(s.client.LastError.Error())
}
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) {
//We can't depend on constants.ArmOSDiskVhd being set
var imageName string
var imageType string
var imageName, imageType string
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
if err != nil {
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
}
@@ -158,6 +245,7 @@ func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string,
}
return err
}
// VHD image
u, err := url.Parse(imageName)
if err != nil {
@@ -171,75 +259,11 @@ func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string,
var blobName = strings.Join(xs[2:], "/")
blob := s.client.BlobStorageClient.GetContainerReference(storageAccountName).GetBlobReference(blobName)
err = blob.Delete(nil)
return err
}
func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
defer s.deleteTemplate(context.Background(), state)
//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))
}
}
_, err = blob.BreakLease(nil)
if err != nil && !strings.Contains(err.Error(), "LeaseNotPresentWithLeaseOperation") {
s.say(s.client.LastError.Error())
return err
}
return blob.Delete(nil)
}
+5 -2
View File
@@ -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) {
if runtime.GOOS != "linux" {
return nil, errors.New("the azure-chroot builder only works on Linux environments")
switch runtime.GOOS {
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()
+2 -34
View File
@@ -3,24 +3,18 @@ package chroot
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"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/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer/builder/azure/common/client"
)
type DiskAttacher interface {
AttachDisk(ctx context.Context, disk string) (lun int32, err error)
DiskPathForLun(lun int32) string
WaitForDevice(ctx context.Context, i int32) (device string, err error)
DetachDisk(ctx context.Context, disk string) (err 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
}
func (diskAttacher) 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 := 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()
}
}
}
var DiskNotFoundError = errors.New("Disk not found")
func (da *diskAttacher) DetachDisk(ctx context.Context, diskID string) error {
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) {
dataDisks, err := da.getDisks(ctx)
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.")
}
+8 -1
View File
@@ -9,6 +9,7 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/packer/common"
@@ -62,7 +63,13 @@ func (s *StepMountDevice) Run(ctx context.Context, state multistep.StateBag) mul
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)
@@ -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, " ")
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...")
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
// resulting appliance file. Packer JSON configuration file example:
//
// In JSON:
// ```json
// {
// "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
// description](https://www.virtualbox.org/manual/ch09.html#vboxmanage-export-ovf)
// may contain arbitrary strings; the GUI interprets HTML formatting. However,
+32 -9
View File
@@ -6,17 +6,40 @@ import (
"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 {
// Custom `VBoxManage` commands to execute in order to further customize
// the virtual machine being created. The value of this is an array of
// commands to execute. The commands are executed in the order defined in
// the template. For each command, the command is defined itself as an
// array of strings, where each string represents a single argument on the
// command-line to `VBoxManage` (but excluding `VBoxManage` itself). Each
// arg is treated as a [configuration
// template](/docs/templates/engine), where the `Name` variable is
// replaced with the VM name. More details on how to use `VBoxManage` are
// below.
// the virtual machine being created. The example shown below sets the memory and number of CPUs
// within the virtual machine:
//
// In JSON:
// ```json
// "vboxmanage": [
// ["modifyvm", "{{.Name}}", "--memory", "1024"],
// ["modifyvm", "{{.Name}}", "--cpus", "2"]
// ]
// ```
//
// 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"`
// Identical to vboxmanage,
// except that it is run after the virtual machine is shutdown, and before the
+8
View File
@@ -89,12 +89,20 @@ type Config struct {
// pack](https://www.virtualbox.org/wiki/Downloads#VirtualBox6.0.14OracleVMVirtualBoxExtensionPack)
// and you will need to enable EFI mode for nvme to work, ex:
//
// In JSON:
// ```json
// "vboxmanage": [
// [ "modifyvm", "{{.Name}}", "--firmware", "EFI" ],
// ]
// ```
//
// In HCL2:
// ```hcl
// vboxmanage = [
// [ "modifyvm", "{{.Name}}", "--firmware", "EFI" ],
// ]
// ```
//
HardDriveInterface string `mapstructure:"hard_drive_interface" required:"false"`
// 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
+18 -4
View File
@@ -154,7 +154,7 @@ func (d *ESX5Driver) Stop(vmxPathLocal string) error {
func (d *ESX5Driver) Register(vmxPathLocal string) error {
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
}
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
}
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)
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
return "", err
@@ -198,7 +198,7 @@ func (d *ESX5Driver) UploadISO(localPath string, checksum string) (string, error
}
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
}
@@ -667,12 +667,26 @@ func (d *ESX5Driver) mkdir(path string) error {
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)
if err != nil {
return err
}
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)
}
+2
View File
@@ -31,6 +31,8 @@ type HWConfig struct {
// machine](https://kb.vmware.com/s/article/1001805) for desktop VMware
// clients. For ESXi, refer to the proper ESXi documentation.
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
// building the VM. Defaults to false.
Sound bool `mapstructure:"sound" required:"false"`
+32 -6
View File
@@ -10,13 +10,39 @@ import (
)
type OutputConfig struct {
// This is the path to the directory where the
// resulting virtual machine will be created. This may be relative or absolute.
// If relative, the path is relative to the working directory when packer
// is executed. 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.
// This is the path on your local machine (the one running Packer) to the
// directory where the resulting virtual machine will be created.
// This may be relative or absolute. If relative, the path is relative to
// the working directory when packer is executed.
//
// 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"`
// 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 {
+6 -2
View File
@@ -1,12 +1,16 @@
package common
import (
"github.com/hashicorp/packer/packer"
)
type RemoteDriver interface {
Driver
// 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
// 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(localPath string) error
@@ -24,7 +28,7 @@ type RemoteDriver interface {
IsDestroyed() (bool, error)
// 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(src, dst string) error
+40 -2
View File
@@ -1,5 +1,9 @@
package common
import (
"github.com/hashicorp/packer/packer"
)
type RemoteDriverMock struct {
DriverMock
@@ -29,10 +33,14 @@ type RemoteDriverMock struct {
RemovedCachePath string
CacheRemoved bool
ReturnValDirExists bool
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.UploadISOPath = path
return d.UploadISOResult, d.UploadISOErr
@@ -60,7 +68,7 @@ func (d *RemoteDriverMock) IsDestroyed() (bool, error) {
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
}
@@ -77,3 +85,33 @@ func (d *RemoteDriverMock) RemoveCache(localPath string) error {
func (d *RemoteDriverMock) ReloadVM() error {
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
}
+53 -4
View File
@@ -16,13 +16,62 @@ import (
type StepOutputDir struct {
Force bool
OutputConfig *OutputConfig
VMName string
RemoteType string
success bool
}
func (s *StepOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
dir := state.Get("dir").(OutputDir)
ui := state.Get("ui").(packer.Ui)
func (s *StepOutputDir) SetOutputAndExportDirs(state multistep.StateBag) OutputDir {
driver := state.Get("driver")
// 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()
if err != nil {
state.Put("error", err)
@@ -31,7 +80,7 @@ func (s *StepOutputDir) Run(ctx context.Context, state multistep.StateBag) multi
if exists {
if s.Force {
ui.Say("Deleting previous output directory...")
ui.Message("Deleting previous output directory...")
dir.RemoveAll()
} else {
state.Put("error", fmt.Errorf(
+97 -32
View File
@@ -9,16 +9,14 @@ import (
"github.com/hashicorp/packer/helper/multistep"
)
func testOutputDir(t *testing.T) *LocalOutputDir {
func testOutputDir(t *testing.T) string {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
os.RemoveAll(td)
result := new(LocalOutputDir)
result.SetOutputDir(td)
return result
return td
}
func TestStepOutputDir_impl(t *testing.T) {
@@ -27,12 +25,20 @@ func TestStepOutputDir_impl(t *testing.T) {
func TestStepOutputDir(t *testing.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
defer os.RemoveAll(dir.dir)
state.Put("dir", dir)
defer os.RemoveAll(td)
// Test the run
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 {
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)
}
// Test the cleanup
step.Cleanup(state)
if _, err := os.Stat(dir.dir); err != nil {
if _, err := os.Stat(td); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestStepOutputDir_existsNoForce(t *testing.T) {
state := testState(t)
step := new(StepOutputDir)
dir := testOutputDir(t)
state.Put("dir", dir)
td := testOutputDir(t)
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
if err := os.MkdirAll(dir.dir, 0755); err != nil {
if err := os.MkdirAll(td, 0755); err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir.dir)
defer os.RemoveAll(td)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
@@ -75,24 +89,33 @@ func TestStepOutputDir_existsNoForce(t *testing.T) {
// Test the cleanup
step.Cleanup(state)
if _, err := os.Stat(dir.dir); err != nil {
if _, err := os.Stat(td); err != nil {
t.Fatal("should not delete dir")
}
}
func TestStepOutputDir_existsForce(t *testing.T) {
state := testState(t)
step := new(StepOutputDir)
td := testOutputDir(t)
outconfig := &OutputConfig{
OutputDir: td,
}
step := &StepOutputDir{
OutputConfig: outconfig,
VMName: "testVM",
}
step.Force = true
dir := testOutputDir(t)
state.Put("dir", dir)
// Delete the test output directory when done
defer os.RemoveAll(td)
// 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)
}
defer os.RemoveAll(dir.dir)
defer os.RemoveAll(td)
// Test the run
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 {
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)
}
}
func TestStepOutputDir_cancel(t *testing.T) {
state := testState(t)
step := new(StepOutputDir)
td := testOutputDir(t)
outconfig := &OutputConfig{
OutputDir: td,
}
dir := testOutputDir(t)
state.Put("dir", dir)
step := &StepOutputDir{
OutputConfig: outconfig,
VMName: "testVM",
}
// Test the run
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 {
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)
}
// Test cancel/halt
state.Put(multistep.StateCancelled, true)
step.Cleanup(state)
if _, err := os.Stat(dir.dir); err == nil {
if _, err := os.Stat(td); err == nil {
t.Fatal("directory should not exist")
}
}
func TestStepOutputDir_halt(t *testing.T) {
state := testState(t)
step := new(StepOutputDir)
td := testOutputDir(t)
outconfig := &OutputConfig{
OutputDir: td,
}
dir := testOutputDir(t)
state.Put("dir", dir)
step := &StepOutputDir{
OutputConfig: outconfig,
VMName: "testVM",
}
// Test the run
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 {
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)
}
// Test cancel/halt
state.Put(multistep.StateHalted, true)
step.Cleanup(state)
if _, err := os.Stat(dir.dir); err == nil {
if _, err := os.Stat(td); err == nil {
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!")
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ func (s *StepRegister) Cleanup(state multistep.StateBag) {
}
if remoteDriver, ok := driver.(RemoteDriver); ok {
if s.SkipExport {
if s.SkipExport && !cancelled && !halted {
ui.Say("Unregistering virtual machine...")
if err := remoteDriver.Unregister(s.registeredPath); err != nil {
ui.Error(fmt.Sprintf("Error unregistering VM: %s", err))
+1 -1
View File
@@ -45,7 +45,7 @@ func (s *StepRemoteUpload) Run(ctx context.Context, state multistep.StateBag) mu
ui.Say(s.Message)
log.Printf("Remote uploading: %s", path)
newPath, err := remote.UploadISO(path, s.Checksum)
newPath, err := remote.UploadISO(path, s.Checksum, ui)
if err != nil {
err := fmt.Errorf("Error uploading file: %s", err)
state.Put("error", err)
+13 -1
View File
@@ -12,8 +12,20 @@ import (
"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 {
dir := testOutputDir(t)
dir := testLocalOutputDir(t)
if err := dir.MkdirAll(); err != nil {
t.Fatalf("err: %s", err)
}
+1 -1
View File
@@ -32,7 +32,7 @@ func (c *StepUploadVMX) Run(ctx context.Context, state multistep.StateBag) multi
remoteDriver, ok := driver.(RemoteDriver)
if ok {
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))
return multistep.ActionHalt
}
+5 -21
View File
@@ -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)
}
// 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
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("debug", b.config.PackerDebug)
state.Put("dir", dir)
state.Put("driver", driver)
state.Put("hook", hook)
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,
},
&vmwcommon.StepOutputDir{
Force: b.config.PackerForce,
Force: b.config.PackerForce,
OutputConfig: &b.config.OutputConfig,
RemoteType: b.config.RemoteType,
VMName: b.config.VMName,
},
&common.StepCreateFloppy{
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,
VMName: b.config.VMName,
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
exportOutputPath := state.Get("export_output_path").(string) // set in StepOutputDir
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
}
+4
View File
@@ -50,11 +50,13 @@ type FlatConfig struct {
CoreCount *int `mapstructure:"cores" required:"false" cty:"cores" hcl:"cores"`
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"`
NetworkName *string `mapstructure:"network_name" required:"false" cty:"network_name" hcl:"network_name"`
Sound *bool `mapstructure:"sound" required:"false" cty:"sound" hcl:"sound"`
USB *bool `mapstructure:"usb" required:"false" cty:"usb" hcl:"usb"`
Serial *string `mapstructure:"serial" required:"false" cty:"serial" hcl:"serial"`
Parallel *string `mapstructure:"parallel" required:"false" cty:"parallel" hcl:"parallel"`
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"`
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"`
@@ -187,11 +189,13 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, 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_name": &hcldec.AttrSpec{Name: "network_name", Type: cty.String, Required: false},
"sound": &hcldec.AttrSpec{Name: "sound", 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},
"parallel": &hcldec.AttrSpec{Name: "parallel", 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},
"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},
+5
View File
@@ -38,6 +38,7 @@ type vmxTemplateData struct {
Network_Type string
Network_Device string
Network_Adapter string
Network_Name string
Sound_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
network := config.HWConfig.Network
if config.HWConfig.NetworkName != "" {
templateData.Network_Name = config.HWConfig.NetworkName
}
driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver()
// check to see if the driver implements a network mapper for mapping
@@ -542,6 +546,7 @@ ethernet0.pciSlotNumber = "33"
ethernet0.present = "TRUE"
ethernet0.virtualDev = "{{ .Network_Adapter }}"
ethernet0.wakeOnPcktRcv = "FALSE"
{{if .Network_Name }}ethernet0.networkName = "{{ .Network_Name }}"{{end}}
// Hard disks
scsi0.present = "{{ .SCSI_Present }}"
+6 -22
View File
@@ -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)
}
// 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.
state := new(multistep.BasicStateBag)
state.Put("debug", b.config.PackerDebug)
state.Put("dir", dir)
state.Put("driver", driver)
state.Put("hook", hook)
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,
},
&vmwcommon.StepOutputDir{
Force: b.config.PackerForce,
Force: b.config.PackerForce,
OutputConfig: &b.config.OutputConfig,
RemoteType: b.config.RemoteType,
VMName: b.config.VMName,
},
&common.StepCreateFloppy{
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",
},
&StepCloneVMX{
OutputDir: b.config.OutputDir,
Path: b.config.SourcePath,
OutputDir: &b.config.OutputDir,
VMName: b.config.VMName,
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,
VMName: b.config.VMName,
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
log.Printf("Generating artifact...")
exportOutputPath := state.Get("export_output_path").(string) // set in StepOutputDir
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
}
+2
View File
@@ -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"`
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"`
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"`
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"`
@@ -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},
"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},
"remote_output_directory": &hcldec.AttrSpec{Name: "remote_output_directory", Type: cty.String, 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_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
+3 -3
View File
@@ -16,7 +16,7 @@ import (
// StepCloneVMX takes a VMX file and clones the VM into the output directory.
type StepCloneVMX struct {
OutputDir string
OutputDir *string
Path string
VMName string
Linked bool
@@ -33,7 +33,7 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
ui := state.Get("ui").(packer.Ui)
// 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...")
log.Printf("Cloning from: %s", s.Path)
log.Printf("Cloning to: %s", vmxPath)
@@ -96,7 +96,7 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
var diskFullPaths []string
for _, diskFilename := range diskFilenames {
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 {
+1 -1
View File
@@ -62,7 +62,7 @@ func TestStepCloneVMX(t *testing.T) {
state := testState(t)
step := new(StepCloneVMX)
step.OutputDir = td
step.OutputDir = &td
step.Path = sourcePath
step.VMName = "foo"
+6
View File
@@ -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" {
steps = append(steps,
&common.StepHTTPIPDiscover{
+7 -2
View File
@@ -36,10 +36,12 @@ type Config struct {
// Configuration for exporting VM to an ovf file.
// The VM will not be exported if no [Export Configuration](#export-configuration) is specified.
Export *common.ExportConfig `mapstructure:"export"`
// Configuration for importing the VM template to a Content Library.
// The VM template will not be imported if no [Content Library Import Configuration](#content-library-import-configuration) is specified.
// Configuration for importing a VM template or OVF template to a Content Library.
// 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.
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
}
@@ -75,6 +77,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
if c.ContentLibraryDestinationConfig != nil {
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 {
return nil, errs
+2
View File
@@ -118,6 +118,7 @@ type FlatConfig struct {
ConvertToTemplate *bool `mapstructure:"convert_to_template" cty:"convert_to_template" hcl:"convert_to_template"`
Export *common.FlatExportConfig `mapstructure:"export" cty:"export" hcl:"export"`
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.
@@ -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},
"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())},
"customize": &hcldec.BlockSpec{TypeName: "customize", Nested: hcldec.ObjectSpec((*FlatCustomizeConfig)(nil).HCL2Spec())},
}
return s
}
+4 -2
View File
@@ -32,8 +32,10 @@ type CloneConfig struct {
DiskSize int64 `mapstructure:"disk_size"`
// Create VM as a linked clone from latest snapshot. Defaults to `false`.
LinkedClone bool `mapstructure:"linked_clone"`
// Set the network in which the VM will be connected to. If no network is specified, `host`
// must be specified to allow Packer to look for the available network.
// Set the network in which the VM will be connected to. If no network is
// 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"`
// VM notes.
Notes string `mapstructure:"notes"`
+276
View File
@@ -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
}
+10
View File
@@ -21,12 +21,22 @@ type OutputConfig struct {
// created, must 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"`
// 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 {
if c.OutputDir == "" {
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
}
if c.DirPerm == 0 {
c.DirPerm = 0750
}
return nil
}
@@ -2,6 +2,8 @@
package common
import (
"os"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
@@ -9,7 +11,8 @@ import (
// FlatOutputConfig is an auto-generated flat version of OutputConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
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.
@@ -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.
func (*FlatOutputConfig) HCL2Spec() 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
}
+8 -4
View File
@@ -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))
}
var keyAlt, keyCtrl, keyShift bool
sendCodes := func(code key.Code, down bool) error {
var keyAlt, keyCtrl, keyShift bool
switch code {
case key.CodeLeftAlt:
keyAlt = down
case key.CodeLeftControl:
keyCtrl = down
default:
case key.CodeLeftShift:
keyShift = down
}
shift := down
if keyShift {
shift = keyShift
}
_, err := vm.TypeOnKeyboard(driver.KeyInput{
Scancode: code,
Ctrl: keyCtrl,
Alt: keyAlt,
Shift: keyShift,
Shift: shift,
})
if err != nil {
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
+5 -5
View File
@@ -51,12 +51,12 @@ func (s *StepConfigParams) Run(_ context.Context, state multistep.StateBag) mult
if s.Config.ToolsUpgradePolicy {
info.ToolsUpgradePolicy = "UpgradeAtPowerCycle"
}
}
ui.Say("Adding configuration parameters...")
if err := vm.AddConfigParams(configParams, info); err != nil {
state.Put("error", fmt.Errorf("error adding configuration parameters: %v", err))
return multistep.ActionHalt
}
ui.Say("Adding configuration parameters...")
if err := vm.AddConfigParams(configParams, info); err != nil {
state.Put("error", fmt.Errorf("error adding configuration parameters: %v", err))
return multistep.ActionHalt
}
return multistep.ActionContinue
+20 -1
View File
@@ -31,6 +31,7 @@ import (
//
// Example usage:
//
// In JSON:
// ```json
// ...
// "vm_name": "example-ubuntu",
@@ -40,6 +41,16 @@ import (
// "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:
//
// ```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:
//
// In JSON:
// ```json
// ...
// "export": {
// "options": ["mac"]
// },
// ```
// In HCL2:
// ```hcl
// ...
// export {
// options = ["mac"]
// }
// ```
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"))
}
+16 -12
View File
@@ -2,6 +2,8 @@
package common
import (
"os"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
@@ -9,12 +11,13 @@ import (
// FlatExportConfig is an auto-generated flat version of ExportConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatExportConfig struct {
Name *string `mapstructure:"name" cty:"name" hcl:"name"`
Force *bool `mapstructure:"force" cty:"force" hcl:"force"`
Images *bool `mapstructure:"images" cty:"images" hcl:"images"`
Manifest *string `mapstructure:"manifest" cty:"manifest" hcl:"manifest"`
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
Options []string `mapstructure:"options" cty:"options" hcl:"options"`
Name *string `mapstructure:"name" cty:"name" hcl:"name"`
Force *bool `mapstructure:"force" cty:"force" hcl:"force"`
Images *bool `mapstructure:"images" cty:"images" hcl:"images"`
Manifest *string `mapstructure:"manifest" cty:"manifest" hcl:"manifest"`
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"`
Options []string `mapstructure:"options" cty:"options" hcl:"options"`
}
// 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.
func (*FlatExportConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
"force": &hcldec.AttrSpec{Name: "force", 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},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
"options": &hcldec.AttrSpec{Name: "options", Type: cty.List(cty.String), Required: false},
"name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
"force": &hcldec.AttrSpec{Name: "force", 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},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: 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
}
@@ -13,28 +13,41 @@ import (
"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.
// The virtual machine template is stored in a newly created library item.
// With this configuration Packer creates a library item in a content library whose content is a VM template
// 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 {
// 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.
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).
// Defaults to [vm_name](#vm_name) + timestamp.
// Name of the library item that will be created or updated.
// 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"`
// 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"`
// 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 host are both specified, host must be a member of cluster.
// This option is not used when importing OVF templates.
// Defaults to [cluster](#cluster).
Cluster string `mapstructure:"cluster"`
// 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.
Folder string `mapstructure:"folder"`
// 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 cluster are both specified, host must be a member of cluster.
// This option is not used when importing OVF templates.
// Defaults to [host](#host).
Host string `mapstructure:"host"`
// 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.
ResourcePool string `mapstructure:"resource_pool"`
// 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.
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"`
// 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 {
@@ -54,31 +71,38 @@ func (c *ContentLibraryDestinationConfig) Prepare(lc *LocationConfig) []error {
if c.Library == "" {
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 == "" {
// Add timestamp to the 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))
if c.Ovf {
if c.Name == "" {
c.Name = lc.VMName
}
} else {
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 == "" {
// 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 {
@@ -95,7 +119,42 @@ type StepImportToContentLibrary struct {
func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
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{
Name: s.ContentLibConfig.Name,
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))
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
return vm.ImportToContentLibrary(template)
}
func (s *StepImportToContentLibrary) Cleanup(multistep.StateBag) {
@@ -18,6 +18,7 @@ type FlatContentLibraryDestinationConfig struct {
ResourcePool *string `mapstructure:"resource_pool" cty:"resource_pool" hcl:"resource_pool"`
Datastore *string `mapstructure:"datastore" cty:"datastore" hcl:"datastore"`
Destroy *bool `mapstructure:"destroy" cty:"destroy" hcl:"destroy"`
Ovf *bool `mapstructure:"ovf" cty:"ovf" hcl:"ovf"`
}
// 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},
"datastore": &hcldec.AttrSpec{Name: "datastore", Type: cty.String, Required: false},
"destroy": &hcldec.AttrSpec{Name: "destroy", Type: cty.Bool, Required: false},
"ovf": &hcldec.AttrSpec{Name: "ovf", Type: cty.Bool, Required: false},
}
return s
}
+15 -1
View File
@@ -97,10 +97,16 @@ func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multi
for {
select {
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.")
state.Put("error", err)
ui.Error(err.Error())
cancel()
return multistep.ActionHalt
case <-ctx.Done():
cancel()
@@ -140,6 +146,14 @@ loop:
if err != nil {
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 == "" {
log.Printf("VM IP aquired: %s", ip)
+80
View File
@@ -2,8 +2,12 @@ package driver
import (
"fmt"
"path"
"regexp"
"strings"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/property"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
@@ -57,6 +61,21 @@ func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
}, 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) {
var p []string
if len(params) == 0 {
@@ -85,6 +104,42 @@ func (ds *Datastore) ResolvePath(path string) string {
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 {
p := soap.DefaultUpload
ctx := ds.driver.ctx
@@ -128,3 +183,28 @@ func RemoveDatastorePrefix(path string) string {
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
}
+54
View File
@@ -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)
}
}
}
+26 -10
View File
@@ -19,7 +19,8 @@ type Driver struct {
// context that controls the authenticated sessions used to run the VM commands
ctx context.Context
client *govmomi.Client
restClient *rest.Client
vimClient *vim25.Client
restClient *RestClient
finder *find.Finder
datacenter *object.Datacenter
}
@@ -59,12 +60,6 @@ func NewDriver(config *ConnectConfig) (*Driver, error) {
return nil, err
}
restClient := rest.NewClient(vimClient)
err = restClient.Login(ctx, credentials)
if err != nil {
return nil, err
}
finder := find.NewFinder(client.Client, false)
datacenter, err := finder.DatacenterOrDefault(ctx, config.Datacenter)
if err != nil {
@@ -73,11 +68,32 @@ func NewDriver(config *ConnectConfig) (*Driver, error) {
finder.SetDatacenter(datacenter)
d := Driver{
ctx: ctx,
client: client,
restClient: restClient,
ctx: ctx,
client: client,
vimClient: vimClient,
restClient: &RestClient{
client: rest.NewClient(vimClient),
credentials: credentials,
},
datacenter: datacenter,
finder: finder,
}
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)
}
+96 -3
View File
@@ -1,14 +1,21 @@
package driver
import "github.com/vmware/govmomi/vapi/library"
import (
"fmt"
"log"
"path"
"strings"
"github.com/vmware/govmomi/vapi/library"
)
type Library struct {
driver *Driver
library *library.Library
}
func (d *Driver) FindContentLibrary(name string) (*Library, error) {
lm := library.NewManager(d.restClient)
func (d *Driver) FindContentLibraryByName(name string) (*Library, error) {
lm := library.NewManager(d.restClient.client)
l, err := lm.GetLibraryByName(d.ctx, name)
if err != nil {
return nil, err
@@ -18,3 +25,89 @@ func (d *Driver) FindContentLibrary(name string) (*Library, error) {
driver: d,
}, 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]
}
+62
View File
@@ -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)
}
}
}
+72 -10
View File
@@ -524,6 +524,14 @@ func (vm *VirtualMachine) Configure(config *HardwareConfig) error {
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 {
var confSpec types.VirtualMachineConfigSpec
@@ -675,17 +683,57 @@ func (vm *VirtualMachine) ConvertToTemplate() error {
return vm.vm.MarkAsTemplate(vm.driver.ctx)
}
func (vm *VirtualMachine) ImportToContentLibrary(template vcenter.Template) error {
template.SourceVM = vm.vm.Reference().Value
func (vm *VirtualMachine) ImportOvfToContentLibrary(ovf vcenter.OVF) error {
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 {
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)
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.SourceVM = vm.vm.Reference().Value
if 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
}
vcm := vcenter.NewManager(vm.driver.restClient)
vcm := vcenter.NewManager(vm.driver.restClient.client)
_, 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) {
@@ -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)
if err != nil {
return err
@@ -915,11 +967,21 @@ func (vm *VirtualMachine) AddCdrom(controllerType string, isoPath string) error
return err
}
if isoPath != "" {
devices.InsertIso(cdrom, isoPath)
if datastoreIsoPath != "" {
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)
}
+8 -2
View File
@@ -15,8 +15,14 @@ import (
type CDRomConfig struct {
// Which controller to use. Example: `sata`. Defaults to `ide`.
CdromType string `mapstructure:"cdrom_type"`
// List of datastore paths to ISO files that will be mounted to the VM.
// Example: `"[datastore1] ISO/ubuntu.iso"`.
// List of Datastore or Content Library paths to ISO files that will be mounted to the VM.
// 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"`
}

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