Compare commits

...

103 Commits

Author SHA1 Message Date
Wilken Rivera d597e93e70 improve error message 2020-09-25 12:12:31 -04:00
Wilken Rivera 1bb154de5a Add support for reading configs
This is the first step in attempting to read a Packer configuration file
via the packer init command. At the present moment it will try to read
one or more configuration templates from a given directory or path. Once
parsed it will error if parsing fails or exist successfully if it is
able to parse the file.

Looking at how the code is structured there will need to be changes made
to the following places:

- When no configuration file is found Packer will display an error. That
error should be bubbled up a bit so that the caller command can
determine if it should be displayed or not. For packer init no
configuration is not an error. Maybe it should be?

- After a configuration has been parsed there needs to be a single way
to determine a list of plugins associated with the configuration. HCL
and JSON configs have fields for this data but some is exported and some
is unexported. Adapting the packerHandler interface may be an option
here. More investigation needed.
2020-09-16 14:46:16 -04:00
Wilken Rivera 8069ae4f43 Add flagset to init command 2020-09-15 10:19:56 -04:00
Wilken Rivera e8f7076416 Add simple init command 2020-09-14 16:33:51 -04:00
Andreas Botzner c032d463d3 Bootcommand Fix For Proxmox Builder (#9885) 2020-09-14 17:17:38 +02:00
Sylvia Moss caf65781d7 Add vsphere-clone mac_address option (#9930)
* add vsphere-clone mac_address option

* check on prepare if network is set together with mac_address
2020-09-14 13:18:39 +02:00
Andreas Botzner 3739970b84 Adds ability to specify interfaces for http_directroy and VM for the Proxmox builder (#9874)
Adds two config options to the Proxmox builder:
```
http_interface
vm_interface
```
Both give the user the ability to specify an interface whos IP will be set as the `.HTTPIP` and VMIP respectively.

This is useful when the VM and or the machine executing Packer has multiple interfaces.
Until now Packer would accept the first non-loopback IP as the `.HTTPIP` and VMIP.

I'm open to suggestions and any kind of feedback. 


* Added ability to define the NIC where the HTTPIP is taken from.
* Added VM interface config option
* fmt and documentation
* Moved HTTPInterface into HTTPCommon
* Build Fix
* Documentation Fix
2020-09-14 12:24:01 +02:00
GennadySpb ad4ed3c986 Update CHANGELOG.md (#9935) 2020-09-14 12:14:19 +02:00
Wilken Rivera f94e78077a update CHANGELOG 2020-09-11 14:13:10 -04:00
Rui Lopes 53331644a6 add support for using xorriso to create the cdrom iso (#9919) 2020-09-11 14:11:19 -04:00
Megan Marsh bcd3c33e49 Merge pull request #9931 from GennadySpb/yndx-import-fix-url
yandex-import: Fix S3 URL construct process
2020-09-11 07:48:11 -07:00
Sylvia Moss 31f08909b4 Add usb_scan_codes option to use USB scancodes instead of VNC for remote builds (#9895) 2020-09-11 16:46:33 +02:00
Gennady Lipenkov e078bd6bf2 Fix S3 URL construct process 2020-09-11 16:49:30 +03:00
GennadySpb 2837d56885 Check for error after runner completes (#9925) 2020-09-11 11:14:49 +02:00
Megan Marsh 5ca9f6539f remove xen from qemu docs, since it turns out it has not been supported with solely qemu for some time (#9917) 2020-09-09 19:42:37 -04:00
Megan Marsh 8b9009c571 Merge pull request #9856 from OblateSpheroid/add_vnic_details
Feat (builder/oracle-oci): Add VNIC details to launch details
2020-09-09 16:15:48 -07:00
Megan Marsh 8ffa0d2060 Merge pull request #9842 from jason-azze/patch-1
Builders vsphere-iso and vsphere-clone - CPUs is actually for cores, not sockets.
2020-09-09 16:03:45 -07:00
Megan Marsh ca650c9602 fix struct comment for generation 2020-09-09 16:01:30 -07:00
Megan Marsh 25b889216d update changelog 2020-09-09 15:07:27 -07:00
Megan Marsh 81f9f4dd27 Merge pull request #9911 from hashicorp/ssh_redocument_options
Ssh redocument options
2020-09-09 08:44:32 -07:00
Megan Marsh 24d5824061 more docs update to add ssh private key file to all the builders that have them built into the create ssh key step 2020-09-09 08:33:38 -07:00
Megan Marsh ad0b413e2d add private key file docs to qemu builder 2020-09-09 07:41:16 -07:00
js-g 9d5dedc3db tests: add some fields to config_test 2020-09-09 09:25:58 -04:00
Patrik f8e31bff12 feat(builder): bump scaleway to new sdk (#9902) 2020-09-09 10:27:48 +02:00
Wilken Rivera 0719f906d4 Replace if/else with simple if (#9913) 2020-09-09 10:24:17 +02:00
Megan Marsh 8aaa1bd7a6 Merge pull request #9796 from hashicorp/do_8778
create secondary iso containing user's desired files
2020-09-08 15:05:48 -07:00
Kevin Pruett 3a5beedddf Merge pull request #9914 from hashicorp/pruett.update-search
Integrate @hashicorp/react-search/tools script
2020-09-08 17:24:41 -04:00
Kevin Pruett e5cd5a9360 Integrate @hashicorp/react-search/tools script 2020-09-08 17:16:42 -04:00
Megan Marsh 26f3299fb7 Merge pull request #9903 from artis3n/ansible-collections
Ansible Galaxy Collections support in Ansible provisioner
2020-09-08 12:34:39 -07:00
Megan Marsh 40c128767e regenerate docs 2020-09-08 12:17:58 -07:00
js-g dd54651125 fix (builder/oracle-oci): add freeform and defined tags to create_vnic_details 2020-09-08 14:40:21 -04:00
Richard Turc b4ff0ea4bc [builder/qemu] Skip resize step when skip_resize_disk is enable #9860 (#9896)
* [builder/qemu] Skip resize step when skip_resize_disk is enable #9860

* Update builder/qemu/builder_test.go

Improve the code quality

Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>

* Update files for unit tests

Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>
2020-09-08 14:32:08 -04:00
Megan Marsh 988ab66731 Update common/extra_iso_config.go
thanks for the copyediting :D

Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>
2020-09-08 11:18:10 -07:00
Megan Marsh 900100f591 Update website/pages/docs/builders/hyperv/iso.mdx
Co-authored-by: Wilken Rivera <dev@wilkenrivera.com>
2020-09-08 11:17:20 -07:00
js-g 254716a02a docs (oracle-oci): add description and example for create_vnic_details 2020-09-08 13:53:19 -04:00
js-g f5dc260cf5 refactor (builder/oracle-oci): pass CreateVnicDetails as struct instead of map 2020-09-08 12:03:18 -04:00
Wilken Rivera 2da89db272 Merge pull request #9905 from The-Loeki/patch-2
qemu vnc: hardcoded lowerbound leaves negative ports
2020-09-08 10:17:53 -04:00
Ronald van Zantvoort e362b7f43f build only only twice (#9904)
remove duplicate entry for build > --only
2020-09-07 11:56:28 +02:00
Ronald van Zantvoort 7335695c84 qemu vnc: hardcoded lowerbound leaves negative ports
```hcl2
  vnc_port_min = 5000
  vnc_port_max = 5500
```

Build returns faulty:
```
    qemu.one: view the screen of the VM, connect via VNC to vnc://127.0.0.1:5138
(...)
2020/09/06 19:09:52 packer-builder-qemu plugin: Qemu stderr: qemu-system-x86_64: -vnc 127.0.0.1:-762,password: can't convert to a number: -762
```
2020-09-06 21:18:12 +02:00
Artis3n 4ecade3908 collectionsPath appends to the correct slice
Fixes what would have been a failure if roles+collections were tested at
once
2020-09-05 18:43:10 -04:00
Artis3n 39183d1b76 Updated documentation with make generate 2020-09-05 18:29:52 -04:00
Artis3n 9a2908d1d9 Use the galaxy playbook in the galaxy packer file 2020-09-05 18:15:42 -04:00
Artis3n 611899f7b2 Actually use the collectionArgs, that would be helpful 2020-09-05 16:51:38 -04:00
Artis3n 41a8a96821 Added acceptance test for Galaxy Collection install 2020-09-05 16:48:20 -04:00
Artis3n 3d2259a6d4 Typo fix in comment 2020-09-05 16:19:05 -04:00
Artis3n 19f0114035 Added support for Ansible Collections to the Galaxy install
Fixes #8821
2020-09-05 16:10:02 -04:00
Megan Marsh 94a9beaac6 fix up documentation for virtualbox key pair ssh options 2020-09-04 16:55:58 -07:00
Megan Marsh 61af278b79 document previously undocumented ssh options for tencent cloud 2020-09-04 16:50:45 -07:00
Megan Marsh bd2aa40d2c document previously undocumented ssh options for scaleway 2020-09-04 16:50:31 -07:00
Megan Marsh c7356fb776 document previously undocumented ssh options for outscale 2020-09-04 16:49:48 -07:00
Megan Marsh 850fda6aa1 document previously undocumented ssh options for oracle classic 2020-09-04 16:49:31 -07:00
Megan Marsh f3633da200 document previously undocumented ssh options for jdcloud 2020-09-04 16:49:12 -07:00
Megan Marsh 00c6efd5cf document previously undocumented ssh options for cloudstack 2020-09-04 16:48:47 -07:00
Megan Marsh bca4439149 add undocumented ssh options to alicloud builder docs 2020-09-04 16:48:05 -07:00
Megan Marsh d2dd158fe2 Implement cd_files option. The Addfile code is modified from the floppy_files
tooling. Tested on OSX, `Windows, and Linux. Documented command line dependencies.
Added acceptance tests (behind testacc flag, since there are dependencies).
This option is only implemented for the vmware and hyperv iso builders. It will be
implemented for others in later PRs in order to make review easier.
2020-09-04 12:20:19 -07:00
Megan Marsh dfe8aa51b0 Merge pull request #9845 from hashicorp/run_and_stream_helper_func
move runAndStream into a generalized helper function to enable easy l…
2020-09-04 12:19:37 -07:00
Megan Marsh 5120ad9d9a add test file 2020-09-04 11:56:32 -07:00
Megan Marsh 20d62654ae move runAndStream into a generalized helper function to enable easy local exectution in other places than just the docker builder 2020-09-04 11:56:32 -07:00
Kevin Pruett bce6fdf03a Merge pull request #9870 from hashicorp/pruett.integrate-search
Integrate @hashicorp/react-search into layout
2020-09-04 10:52:20 -04:00
Lars Lehtonen 03a0beb683 builder/vmware/common: simplify type assertion switch (#9893) 2020-09-04 16:25:19 +02:00
Wilken Rivera 6334fc2da4 Merge pull request #9891 from hashicorp/d-ssh-communicator-supported-opts
communicator/ssh: Add note for builder specific SSH supported options
2020-09-03 13:51:18 -04:00
Wilken Rivera 7813ad5ef4 communicator/ssh: Add note for builder specific SSH supported options 2020-09-03 13:31:47 -04:00
Sylvia Moss ba74602d63 Allows publishing to a Shared Image Gallery with a different subscription id (#9875) 2020-09-03 17:13:32 +02:00
Kevin Pruett 6c4e9e9e68 Integrate @hashicorp/react-search into layout 2020-09-03 11:08:09 -04:00
Wilken Rivera d66f85e375 Merge pull request #9887 from catsby/no-more-catsby
Remove comments about sharing with catsby
2020-09-03 10:20:37 -04:00
catsby 7a7d3ff350 Remove comments about sharing with catsby 2020-09-03 09:14:40 -05:00
Sylvia Moss 62e1663846 update build command with bigger max_old_space_size (#9882) 2020-09-03 11:48:54 +02:00
Troy Lindsay d1a61cac33 Add NVMe controller support to vsphere-iso builder (#9880) 2020-09-03 11:14:58 +02:00
Troy Lindsay 602729ac23 Fix vsphere-iso acceptance tests (#9878) (#9881) 2020-09-03 10:17:49 +02:00
Wilken Rivera c492199ba5 Merge pull request #9877 from hashicorp/rae/move-aws-id-to-env-var
update sharing test to req aws account id
2020-09-02 15:57:50 -04:00
Rae Krantz ab23e839c8 update sharing test to req aws account id 2020-09-02 11:44:38 -04:00
Adrien Delorme efd69aea2a console: allow to use console when there are untyped vars (#9864)
* the var will simply be "unknown"
* add a test
2020-09-02 17:19:56 +02:00
Adrien Delorme a70164f489 HCL2: when the type of a variable is not known evaluate setting as a litteral. (#9863)
* tests
* docs
2020-09-02 16:26:50 +02:00
Sylvia Moss 903deb9e6a vSphere StepHardware tests (#9866) 2020-09-02 10:19:57 +02:00
Megan Marsh bddf3b03f7 Merge pull request #9832 from hashicorp/azr-inspect-allow-unsed-variables
Inspect allow unset variables in HCL2 and JSON
2020-09-01 11:16:54 -07:00
Megan Marsh e96cc07a1a Merge pull request #9853 from remyleone/boottype_local
change default scaleway boottype to local
2020-09-01 10:44:42 -07:00
Wilken Rivera 79bc643c17 post-processor/digitalocean-import: Update documentation (#9865)
* Add a note to use the DigitialOcean Builder when working directly on Digital Ocean
* Add HCL2 example to the documentation
2020-09-01 11:08:06 -04:00
Wilken Rivera 10e74961d2 Add check for empty artifact.Files slice (#9857)
* Add check for empty artifact.Files slice

Tests before change
```
⇶  go test ./post-processor/digitalocean-import/... -run=TestPostProcsor_extractImageArtifact
2020/08/31 13:51:25 Looking for image in artifact
--- FAIL: TestPostProcsor_extractImageArtifact (0.00s)
panic: runtime error: index out of range [0] with length 0 [recovered]
        panic: runtime error: index out of range [0] with length 0

goroutine 7 [running]:
testing.tRunner.func1.1(0xfb0300, 0xc000456460)
        /usr/local/go/src/testing/testing.go:940 +0x2f5
testing.tRunner.func1(0xc0003ab560)
        /usr/local/go/src/testing/testing.go:943 +0x3f9
panic(0xfb0300, 0xc000456460)
        /usr/local/go/src/runtime/panic.go:969 +0x166
github.com/hashicorp/packer/post-processor/digitalocean-import.extractImageArtifact(0x0, 0x0, 0x0, 0x24, 0xc000060ea0, 0x453937, 0x1431250)
        /home/wilken/Development/packer/post-processor/digitalocean-import/post-processor.go:262 +0x36d
github.com/hashicorp/packer/post-processor/digitalocean-import.TestPostProcsor_extractImageArtifact(0xc0003ab560)
        /home/wilken/Development/packer/post-processor/digitalocean-import/post-processor_test.go:28 +0x2b0
testing.tRunner(0xc0003ab560, 0x1077208)
        /usr/local/go/src/testing/testing.go:991 +0xdc
created by testing.(*T).Run
        /usr/local/go/src/testing/testing.go:1042 +0x357
FAIL    github.com/hashicorp/packer/post-processor/digitalocean-import  0.009s
FAIL
```

Tests after change
```
[go-1.14.2] [1] wilken@automaton in ~/Development/packer/ on fix_9848 (ahead 1)
⇶  go test ./post-processor/digitalocean-import/... -run=TestPostProcsor_extractImageArtifact
ok      github.com/hashicorp/packer/post-processor/digitalocean-import  0.006s
```

* Update to reflect review feedback
2020-09-01 10:59:01 -04:00
Adrien Delorme bdf198594e hcl2 inspect: sort variables to have a consistent output 2020-09-01 15:28:16 +02:00
Adrien Delorme a25f057984 add tests for unknown values 2020-09-01 15:28:16 +02:00
Adrien Delorme 161879b98a test unknown variables and locals 2020-09-01 11:59:07 +02:00
Adrien Delorme 652878059c inspect command: ignore init errors as some value can be unset 2020-09-01 11:59:07 +02:00
Adrien Delorme eb4069a1b7 hcl variables: return an unknown value in when no default is set 2020-09-01 11:59:07 +02:00
Adrien Delorme 91c5a4613c HCL2 inspect remove debug values 2020-09-01 11:59:07 +02:00
Sylvia Moss 9eaa2e17f7 add netlify node option to increase old space size (#9859) 2020-09-01 11:24:30 +02:00
Sylvia Moss 24dbd0a28b Fix iso_path validation regex (#9855) 2020-09-01 10:11:48 +02:00
Rémy Léone 05aecc56ea Fix 2020-09-01 09:14:07 +02:00
Wilken Rivera f4556fd3f1 Merge pull request #9831 from hashicorp/remedial_remote_export_config_tests
add tests to make sure prepare for export defaults properly
2020-08-31 15:47:05 -04:00
js-g f74a425661 feat (builder/oracle-oci): pass CreateVnicDetails to instance 2020-08-31 13:13:33 -04:00
Sylvia Moss 58a0bdd780 Look for a default resource pool when root resource pool is not found (#9809) 2020-08-31 16:26:48 +02:00
Wilken Rivera 03a374f4b6 Fix linting issues for SA6005 check (#9854)
Before change
```
⇶  golangci-lint run --disable-all --no-config --enable=staticcheck | ack SA6005
builder/profitbricks/step_create_server.go:254:22: SA6005: should use strings.EqualFold(a, b) instead of strings.ToLower(a) == strings.ToLower(b) (staticcheck)
builder/oneandone/config.go:97:7: SA6005: should use strings.EqualFold(a, b) instead of strings.ToLower(a) == strings.ToLower(b) (staticcheck)
builder/vmware/common/driver_parser.go:1199:7: SA6005: should use strings.EqualFold(a, b) instead of strings.ToLower(a) == strings.ToLower(b) (staticcheck)
```

After change
```
⇶  golangci-lint run --disable-all --no-config --enable=staticcheck | ack SA6005

```
2020-08-31 15:44:42 +02:00
GennadySpb 804fefef17 yandex-import: allow set custom API endpoint (#9850)
* Separate Access Config from yandex builder Config

* make use of Access Config explicit

* Move `MaxRetries` into AccessConfig

* NewDriverYC use AccessConfig instead Config

* yandex-import PP use common Access Config

Now support set custom API Endpoint

* yandex-export PP use common Access Config

Now support set custom API Endpoint too (as yandex-import)

* fix test

* Tiny doc updates.
2020-08-31 15:29:20 +02:00
Rémy Léone f578b93f7e update scaleway code owners (#9852) 2020-08-31 14:41:45 +02:00
GennadySpb 0df2e15d9f If proposed exit code not equal 0 set proper metadata key to expected 'cloud-init-error'. (#9849)
Last one checked at `StepWaitCloudInitScript`.
2020-08-31 14:38:22 +02:00
OblateSpheroid 0ad26cce01 Feat (oracle-oci): allow freeform and defined tags to be added instance (#9802)
* feat (oracle-oci): allow freeform and defined tags to be added to instance (#6313)

* docs (oracle-oci): add descriptions for instance_tags and instance_defined_tags (#6313)

* fix: fmt

* fix: generate hcl2spec
2020-08-31 14:36:09 +02:00
Rui Lopes 281af9a03d prefer $APPDATA over $HOME in Windows (#9830) 2020-08-31 14:35:15 +02:00
Rémy Léone ecd1a49a35 change default scaleway boottype to local 2020-08-31 13:15:37 +02:00
Andreas Botzner e4f975fae1 Allows for the mounting of ISOs when a Proxmox VM s created. Same as … (#9653)
Allows the mounting of additional ISOs when the VM is created. The config option was taken from PR #9055 and slightly changed. Users can specify an array of bus names, bus numbers and filenames.

"cd_drive":[
{
"bus": "ide",
"bus_number": 3,
"filename": "isos:iso/virtio-win-0.1.187.iso"
},
{
"bus": "sata",
"bus_number": 3,
"filename": "isos:iso/someother.iso"
}
]

Closes: #7950
Co-authored-by: Calle Pettersson <carlpett@users.noreply.github.com>
2020-08-31 10:48:24 +02:00
Sylvia Moss 942bfbf221 Add driver mocks and write tests to steps Remote Upload and Create VM (#9833) 2020-08-31 10:34:41 +02:00
packer-ci cf622346ab Putting source back into Dev Mode 2020-08-28 18:23:03 +00:00
Jason Azze 9156563314 Update HardwareConfig-not-required.mdx
I've tested the behavior of CPUs and cpu_cores against both vSphere 5.5 and 6.7. In both cases, CPUs gives you virtual cores, not sockets.
For example, I want 6 cores per socket across 2 sockets for 12 total cores. Based on the wording of this doc, I set CPUs to 2 and cpu_cores to 6. The documentation implies that will give me 2 sockets with 6 cores each. The actual behavior is you get 2 cores, and when you crack open the VMs configuration, you see that cores per socket is set to 6 -- which is meaningless.

Setting CPUs to 12 and cpu_cores to 6 gives me what I wanted. So the wording I propose is 
```
- `CPUs` (int32) - Number of CPU cores.
```
2020-08-27 15:10:33 -04:00
Megan Marsh 1c5bc41beb refactor ovftool validation into vmware driver 2020-08-27 10:55:57 -07:00
Megan Marsh bbc3a5b0d1 add tests to make sure prepare for export defaults properly 2020-08-27 10:55:57 -07:00
443 changed files with 99970 additions and 15057 deletions
+37
View File
@@ -1,3 +1,40 @@
## 1.6.3 (Upcoming)
### IMPROVEMENTS:
* builder/azure: Support publishing to a Shared Image Gallery with a different
subscription id [GH-9875]
* builder/oracle-oci: Allow freeform and defined tags to be added instance.
[GH-9802]
* builder/proxmox: Allow the mounting of multiple ISOs via the `cd_drive`
option. [GH-9653]
* builder/qemu: New `skip_resize_disk` option. [GH-9896]
* builder/scaleway: Change default boottype to local. [GH-9853]
* builder/scaleway: Update scaleway to use non-deprecated sdk. [GH-9902]
* builder/vsphere-iso: Add NVMe controller support. [GH-9880]
* builder/vsphere: Look for a default resource pool when root resource pool is
not found. [GH-9809]
* core: New `cd_files` option to mount iso for modern OSes which don't support
floppies. [GH-9796] [GH-9919]
* HCL2: When the type of a variable is not known evaluate setting as a literal
string instead of a variable name. [GH-9863]
* post-processor/yandex-import: Allow custom API endpoint. [GH-9850]
* provisioner/ansible: Add support for Ansible Galaxy Collections. [GH-9903]
### BUG FIXES:
* builder/qemu: Fix hardcoded lowerbound causing negative ports [GH-9905]
* builder/vsphere: Fix overly strict iso_path validation regex. [GH-9855]
* command/console: Prevent failure when there are unknown vars. [GH-9864]
* command/inspect: Allow unset variables in HCL2 and JSON. [GH-9832]
* core: use $APPDATA over $HOME on Windows hosts when determining homedir.
[GH-9830]
* post-processor/digitalocean-import: Fix crash caused by empty artifact.Files
slice. [GH-9857]
* post-processor/yandex-export: Set metadata key to expected value on error.
[GH-9849]
* post-processor/yandex-export: Check for error after runner completes.
[GH-9925]
* post-processor/yandex-import: Fix S3 URL construct process. [GH-9931]
## 1.6.2 (August 28, 2020)
### FEATURES:
+2 -2
View File
@@ -49,8 +49,8 @@
/builder/proxmox/ @carlpett
/website/pages/docs/builders/proxmox* @carlpett
/builder/scaleway/ @sieben @mvaude @jqueuniet @fflorens @brmzkw
/website/pages/docs/builders/scaleway* @sieben @mvaude @jqueuniet @fflorens @brmzkw
/builder/scaleway/ @scaleway/devtools
/website/pages/docs/builders/scaleway* @scaleway/devtools
/builder/hcloud/ @LKaemmerling
/website/pages/docs/builders/hcloud* @LKaemmerling
-1
View File
@@ -422,7 +422,6 @@ func TestBuilderAcc_ECSImageSharing(t *testing.T) {
})
}
// share with catsby
const testBuilderAccSharing = `
{
"builders": [{
+15 -5
View File
@@ -6,6 +6,7 @@ package ebs
import (
"fmt"
"os"
"testing"
"github.com/aws/aws-sdk-go/aws"
@@ -104,10 +105,10 @@ func checkSnapshotsDeleted(snapshotIds []*string) builderT.TestCheckFunc {
func TestBuilderAcc_amiSharing(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
PreCheck: func() { testAccSharingPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccSharing,
Check: checkAMISharing(2, "932021504756", "all"),
Template: buildSharingConfig(os.Getenv("TESTACC_AWS_ACCOUNT_ID")),
Check: checkAMISharing(2, os.Getenv("TESTACC_AWS_ACCOUNT_ID"), "all"),
})
}
@@ -248,6 +249,12 @@ func checkBootEncrypted() builderT.TestCheckFunc {
func testAccPreCheck(t *testing.T) {
}
func testAccSharingPreCheck(t *testing.T) {
if v := os.Getenv("TESTACC_AWS_ACCOUNT_ID"); v == "" {
t.Fatal(fmt.Sprintf("TESTACC_AWS_ACCOUNT_ID must be set for acceptance tests"))
}
}
func testEC2Conn() (*ec2.EC2, error) {
access := &common.AccessConfig{RawRegion: "us-east-1"}
session, err := access.Session()
@@ -314,7 +321,6 @@ const testBuilderAccForceDeleteSnapshot = `
}
`
// share with catsby
const testBuilderAccSharing = `
{
"builders": [{
@@ -324,7 +330,7 @@ const testBuilderAccSharing = `
"source_ami": "ami-76b2a71e",
"ssh_username": "ubuntu",
"ami_name": "packer-test {{timestamp}}",
"ami_users":["932021504756"],
"ami_users":["%s"],
"ami_groups":["all"]
}]
}
@@ -351,3 +357,7 @@ func buildForceDeregisterConfig(val, name string) string {
func buildForceDeleteSnapshotConfig(val, name string) string {
return fmt.Sprintf(testBuilderAccForceDeleteSnapshot, val, val, name)
}
func buildSharingConfig(val string) string {
return fmt.Sprintf(testBuilderAccSharing, val)
}
+20 -18
View File
@@ -128,8 +128,8 @@ func byConcatDecorators(decorators ...autorest.RespondDecorator) autorest.Respon
}
}
func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string,
cloud *azure.Environment, SharedGalleryTimeout time.Duration, PollingDuration time.Duration,
func NewAzureClient(subscriptionID, sigSubscriptionID, resourceGroupName, storageAccountName string,
cloud *azure.Environment, sharedGalleryTimeout time.Duration, pollingDuration time.Duration,
servicePrincipalToken, servicePrincipalTokenVault *adal.ServicePrincipalToken) (*AzureClient, error) {
var azureClient = &AzureClient{}
@@ -141,56 +141,56 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
azureClient.DeploymentsClient.RequestInspector = withInspection(maxlen)
azureClient.DeploymentsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DeploymentsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DeploymentsClient.UserAgent)
azureClient.DeploymentsClient.Client.PollingDuration = PollingDuration
azureClient.DeploymentsClient.Client.PollingDuration = pollingDuration
azureClient.DeploymentOperationsClient = resources.NewDeploymentOperationsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.DeploymentOperationsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.DeploymentOperationsClient.RequestInspector = withInspection(maxlen)
azureClient.DeploymentOperationsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DeploymentOperationsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DeploymentOperationsClient.UserAgent)
azureClient.DeploymentOperationsClient.Client.PollingDuration = PollingDuration
azureClient.DeploymentOperationsClient.Client.PollingDuration = pollingDuration
azureClient.DisksClient = compute.NewDisksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.DisksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.DisksClient.RequestInspector = withInspection(maxlen)
azureClient.DisksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DisksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DisksClient.UserAgent)
azureClient.DisksClient.Client.PollingDuration = PollingDuration
azureClient.DisksClient.Client.PollingDuration = pollingDuration
azureClient.GroupsClient = resources.NewGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GroupsClient.RequestInspector = withInspection(maxlen)
azureClient.GroupsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GroupsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GroupsClient.UserAgent)
azureClient.GroupsClient.Client.PollingDuration = PollingDuration
azureClient.GroupsClient.Client.PollingDuration = pollingDuration
azureClient.ImagesClient = compute.NewImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.ImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.ImagesClient.RequestInspector = withInspection(maxlen)
azureClient.ImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.ImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.ImagesClient.UserAgent)
azureClient.ImagesClient.Client.PollingDuration = PollingDuration
azureClient.ImagesClient.Client.PollingDuration = pollingDuration
azureClient.InterfacesClient = network.NewInterfacesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.InterfacesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.InterfacesClient.RequestInspector = withInspection(maxlen)
azureClient.InterfacesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.InterfacesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.InterfacesClient.UserAgent)
azureClient.InterfacesClient.Client.PollingDuration = PollingDuration
azureClient.InterfacesClient.Client.PollingDuration = pollingDuration
azureClient.SubnetsClient = network.NewSubnetsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SubnetsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.SubnetsClient.RequestInspector = withInspection(maxlen)
azureClient.SubnetsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.SubnetsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.SubnetsClient.UserAgent)
azureClient.SubnetsClient.Client.PollingDuration = PollingDuration
azureClient.SubnetsClient.Client.PollingDuration = pollingDuration
azureClient.VirtualNetworksClient = network.NewVirtualNetworksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.VirtualNetworksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.VirtualNetworksClient.RequestInspector = withInspection(maxlen)
azureClient.VirtualNetworksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VirtualNetworksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VirtualNetworksClient.UserAgent)
azureClient.VirtualNetworksClient.Client.PollingDuration = PollingDuration
azureClient.VirtualNetworksClient.Client.PollingDuration = pollingDuration
azureClient.SecurityGroupsClient = network.NewSecurityGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SecurityGroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
@@ -203,42 +203,44 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
azureClient.PublicIPAddressesClient.RequestInspector = withInspection(maxlen)
azureClient.PublicIPAddressesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.PublicIPAddressesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.PublicIPAddressesClient.UserAgent)
azureClient.PublicIPAddressesClient.Client.PollingDuration = PollingDuration
azureClient.PublicIPAddressesClient.Client.PollingDuration = pollingDuration
azureClient.VirtualMachinesClient = compute.NewVirtualMachinesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.VirtualMachinesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.VirtualMachinesClient.RequestInspector = withInspection(maxlen)
azureClient.VirtualMachinesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), templateCapture(azureClient), errorCapture(azureClient))
azureClient.VirtualMachinesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VirtualMachinesClient.UserAgent)
azureClient.VirtualMachinesClient.Client.PollingDuration = PollingDuration
azureClient.VirtualMachinesClient.Client.PollingDuration = pollingDuration
azureClient.SnapshotsClient = compute.NewSnapshotsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SnapshotsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.SnapshotsClient.RequestInspector = withInspection(maxlen)
azureClient.SnapshotsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.SnapshotsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.SnapshotsClient.UserAgent)
azureClient.SnapshotsClient.Client.PollingDuration = PollingDuration
azureClient.SnapshotsClient.Client.PollingDuration = pollingDuration
azureClient.AccountsClient = armStorage.NewAccountsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.AccountsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.AccountsClient.RequestInspector = withInspection(maxlen)
azureClient.AccountsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.AccountsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.AccountsClient.UserAgent)
azureClient.AccountsClient.Client.PollingDuration = PollingDuration
azureClient.AccountsClient.Client.PollingDuration = pollingDuration
azureClient.GalleryImageVersionsClient = newCompute.NewGalleryImageVersionsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GalleryImageVersionsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GalleryImageVersionsClient.RequestInspector = withInspection(maxlen)
azureClient.GalleryImageVersionsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GalleryImageVersionsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GalleryImageVersionsClient.UserAgent)
azureClient.GalleryImageVersionsClient.Client.PollingDuration = SharedGalleryTimeout
azureClient.GalleryImageVersionsClient.Client.PollingDuration = sharedGalleryTimeout
azureClient.GalleryImageVersionsClient.SubscriptionID = sigSubscriptionID
azureClient.GalleryImagesClient = newCompute.NewGalleryImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GalleryImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GalleryImagesClient.RequestInspector = withInspection(maxlen)
azureClient.GalleryImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GalleryImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GalleryImagesClient.UserAgent)
azureClient.GalleryImagesClient.Client.PollingDuration = PollingDuration
azureClient.GalleryImagesClient.Client.PollingDuration = pollingDuration
azureClient.GalleryImagesClient.SubscriptionID = sigSubscriptionID
keyVaultURL, err := url.Parse(cloud.KeyVaultEndpoint)
if err != nil {
@@ -250,7 +252,7 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
azureClient.VaultClient.RequestInspector = withInspection(maxlen)
azureClient.VaultClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VaultClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VaultClient.UserAgent)
azureClient.VaultClient.Client.PollingDuration = PollingDuration
azureClient.VaultClient.Client.PollingDuration = pollingDuration
// This client is different than the above because it manages the vault
// itself rather than the contents of the vault.
@@ -259,7 +261,7 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
azureClient.VaultClientDelete.RequestInspector = withInspection(maxlen)
azureClient.VaultClientDelete.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VaultClientDelete.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VaultClientDelete.UserAgent)
azureClient.VaultClientDelete.Client.PollingDuration = PollingDuration
azureClient.VaultClientDelete.Client.PollingDuration = pollingDuration
// If this is a managed disk build, this should be ignored.
if resourceGroupName != "" && storageAccountName != "" {
+1
View File
@@ -84,6 +84,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ui.Message("Creating Azure Resource Manager (ARM) client ...")
azureClient, err := NewAzureClient(
b.config.ClientConfig.SubscriptionID,
b.config.SharedGalleryDestination.SigDestinationSubscription,
b.config.ResourceGroupName,
b.config.StorageAccount,
b.config.ClientConfig.CloudEnvironment(),
+56 -19
View File
@@ -90,6 +90,7 @@ type SharedImageGallery struct {
}
type SharedImageGalleryDestination struct {
SigDestinationSubscription string `mapstructure:"subscription"`
SigDestinationResourceGroup string `mapstructure:"resource_group"`
SigDestinationGalleryName string `mapstructure:"gallery_name"`
SigDestinationImageName string `mapstructure:"image_name"`
@@ -118,31 +119,64 @@ type Config struct {
// Use a [Shared Gallery
// image](https://azure.microsoft.com/en-us/blog/announcing-the-public-preview-of-shared-image-gallery/)
// as the source for this build. *VHD targets are incompatible with this
// build type* - the target must be a *Managed Image*.
// build type* - the target must be a *Managed Image*. When using shared_image_gallery as a source, image_publisher,
// image_offer, image_sku, image_version, and custom_managed_image_name should not be set.
//
// "shared_image_gallery": {
// "subscription": "00000000-0000-0000-0000-00000000000",
// "resource_group": "ResourceGroup",
// "gallery_name": "GalleryName",
// "image_name": "ImageName",
// "image_version": "1.0.0"
// }
// "managed_image_name": "TargetImageName",
// "managed_image_resource_group_name": "TargetResourceGroup"
// In JSON
// ```json
// "shared_image_gallery": {
// "subscription": "00000000-0000-0000-0000-00000000000",
// "resource_group": "ResourceGroup",
// "gallery_name": "GalleryName",
// "image_name": "ImageName",
// "image_version": "1.0.0"
// }
// "managed_image_name": "TargetImageName",
// "managed_image_resource_group_name": "TargetResourceGroup"
// ```
// In HCL2
// ```hcl
// shared_image_gallery {
// subscription = "00000000-0000-0000-0000-00000000000"
// resource_group = "ResourceGroup"
// gallery_name = "GalleryName"
// image_name = "ImageName"
// image_version = "1.0.0"
// }
// managed_image_name = "TargetImageName"
// managed_image_resource_group_name = "TargetResourceGroup"
// ```
SharedGallery SharedImageGallery `mapstructure:"shared_image_gallery" required:"false"`
// The name of the Shared Image Gallery under which the managed image will be published as Shared Gallery Image version.
//
// Following is an example.
//
// "shared_image_gallery_destination": {
// "resource_group": "ResourceGroup",
// "gallery_name": "GalleryName",
// "image_name": "ImageName",
// "image_version": "1.0.0",
// "replication_regions": ["regionA", "regionB", "regionC"]
// }
// "managed_image_name": "TargetImageName",
// "managed_image_resource_group_name": "TargetResourceGroup"
// In JSON
// ```json
// "shared_image_gallery_destination": {
// "subscription": "00000000-0000-0000-0000-00000000000",
// "resource_group": "ResourceGroup",
// "gallery_name": "GalleryName",
// "image_name": "ImageName",
// "image_version": "1.0.0",
// "replication_regions": ["regionA", "regionB", "regionC"]
// }
// "managed_image_name": "TargetImageName",
// "managed_image_resource_group_name": "TargetResourceGroup"
// ```
// In HCL2
// ```hcl
// shared_image_gallery_destination {
// subscription = "00000000-0000-0000-0000-00000000000"
// resource_group = "ResourceGroup"
// gallery_name = "GalleryName"
// image_name = "ImageName"
// image_version = "1.0.0"
// replication_regions = ["regionA", "regionB", "regionC"]
// }
// managed_image_name = "TargetImageName"
// managed_image_resource_group_name = "TargetResourceGroup"
// ```
SharedGalleryDestination SharedImageGalleryDestination `mapstructure:"shared_image_gallery_destination"`
// How long to wait for an image to be published to the shared image
// gallery before timing out. If your Packer build is failing on the
@@ -978,6 +1012,9 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
if len(c.SharedGalleryDestination.SigDestinationReplicationRegions) == 0 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A list of replication_regions must be specified for shared_image_gallery_destination"))
}
if c.SharedGalleryDestination.SigDestinationSubscription == "" {
c.SharedGalleryDestination.SigDestinationSubscription = c.ClientConfig.SubscriptionID
}
}
if c.SharedGalleryTimeout == 0 {
// default to a one-hour timeout. In the sdk, the default is 15 m.
+2
View File
@@ -311,6 +311,7 @@ func (*FlatSharedImageGallery) HCL2Spec() map[string]hcldec.Spec {
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatSharedImageGalleryDestination struct {
SigDestinationSubscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"`
SigDestinationResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"`
SigDestinationGalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"`
SigDestinationImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
@@ -330,6 +331,7 @@ func (*SharedImageGalleryDestination) FlatMapstructure() interface{ HCL2Spec() m
// The decoded values from this spec will then be applied to a FlatSharedImageGalleryDestination.
func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false},
"resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false},
"gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
+2
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
@@ -132,6 +133,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
+5 -93
View File
@@ -1,114 +1,26 @@
package docker
import (
"fmt"
"io"
"log"
"os/exec"
"regexp"
"strings"
"sync"
"syscall"
"github.com/hashicorp/packer/common/iochan"
"github.com/hashicorp/packer/helper/builder/localexec"
"github.com/hashicorp/packer/packer"
)
func runAndStream(cmd *exec.Cmd, ui packer.Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
args := make([]string, len(cmd.Args)-1)
copy(args, cmd.Args[1:])
// Scrub password from the log output.
capturedPassword := ""
for i, v := range args {
if v == "-p" || v == "--password" {
args[i+1] = "<Filtered>"
capturedPassword = args[i+1]
break
}
}
log.Printf("Executing: %s %v", cmd.Path, args)
cmd.Stdout = stdout_w
cmd.Stderr = stderr_w
if err := cmd.Start(); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan int, 1)
stdoutCh := iochan.LineReader(stdout_r)
stderrCh := iochan.LineReader(stderr_r)
// Start the goroutine to watch for the exit
go func() {
defer stdout_w.Close()
defer stderr_w.Close()
exitStatus := 0
err := cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus = 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
}
exitCh <- exitStatus
}()
// This waitgroup waits for the streaming to end
var streamWg sync.WaitGroup
streamWg.Add(2)
streamFunc := func(ch <-chan string) {
defer streamWg.Done()
for data := range ch {
data = cleanOutputLine(data)
if data != "" {
ui.Message(data)
}
}
}
// Stream stderr/stdout
go streamFunc(stderrCh)
go streamFunc(stdoutCh)
// Wait for the process to end and then wait for the streaming to end
exitStatus := <-exitCh
streamWg.Wait()
if exitStatus != 0 {
return fmt.Errorf("Bad exit status: %d", exitStatus)
}
return nil
}
// cleanOutputLine cleans up a line so that '\r' don't muck up the
// UI output when we're reading from a remote command.
func cleanOutputLine(line string) string {
// Build a regular expression that will get rid of shell codes
re := regexp.MustCompile("(?i)\x1b\\[([0-9]{1,2}(;[0-9]{1,2})?)?[a|b|m|k]")
line = re.ReplaceAllString(line, "")
// Trim surrounding whitespace
line = strings.TrimSpace(line)
// Trim up to the first carriage return, since that text would be
// lost anyways.
idx := strings.LastIndex(line, "\r")
if idx > -1 {
line = line[idx+1:]
}
return line
// run local command and stream output to UI.
return localexec.RunAndStream(cmd, ui, []string{capturedPassword})
}
+3 -2
View File
@@ -37,6 +37,7 @@ const (
type CommonConfig struct {
common.FloppyConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
// The block size of the VHD to be created.
// Recommended disk block size for Linux hyper-v guests is 1 MiB. This
// defaults to "32" MiB.
@@ -210,8 +211,8 @@ func (c *CommonConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
}
// Errors
floppyerrs := c.FloppyConfig.Prepare(ctx)
errs = append(errs, floppyerrs...)
errs = append(errs, c.FloppyConfig.Prepare(ctx)...)
errs = append(errs, c.CDConfig.Prepare(ctx)...)
if c.GuestAdditionsMode == "" {
if c.GuestAdditionsPath != "" {
c.GuestAdditionsMode = "attach"
@@ -34,7 +34,17 @@ func (s *StepMountSecondaryDvdImages) Run(ctx context.Context, state multistep.S
// For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1)
var dvdProperties []DvdControllerProperties
for _, isoPath := range s.IsoPaths {
isoPaths := s.IsoPaths
// Add our custom CD, if it exists
cd_path, ok := state.Get("cd_path").(string)
if ok {
if cd_path != "" {
isoPaths = append(isoPaths, cd_path)
}
}
for _, isoPath := range isoPaths {
var properties DvdControllerProperties
controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation)
+4 -1
View File
@@ -254,7 +254,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
GuestAdditionsPath: b.config.GuestAdditionsPath,
Generation: b.config.Generation,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&hypervcommon.StepMountSecondaryDvdImages{
IsoPaths: b.config.SecondaryDvdImages,
Generation: b.config.Generation,
+6
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -79,6 +80,8 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
@@ -136,6 +139,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -195,6 +199,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
+6
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -79,6 +80,8 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
@@ -138,6 +141,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -197,6 +201,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
+1 -1
View File
@@ -94,7 +94,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs, err)
}
for _, dc := range dcs {
if strings.ToLower(dc.CountryCode) == strings.ToLower(c.DataCenterName) {
if strings.EqualFold(dc.CountryCode, c.DataCenterName) {
c.DataCenterId = dc.Id
break
}
+20 -4
View File
@@ -1,4 +1,4 @@
//go:generate mapstructure-to-hcl2 -type Config
//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails
package oci
@@ -22,6 +22,19 @@ import (
ociauth "github.com/oracle/oci-go-sdk/common/auth"
)
type CreateVNICDetails struct {
// fields that can be specified under "create_vnic_details"
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false"`
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false"`
DisplayName *string `mapstructure:"display_name" required:"false"`
FreeformTags map[string]string `mapstructure:"tags" required:"false"`
HostnameLabel *string `mapstructure:"hostname_label" required:"false"`
NsgIds []string `mapstructure:"nsg_ids" required:"false"`
PrivateIp *string `mapstructure:"private_ip" required:"false"`
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false"`
SubnetId *string `mapstructure:"subnet_id" required:"false"`
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
@@ -57,11 +70,13 @@ type Config struct {
// Image
BaseImageID string `mapstructure:"base_image_ocid"`
Shape string `mapstructure:"shape"`
ImageName string `mapstructure:"image_name"`
// Instance
InstanceName string `mapstructure:"instance_name"`
InstanceName string `mapstructure:"instance_name"`
InstanceTags map[string]string `mapstructure:"instance_tags"`
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags"`
Shape string `mapstructure:"shape"`
// Metadata optionally contains custom metadata key/value pairs provided in the
// configuration. While this can be used to set metadata["user_data"] the explicit
@@ -75,7 +90,8 @@ type Config struct {
UserDataFile string `mapstructure:"user_data_file"`
// Networking
SubnetID string `mapstructure:"subnet_ocid"`
SubnetID string `mapstructure:"subnet_ocid"`
CreateVnicDetails CreateVNICDetails `mapstructure:"create_vnic_details"`
// Tagging
Tags map[string]string `mapstructure:"tags"`
+48 -3
View File
@@ -1,4 +1,4 @@
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
// Code generated by "mapstructure-to-hcl2 -type Config,CreateVNICDetails"; DO NOT EDIT.
package oci
import (
@@ -76,13 +76,16 @@ type FlatConfig struct {
AvailabilityDomain *string `mapstructure:"availability_domain" cty:"availability_domain" hcl:"availability_domain"`
CompartmentID *string `mapstructure:"compartment_ocid" cty:"compartment_ocid" hcl:"compartment_ocid"`
BaseImageID *string `mapstructure:"base_image_ocid" cty:"base_image_ocid" hcl:"base_image_ocid"`
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
InstanceName *string `mapstructure:"instance_name" cty:"instance_name" hcl:"instance_name"`
InstanceTags map[string]string `mapstructure:"instance_tags" cty:"instance_tags" hcl:"instance_tags"`
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags" cty:"instance_defined_tags" hcl:"instance_defined_tags"`
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
Metadata map[string]string `mapstructure:"metadata" cty:"metadata" hcl:"metadata"`
UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"`
UserDataFile *string `mapstructure:"user_data_file" cty:"user_data_file" hcl:"user_data_file"`
SubnetID *string `mapstructure:"subnet_ocid" cty:"subnet_ocid" hcl:"subnet_ocid"`
CreateVnicDetails *FlatCreateVNICDetails `mapstructure:"create_vnic_details" cty:"create_vnic_details" hcl:"create_vnic_details"`
Tags map[string]string `mapstructure:"tags" cty:"tags" hcl:"tags"`
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" cty:"defined_tags" hcl:"defined_tags"`
}
@@ -166,15 +169,57 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"availability_domain": &hcldec.AttrSpec{Name: "availability_domain", Type: cty.String, Required: false},
"compartment_ocid": &hcldec.AttrSpec{Name: "compartment_ocid", Type: cty.String, Required: false},
"base_image_ocid": &hcldec.AttrSpec{Name: "base_image_ocid", Type: cty.String, Required: false},
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false},
"instance_tags": &hcldec.AttrSpec{Name: "instance_tags", Type: cty.Map(cty.String), Required: false},
"instance_defined_tags": &hcldec.AttrSpec{Name: "instance_defined_tags", Type: cty.Map(cty.String), Required: false},
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
"metadata": &hcldec.AttrSpec{Name: "metadata", Type: cty.Map(cty.String), Required: false},
"user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false},
"user_data_file": &hcldec.AttrSpec{Name: "user_data_file", Type: cty.String, Required: false},
"subnet_ocid": &hcldec.AttrSpec{Name: "subnet_ocid", Type: cty.String, Required: false},
"create_vnic_details": &hcldec.BlockSpec{TypeName: "create_vnic_details", Nested: hcldec.ObjectSpec((*FlatCreateVNICDetails)(nil).HCL2Spec())},
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
}
return s
}
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatCreateVNICDetails struct {
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false" cty:"assign_public_ip" hcl:"assign_public_ip"`
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false" cty:"defined_tags" hcl:"defined_tags"`
DisplayName *string `mapstructure:"display_name" required:"false" cty:"display_name" hcl:"display_name"`
FreeformTags map[string]string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
HostnameLabel *string `mapstructure:"hostname_label" required:"false" cty:"hostname_label" hcl:"hostname_label"`
NsgIds []string `mapstructure:"nsg_ids" required:"false" cty:"nsg_ids" hcl:"nsg_ids"`
PrivateIp *string `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"`
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false" cty:"skip_source_dest_check" hcl:"skip_source_dest_check"`
SubnetId *string `mapstructure:"subnet_id" required:"false" cty:"subnet_id" hcl:"subnet_id"`
}
// FlatMapstructure returns a new FlatCreateVNICDetails.
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*CreateVNICDetails) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatCreateVNICDetails)
}
// HCL2Spec returns the hcl spec of a CreateVNICDetails.
// This spec is used by HCL to read the fields of CreateVNICDetails.
// The decoded values from this spec will then be applied to a FlatCreateVNICDetails.
func (*FlatCreateVNICDetails) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"assign_public_ip": &hcldec.AttrSpec{Name: "assign_public_ip", Type: cty.Bool, Required: false},
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
"hostname_label": &hcldec.AttrSpec{Name: "hostname_label", Type: cty.String, Required: false},
"nsg_ids": &hcldec.AttrSpec{Name: "nsg_ids", Type: cty.List(cty.String), Required: false},
"private_ip": &hcldec.AttrSpec{Name: "private_ip", Type: cty.String, Required: false},
"skip_source_dest_check": &hcldec.AttrSpec{Name: "skip_source_dest_check", Type: cty.Bool, Required: false},
"subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.String, Required: false},
}
return s
}
+10 -1
View File
@@ -21,7 +21,6 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
// Image
"base_image_ocid": "ocd1...",
"shape": "VM.Standard1.1",
"image_name": "HelloWorld",
// Networking
@@ -36,6 +35,16 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
"defined_tags": map[string]map[string]interface{}{
"namespace": {"key": "value"},
},
// Instance Details
"instance_name": "hello-world",
"instance_tags": map[string]string{
"key": "value",
},
"create_vnic_details": map[string]interface{}{
"nsg_ids": []string{"ocd1..."},
},
"shape": "VM.Standard1.1",
}
}
+17
View File
@@ -54,6 +54,8 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
instanceDetails := core.LaunchInstanceDetails{
AvailabilityDomain: &d.cfg.AvailabilityDomain,
CompartmentId: &d.cfg.CompartmentID,
DefinedTags: d.cfg.InstanceDefinedTags,
FreeformTags: d.cfg.InstanceTags,
ImageId: &d.cfg.BaseImageID,
Shape: &d.cfg.Shape,
SubnetId: &d.cfg.SubnetID,
@@ -65,6 +67,21 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
instanceDetails.DisplayName = &d.cfg.InstanceName
}
// Pass VNIC details, if specified, to the instance
CreateVnicDetails := core.CreateVnicDetails{
AssignPublicIp: d.cfg.CreateVnicDetails.AssignPublicIp,
DisplayName: d.cfg.CreateVnicDetails.DisplayName,
HostnameLabel: d.cfg.CreateVnicDetails.HostnameLabel,
NsgIds: d.cfg.CreateVnicDetails.NsgIds,
PrivateIp: d.cfg.CreateVnicDetails.PrivateIp,
SkipSourceDestCheck: d.cfg.CreateVnicDetails.SkipSourceDestCheck,
SubnetId: d.cfg.CreateVnicDetails.SubnetId,
DefinedTags: d.cfg.CreateVnicDetails.DefinedTags,
FreeformTags: d.cfg.CreateVnicDetails.FreeformTags,
}
instanceDetails.CreateVnicDetails = &CreateVnicDetails
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: instanceDetails})
if err != nil {
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -123,6 +124,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
+1 -1
View File
@@ -251,7 +251,7 @@ func (d *stepCreateServer) getImageAlias(imageAlias string, location string, ui
if i != "" {
alias = i
}
if alias != "" && strings.ToLower(alias) == strings.ToLower(imageAlias) {
if alias != "" && strings.EqualFold(alias, imageAlias) {
return alias
}
}
+77 -28
View File
@@ -2,6 +2,7 @@ package proxmox
import (
"fmt"
"strings"
"time"
"unicode"
@@ -10,23 +11,33 @@ import (
)
type proxmoxDriver struct {
client commandTyper
vmRef *proxmox.VmRef
specialMap map[string]string
runeMap map[rune]string
interval time.Duration
client commandTyper
vmRef *proxmox.VmRef
specialMap map[string]string
runeMap map[rune]string
interval time.Duration
specialBuffer []string
normalBuffer []string
}
func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Duration) *proxmoxDriver {
// Mappings for packer shorthand to qemu qkeycodes
sMap := map[string]string{
"spacebar": "spc",
"bs": "backspace",
"del": "delete",
"return": "ret",
"enter": "ret",
"pageUp": "pgup",
"pageDown": "pgdn",
"spacebar": "spc",
"bs": "backspace",
"del": "delete",
"return": "ret",
"enter": "ret",
"pageUp": "pgup",
"pageDown": "pgdn",
"leftshift": "shift",
"rightshift": "shift",
"leftalt": "alt",
"rightalt": "alt_r",
"leftctrl": "ctrl",
"rightctrl": "ctrl_r",
"leftsuper": "meta_l",
"rightsuper": "meta_r",
}
// Mappings for runes that need to be translated to special qkeycodes
// Taken from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
@@ -78,18 +89,26 @@ func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Durati
}
func (p *proxmoxDriver) SendKey(key rune, action bootcommand.KeyAction) error {
if special, ok := p.runeMap[key]; ok {
return p.send(special)
switch action.String() {
case "Press":
if special, ok := p.runeMap[key]; ok {
return p.send(special)
}
var keys string
if unicode.IsUpper(key) {
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
} else {
keys = fmt.Sprintf("%c", key)
}
return p.send(keys)
case "On":
key := fmt.Sprintf("%c", key)
p.normalBuffer = addKeyToBuffer(p.normalBuffer, key)
case "Off":
key := fmt.Sprintf("%c", key)
p.normalBuffer = removeKeyFromBuffer(p.normalBuffer, key)
}
var keys string
if unicode.IsUpper(key) {
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
} else {
keys = fmt.Sprintf("%c", key)
}
return p.send(keys)
return nil
}
func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction) error {
@@ -97,18 +116,48 @@ func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction
if replacement, ok := p.specialMap[special]; ok {
keys = replacement
}
return p.send(keys)
switch action.String() {
case "Press":
return p.send(keys)
case "On":
p.specialBuffer = addKeyToBuffer(p.specialBuffer, keys)
case "Off":
p.specialBuffer = removeKeyFromBuffer(p.specialBuffer, keys)
}
return nil
}
func (p *proxmoxDriver) send(keys string) error {
err := p.client.Sendkey(p.vmRef, keys)
func (p *proxmoxDriver) send(key string) error {
keys := append(p.specialBuffer, p.normalBuffer...)
keys = append(keys, key)
keyEventString := bufferToKeyEvent(keys)
err := p.client.Sendkey(p.vmRef, keyEventString)
if err != nil {
return err
}
time.Sleep(p.interval)
return nil
}
func (p *proxmoxDriver) Flush() error { return nil }
func bufferToKeyEvent(keys []string) string {
return strings.Join(keys, "-")
}
func addKeyToBuffer(buffer []string, key string) []string {
for _, value := range buffer {
if value == key {
return buffer
}
}
return append(buffer, key)
}
func removeKeyFromBuffer(buffer []string, key string) []string {
for index, value := range buffer {
if value == key {
buffer[index] = buffer[len(buffer)-1]
return buffer[:len(buffer)-1]
}
}
return buffer
}
+36 -6
View File
@@ -71,8 +71,21 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ResultKey: downloadPathKey,
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
},
}}
for idx := range b.config.AdditionalISOFiles {
steps = append(steps, &common.StepDownload{
Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum,
Description: "additional ISO",
Extension: b.config.AdditionalISOFiles[idx].TargetExtension,
ResultKey: b.config.AdditionalISOFiles[idx].downloadPathKey,
TargetPath: b.config.AdditionalISOFiles[idx].downloadPathKey,
Url: b.config.AdditionalISOFiles[idx].ISOUrls,
})
}
steps = append(steps,
&stepUploadISO{},
&stepUploadAdditionalISOs{},
&stepStartVM{},
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
@@ -96,7 +109,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&stepConvertToTemplate{},
&stepFinalizeTemplateConfig{},
&stepSuccess{},
}
)
// Run the steps
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
@@ -138,16 +152,32 @@ func commHost(host string) func(state multistep.StateBag) (string, error) {
// Reads the first non-loopback interface's IP address from the VM.
// qemu-guest-agent package must be installed on the VM
func getVMIP(state multistep.StateBag) (string, error) {
c := state.Get("proxmoxClient").(*proxmox.Client)
client := state.Get("proxmoxClient").(*proxmox.Client)
config := state.Get("config").(*Config)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
ifs, err := c.GetVmAgentNetworkInterfaces(vmRef)
ifs, err := client.GetVmAgentNetworkInterfaces(vmRef)
if err != nil {
return "", err
}
// TODO: Do something smarter here? Allow specifying interface? Or address family?
// For now, just go for first non-loopback
if config.VMInterface != "" {
for _, iface := range ifs {
if config.VMInterface != iface.Name {
continue
}
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface)
}
return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface)
}
for _, iface := range ifs {
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
+69 -1
View File
@@ -1,4 +1,4 @@
//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig
//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig,storageConfig
package proxmox
@@ -8,6 +8,7 @@ import (
"log"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -64,6 +65,9 @@ type Config struct {
shouldUploadISO bool
AdditionalISOFiles []storageConfig `mapstructure:"additional_iso_files"`
VMInterface string `mapstructure:"vm_interface"`
ctx interpolate.Context
}
@@ -87,6 +91,15 @@ type vgaConfig struct {
Type string `mapstructure:"type"`
Memory int `mapstructure:"memory"`
}
type storageConfig struct {
common.ISOConfig `mapstructure:",squash"`
Device string `mapstructure:"device"`
ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
Unmount bool `mapstructure:"unmount"`
shouldUploadISO bool
downloadPathKey string
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
// Agent defaults to true
@@ -183,6 +196,61 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("disk format must be specified for pool type %q", c.Disks[idx].StoragePoolType))
}
}
for idx := range c.AdditionalISOFiles {
// Check AdditionalISO config
// Either a pre-uploaded ISO should be referenced in iso_file, OR a URL
// (possibly to a local file) to an ISO file that will be downloaded and
// then uploaded to Proxmox.
if c.AdditionalISOFiles[idx].ISOFile != "" {
c.AdditionalISOFiles[idx].shouldUploadISO = false
} else {
c.AdditionalISOFiles[idx].downloadPathKey = "downloaded_additional_iso_path_" + strconv.Itoa(idx)
isoWarnings, isoErrors := c.AdditionalISOFiles[idx].ISOConfig.Prepare(&c.ctx)
errs = packer.MultiErrorAppend(errs, isoErrors...)
warnings = append(warnings, isoWarnings...)
c.AdditionalISOFiles[idx].shouldUploadISO = true
}
if c.AdditionalISOFiles[idx].Device == "" {
log.Printf("AdditionalISOFile %d Device not set, using default 'ide3'", idx)
c.AdditionalISOFiles[idx].Device = "ide3"
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "ide") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[3:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[3:]))
}
if busnumber == 2 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus 2 is used by boot ISO"))
}
if busnumber > 3 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus index can't be higher than 3"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "sata") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 5 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SATA bus index can't be higher than 5"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "scsi") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 30 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SCSI bus index can't be higher than 30"))
}
}
if (c.AdditionalISOFiles[idx].ISOFile == "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) == 0) || (c.AdditionalISOFiles[idx].ISOFile != "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) != 0) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("either iso_file or iso_url, but not both, must be specified for AdditionalISO file %s", c.AdditionalISOFiles[idx].Device))
}
if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified"))
}
}
if c.SCSIController == "" {
log.Printf("SCSI controller not set, using default 'lsi'")
c.SCSIController = "lsi"
+140 -95
View File
@@ -1,4 +1,4 @@
// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig"; DO NOT EDIT.
// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig,storageConfig"; DO NOT EDIT.
package proxmox
import (
@@ -9,100 +9,103 @@ import (
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"`
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"`
Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"`
Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"`
CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"`
Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"`
OS *string `mapstructure:"os" cty:"os" hcl:"os"`
VGA *FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"`
NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"`
Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"`
Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"`
DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"`
TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"`
TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"`
UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"`
CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"`
CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"`
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"`
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"`
Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"`
Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"`
CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"`
Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"`
OS *string `mapstructure:"os" cty:"os" hcl:"os"`
VGA *FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"`
NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"`
Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"`
Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"`
DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"`
TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"`
TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"`
UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"`
CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"`
CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"`
AdditionalISOFiles []FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"`
VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"`
}
// FlatMapstructure returns a new FlatConfig.
@@ -128,6 +131,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -211,6 +215,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"unmount_iso": &hcldec.AttrSpec{Name: "unmount_iso", Type: cty.Bool, Required: false},
"cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false},
"cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false},
"additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*FlatstorageConfig)(nil).HCL2Spec())},
"vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false},
}
return s
}
@@ -281,6 +287,45 @@ func (*FlatnicConfig) HCL2Spec() map[string]hcldec.Spec {
return s
}
// FlatstorageConfig is an auto-generated flat version of storageConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatstorageConfig struct {
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
Device *string `mapstructure:"device" cty:"device" hcl:"device"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
Unmount *bool `mapstructure:"unmount" cty:"unmount" hcl:"unmount"`
}
// FlatMapstructure returns a new FlatstorageConfig.
// FlatstorageConfig is an auto-generated flat version of storageConfig.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*storageConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatstorageConfig)
}
// HCL2Spec returns the hcl spec of a storageConfig.
// This spec is used by HCL to read the fields of storageConfig.
// The decoded values from this spec will then be applied to a FlatstorageConfig.
func (*FlatstorageConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
"iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false},
"iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false},
"device": &hcldec.AttrSpec{Name: "device", Type: cty.String, Required: false},
"iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false},
"iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false},
"unmount": &hcldec.AttrSpec{Name: "unmount", Type: cty.Bool, Required: false},
}
return s
}
// FlatvgaConfig is an auto-generated flat version of vgaConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatvgaConfig struct {
@@ -93,6 +93,29 @@ func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.St
}
}
if len(c.AdditionalISOFiles) > 0 {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for idx := range c.AdditionalISOFiles {
cdrom := c.AdditionalISOFiles[idx].Device
if c.AdditionalISOFiles[idx].Unmount {
if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes[cdrom] = "none,media=cdrom"
} else {
changes[cdrom] = c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom"
}
}
}
if len(changes) > 0 {
_, err := client.SetVmConfig(vmRef, changes)
if err != nil {
+13
View File
@@ -91,6 +91,19 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
// instance id inside of the provisioners, used in step_provision.
state.Put("instance_id", vmRef)
for idx := range c.AdditionalISOFiles {
params := map[string]interface{}{
c.AdditionalISOFiles[idx].Device: c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom",
}
_, err = client.SetVmConfig(vmRef, params)
if err != nil {
err := fmt.Errorf("Error configuring VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
ui.Say("Starting VM")
_, err = client.StartVm(vmRef)
if err != nil {
+31 -13
View File
@@ -53,14 +53,20 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt
}
}
httpIP, err := hostIP()
if err != nil {
err := fmt.Errorf("Failed to determine host IP: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
var httpIP string
var err error
if c.HTTPAddress != "0.0.0.0" {
httpIP = c.HTTPAddress
} else {
httpIP, err = hostIP(c.HTTPInterface)
if err != nil {
err := fmt.Errorf("Failed to determine host IP: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
state.Put("http_ip", httpIP)
s.Ctx.Data = &bootCommandTemplateData{
HTTPIP: httpIP,
@@ -97,12 +103,25 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
func hostIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", err
}
func hostIP(ifname string) (string, error) {
var addrs []net.Addr
var err error
if ifname != "" {
iface, err := net.InterfaceByName(ifname)
if err != nil {
return "", err
}
addrs, err = iface.Addrs()
if err != nil {
return "", err
}
} else {
addrs, err = net.InterfaceAddrs()
if err != nil {
return "", err
}
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
@@ -110,6 +129,5 @@ func hostIP() (string, error) {
}
}
}
return "", errors.New("No host IP found")
}
@@ -52,6 +52,34 @@ func TestTypeBootCommand(t *testing.T) {
expectedKeysSent: "shift-hellospcshift-worldspc2dot0fooshift-1barshift-2baz",
expectedAction: multistep.ActionContinue,
},
{
name: "holding and releasing keys",
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<leftShiftOn>hello<rightAltOn>world<leftShiftOff><rightAltOff>"}}},
expectCallSendkey: true,
expectedKeysSent: "shift-hshift-eshift-lshift-lshift-oshift-alt_r-wshift-alt_r-oshift-alt_r-rshift-alt_r-lshift-alt_r-d",
expectedAction: multistep.ActionContinue,
},
{
name: "holding multiple alphabetical keys and shift",
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn>n<leftShiftOff><cOff>"}}},
expectCallSendkey: true,
expectedKeysSent: "shift-c-n",
expectedAction: multistep.ActionContinue,
},
{
name: "noop keystrokes",
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff><leftAltOn><leftShiftOff><leftAltOff>"}}},
expectCallSendkey: true,
expectedKeysSent: "",
expectedAction: multistep.ActionContinue,
},
{
name: "noop keystrokes mixed",
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff>h<leftShiftOff>"}}},
expectCallSendkey: true,
expectedKeysSent: "shift-h",
expectedAction: multistep.ActionContinue,
},
{
name: "without boot command sendkey should not be called",
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{}}},
@@ -0,0 +1,68 @@
package proxmox
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// stepUploadAdditionalISOs uploads all additional ISO files that are mountet
// to the VM
type stepUploadAdditionalISOs struct{}
type uploader interface {
Upload(node string, storage string, contentType string, filename string, file io.Reader) error
}
var _ uploader = &proxmox.Client{}
func (s *stepUploadAdditionalISOs) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(uploader)
c := state.Get("config").(*Config)
for idx := range c.AdditionalISOFiles {
if !c.AdditionalISOFiles[idx].shouldUploadISO {
state.Put("additional_iso_files", c.AdditionalISOFiles)
continue
}
p := state.Get(c.AdditionalISOFiles[idx].downloadPathKey).(string)
if p == "" {
err := fmt.Errorf("Path to downloaded ISO was empty")
state.Put("erroe", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
isoPath, _ := filepath.EvalSymlinks(p)
r, err := os.Open(isoPath)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
filename := filepath.Base(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls[0])
err = client.Upload(c.Node, c.AdditionalISOFiles[idx].ISOStoragePool, "iso", filename, r)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
isoStoragePath := fmt.Sprintf("%s:iso/%s", c.AdditionalISOFiles[idx].ISOStoragePool, filename)
c.AdditionalISOFiles[idx].ISOFile = isoStoragePath
state.Put("additional_iso_files", c.AdditionalISOFiles)
}
return multistep.ActionContinue
}
func (s *stepUploadAdditionalISOs) Cleanup(state multistep.StateBag) {
}
-6
View File
@@ -3,7 +3,6 @@ package proxmox
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
@@ -15,10 +14,6 @@ import (
// stepUploadISO uploads an ISO file to Proxmox so we can boot from it
type stepUploadISO struct{}
type uploader interface {
Upload(node string, storage string, contentType string, filename string, file io.Reader) error
}
var _ uploader = &proxmox.Client{}
func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
@@ -58,7 +53,6 @@ func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multi
isoStoragePath := fmt.Sprintf("%s:iso/%s", c.ISOStoragePool, filename)
state.Put("iso_file", isoStoragePath)
return multistep.ActionContinue
}
+9
View File
@@ -130,6 +130,10 @@ type Config struct {
// for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size
// number is provided with no units, Packer will default to Megabytes.
DiskSize string `mapstructure:"disk_size" required:"false"`
// Packer resizes the QCOW2 image using
// qemu-img resize. Set this option to true to disable resizing.
// Defaults to false.
SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"`
// The cache mode to use for disk. Allowed values include any of
// `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By
// default, this is set to `writeback`.
@@ -509,6 +513,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
errs, errors.New("disk_additional_size can only be used when disk_image is false"))
}
if b.config.SkipResizeDisk && !(b.config.DiskImage) {
errs = packer.MultiErrorAppend(
errs, errors.New("skip_resize_disk can only be used when disk_image is true"))
}
if _, ok := accels[b.config.Accelerator]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
+4
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -93,6 +94,7 @@ type FlatConfig struct {
CpuCount *int `mapstructure:"cpus" required:"false" cty:"cpus" hcl:"cpus"`
DiskInterface *string `mapstructure:"disk_interface" required:"false" cty:"disk_interface" hcl:"disk_interface"`
DiskSize *string `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"`
SkipResizeDisk *bool `mapstructure:"skip_resize_disk" required:"false" cty:"skip_resize_disk" hcl:"skip_resize_disk"`
DiskCache *string `mapstructure:"disk_cache" required:"false" cty:"disk_cache" hcl:"disk_cache"`
DiskDiscard *string `mapstructure:"disk_discard" required:"false" cty:"disk_discard" hcl:"disk_discard"`
DetectZeroes *string `mapstructure:"disk_detect_zeroes" required:"false" cty:"disk_detect_zeroes" hcl:"disk_detect_zeroes"`
@@ -145,6 +147,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -218,6 +221,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"cpus": &hcldec.AttrSpec{Name: "cpus", Type: cty.Number, Required: false},
"disk_interface": &hcldec.AttrSpec{Name: "disk_interface", Type: cty.String, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.String, Required: false},
"skip_resize_disk": &hcldec.AttrSpec{Name: "skip_resize_disk", Type: cty.Bool, Required: false},
"disk_cache": &hcldec.AttrSpec{Name: "disk_cache", Type: cty.String, Required: false},
"disk_discard": &hcldec.AttrSpec{Name: "disk_discard", Type: cty.String, Required: false},
"disk_detect_zeroes": &hcldec.AttrSpec{Name: "disk_detect_zeroes", Type: cty.String, Required: false},
+15
View File
@@ -302,6 +302,21 @@ func TestBuilderPrepare_UseBackingFile(t *testing.T) {
}
}
func TestBuilderPrepare_SkipResizeDisk(t *testing.T) {
config := testConfig()
config["skip_resize_disk"] = true
config["disk_image"] = false
b := Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns)
}
if err == nil {
t.Errorf("setting skip_resize_disk to true when disk_image is false should have error")
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()
+64
View File
@@ -0,0 +1,64 @@
package qemu
import "sync"
type DriverMock struct {
sync.Mutex
StopCalled bool
StopErr error
QemuCalls [][]string
QemuErrs []error
WaitForShutdownCalled bool
WaitForShutdownState bool
QemuImgCalls [][]string
QemuImgErrs []error
VerifyCalled bool
VerifyErr error
VersionCalled bool
VersionResult string
VersionErr error
}
func (d *DriverMock) Stop() error {
d.StopCalled = true
return d.StopErr
}
func (d *DriverMock) Qemu(args ...string) error {
d.QemuCalls = append(d.QemuCalls, args)
if len(d.QemuErrs) >= len(d.QemuCalls) {
return d.QemuErrs[len(d.QemuCalls)-1]
}
return nil
}
func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
d.WaitForShutdownCalled = true
return d.WaitForShutdownState
}
func (d *DriverMock) QemuImg(args ...string) error {
d.QemuImgCalls = append(d.QemuImgCalls, args)
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
return d.QemuImgErrs[len(d.QemuImgCalls)-1]
}
return nil
}
func (d *DriverMock) Verify() error {
d.VerifyCalled = true
return d.VerifyErr
}
func (d *DriverMock) Version() (string, error) {
d.VersionCalled = true
return d.VersionResult, d.VersionErr
}
+2 -1
View File
@@ -25,7 +25,8 @@ func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) mult
path,
config.DiskSize,
}
if config.DiskImage == false {
if config.DiskImage == false || config.SkipResizeDisk == true {
return multistep.ActionContinue
}
+82
View File
@@ -0,0 +1,82 @@
package qemu
import (
"context"
"testing"
"github.com/hashicorp/packer/helper/multistep"
)
func TestStepResizeDisk_Run(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: true,
SkipResizeDisk: false,
DiskSize: "4096M",
Format: "qcow2",
OutputDir: "/test/",
VMName: "test",
}
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) == 0 {
t.Fatal("should qemu-img called")
}
if len(driver.QemuImgCalls[0]) != 5 {
t.Fatal("should 5 qemu-img parameters")
}
}
func TestStepResizeDisk_SkipIso(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: false,
SkipResizeDisk: false,
}
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) > 0 {
t.Fatal("should NOT qemu-img called")
}
}
func TestStepResizeDisk_SkipOption(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: false,
SkipResizeDisk: true,
}
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) > 0 {
t.Fatal("should NOT qemu-img called")
}
}
+4 -5
View File
@@ -73,12 +73,11 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
var deviceArgs []string
var driveArgs []string
var commHostPort int
var vnc string
if !config.VNCUsePassword {
vnc = fmt.Sprintf("%s:%d", vncIP, vncPort-5900)
} else {
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort-5900)
vncPort = vncPort - config.VNCPortMin
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort)
if config.VNCUsePassword {
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
}
if config.QMPEnable {
+19
View File
@@ -0,0 +1,19 @@
package qemu
import (
"bytes"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func testState(t *testing.T) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("driver", new(DriverMock))
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
return state
}
+18 -9
View File
@@ -4,7 +4,8 @@ import (
"fmt"
"log"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type Artifact struct {
@@ -20,11 +21,11 @@ type Artifact struct {
// The ID of the snapshot
snapshotID string
// The name of the region
regionName string
// The name of the zone
zoneName string
// The client for making API calls
client *api.ScalewayAPI
client *scw.Client
// StateData should store data such as GeneratedData
// to be shared with post-processors
@@ -41,12 +42,12 @@ func (*Artifact) Files() []string {
}
func (a *Artifact) Id() string {
return fmt.Sprintf("%s:%s", a.regionName, a.imageID)
return fmt.Sprintf("%s:%s", a.zoneName, a.imageID)
}
func (a *Artifact) String() string {
return fmt.Sprintf("An image was created: '%v' (ID: %v) in region '%v' based on snapshot '%v' (ID: %v)",
a.imageName, a.imageID, a.regionName, a.snapshotName, a.snapshotID)
return fmt.Sprintf("An image was created: '%v' (ID: %v) in zone '%v' based on snapshot '%v' (ID: %v)",
a.imageName, a.imageID, a.zoneName, a.snapshotName, a.snapshotID)
}
func (a *Artifact) State(name string) interface{} {
@@ -55,11 +56,19 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s (%s)", a.imageID, a.imageName)
if err := a.client.DeleteImage(a.imageID); err != nil {
instanceAPI := instance.NewAPI(a.client)
err := instanceAPI.DeleteImage(&instance.DeleteImageRequest{
ImageID: a.imageID,
})
if err != nil {
return err
}
log.Printf("Destroying snapshot: %s (%s)", a.snapshotID, a.snapshotName)
if err := a.client.DeleteSnapshot(a.snapshotID); err != nil {
err = instanceAPI.DeleteSnapshot(&instance.DeleteSnapshotRequest{
SnapshotID: a.snapshotID,
})
if err != nil {
return err
}
return nil
+1 -1
View File
@@ -27,7 +27,7 @@ func TestArtifactId(t *testing.T) {
func TestArtifactString(t *testing.T) {
generatedData := make(map[string]interface{})
a := &Artifact{"packer-foobar-image", "cc586e45-5156-4f71-b223-cf406b10dd1d", "packer-foobar-snapshot", "cc586e45-5156-4f71-b223-cf406b10dd1c", "ams1", nil, generatedData}
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in region 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in zone 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
+19 -4
View File
@@ -13,7 +13,7 @@ import (
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/scw"
)
// The unique id for the builder
@@ -32,12 +32,27 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
return nil, warnings, errs
}
return nil, nil, nil
return nil, warnings, nil
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
client, err := api.NewScalewayAPI(b.config.Organization, b.config.Token, b.config.UserAgent, b.config.Region)
scwZone, err := scw.ParseZone(b.config.Zone)
if err != nil {
ui.Error(err.Error())
return nil, err
}
clientOpts := []scw.ClientOption{
scw.WithDefaultProjectID(b.config.ProjectID),
scw.WithAuth(b.config.AccessKey, b.config.SecretKey),
scw.WithDefaultZone(scwZone),
}
if b.config.APIURL != "" {
clientOpts = append(clientOpts, scw.WithAPIURL(b.config.APIURL))
}
client, err := scw.NewClient(clientOpts...)
if err != nil {
ui.Error(err.Error())
return nil, err
@@ -96,7 +111,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
imageID: state.Get("image_id").(string),
snapshotName: state.Get("snapshot_name").(string),
snapshotID: state.Get("snapshot_id").(string),
regionName: state.Get("region").(string),
zoneName: b.config.Zone,
client: client,
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}
+11 -13
View File
@@ -9,9 +9,10 @@ import (
func testConfig() map[string]interface{} {
return map[string]interface{}{
"organization_id": "foo",
"api_token": "bar",
"region": "ams1",
"project_id": "00000000-1111-2222-3333-444444444444",
"access_key": "SCWABCXXXXXXXXXXXXXX",
"secret_key": "00000000-1111-2222-3333-444444444444",
"zone": "fr-par-1",
"commercial_type": "START1-S",
"ssh_username": "root",
"image": "image-uuid",
@@ -32,10 +33,7 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
"api_token": []string{},
}
_, warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
_, _, err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
@@ -68,11 +66,11 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
}
}
func TestBuilderPrepare_Region(t *testing.T) {
func TestBuilderPrepare_Zone(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "region")
delete(config, "zone")
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
@@ -81,9 +79,9 @@ func TestBuilderPrepare_Region(t *testing.T) {
t.Fatalf("should error")
}
expected := "ams1"
expected := "fr-par-1"
config["region"] = expected
config["zone"] = expected
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
@@ -93,8 +91,8 @@ func TestBuilderPrepare_Region(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != expected {
t.Errorf("found %s, expected %s", b.config.Region, expected)
if b.config.Zone != expected {
t.Errorf("found %s, expected %s", b.config.Zone, expected)
}
}
+90 -25
View File
@@ -17,27 +17,29 @@ import (
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"github.com/mitchellh/mapstructure"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
// The token to use to authenticate with your account.
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
// can see and generate tokens in the "Credentials"
// section of the control panel.
Token string `mapstructure:"api_token" required:"true"`
// The organization id to use to identify your
// organization. It can also be specified via environment variable
// SCALEWAY_ORGANIZATION. Your organization id is available in the
// "Account" section of the
// control panel.
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
Organization string `mapstructure:"organization_id" required:"true"`
// The name of the region to launch the server in (par1
// or ams1). Consequently, this is the region where the snapshot will be
// available.
Region string `mapstructure:"region" required:"true"`
// The AccessKey corresponding to the secret key.
// It can also be specified via the environment variable SCW_ACCESS_KEY.
AccessKey string `mapstructure:"access_key" required:"true"`
// The SecretKey to authenticate against the Scaleway API.
// It can also be specified via the environment variable SCW_SECRET_KEY.
SecretKey string `mapstructure:"secret_key" required:"true"`
// The Project ID in which the instances, volumes and snapshots will be created.
// It can also be specified via the environment variable SCW_DEFAULT_PROJECT_ID.
ProjectID string `mapstructure:"project_id" required:"true"`
// The Zone in which the instances, volumes and snapshots will be created.
// It can also be specified via the environment variable SCW_DEFAULT_ZONE
Zone string `mapstructure:"zone" required:"true"`
// The Scaleway API URL to use
// It can also be specified via the environment variable SCW_API_URL
APIURL string `mapstructure:"api_url"`
// The UUID of the base image to use. This is the image
// that will be used to launch a new server and provision it. See
// the images list
@@ -69,6 +71,28 @@ type Config struct {
UserAgent string `mapstructure-to-hcl2:",skip"`
ctx interpolate.Context
// Deprecated configs
// The token to use to authenticate with your account.
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
// can see and generate tokens in the "Credentials"
// section of the control panel.
// Deprecated, use SecretKey instead
Token string `mapstructure:"api_token" required:"false"`
// The organization id to use to identify your
// organization. It can also be specified via environment variable
// SCALEWAY_ORGANIZATION. Your organization id is available in the
// "Account" section of the
// control panel.
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
// Deprecated, use ProjectID instead
Organization string `mapstructure:"organization_id" required:"false"`
// The name of the region to launch the server in (par1
// or ams1). Consequently, this is the region where the snapshot will be
// available.
// Deprecated, use Zone instead
Region string `mapstructure:"region" required:"false"`
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
@@ -88,8 +112,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
return nil, err
}
var warnings []string
c.UserAgent = useragent.String()
// Deprecated variables
if c.Organization == "" {
if os.Getenv("SCALEWAY_ORGANIZATION") != "" {
c.Organization = os.Getenv("SCALEWAY_ORGANIZATION")
@@ -98,10 +125,43 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.Organization = os.Getenv("SCALEWAY_API_ACCESS_KEY")
}
}
if c.Organization != "" {
warnings = append(warnings, "organization_id is deprecated in favor of project_id")
c.ProjectID = c.Organization
}
if c.Token == "" {
c.Token = os.Getenv("SCALEWAY_API_TOKEN")
}
if c.Token != "" {
warnings = append(warnings, "token is deprecated in favor of secret_key")
c.SecretKey = c.Token
}
if c.Region != "" {
warnings = append(warnings, "region is deprecated in favor of zone")
c.Zone = c.Region
}
if c.AccessKey == "" {
c.AccessKey = os.Getenv(scw.ScwAccessKeyEnv)
}
if c.SecretKey == "" {
c.SecretKey = os.Getenv(scw.ScwSecretKeyEnv)
}
if c.ProjectID == "" {
c.ProjectID = os.Getenv(scw.ScwDefaultProjectIDEnv)
}
if c.Zone == "" {
c.Zone = os.Getenv(scw.ScwDefaultZoneEnv)
}
if c.APIURL == "" {
c.APIURL = os.Getenv(scw.ScwAPIURLEnv)
}
if c.SnapshotName == "" {
def, err := interpolate.Render("snapshot-packer-{{timestamp}}", nil)
@@ -127,26 +187,31 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
if c.BootType == "" {
c.BootType = "bootscript"
c.BootType = instance.BootTypeLocal.String()
}
var errs *packer.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Organization == "" {
if c.ProjectID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Scaleway Organization ID must be specified"))
errs, errors.New("Scaleway Project ID must be specified"))
}
if c.Token == "" {
if c.SecretKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Scaleway Token must be specified"))
errs, errors.New("Scaleway Secret Key must be specified"))
}
if c.Region == "" {
if c.AccessKey == "" {
warnings = append(warnings, "access_key will be required in future versions")
c.AccessKey = "SCWXXXXXXXXXXXXXXXXX"
}
if c.Zone == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("region is required"))
errs, errors.New("Scaleway Zone is required"))
}
if c.CommercialType == "" {
@@ -160,9 +225,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
return warnings, errs
}
packer.LogSecretFilter.Set(c.Token)
return nil, nil
return warnings, nil
}
+16 -6
View File
@@ -63,9 +63,11 @@ type FlatConfig struct {
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
Token *string `mapstructure:"api_token" required:"true" cty:"api_token" hcl:"api_token"`
Organization *string `mapstructure:"organization_id" required:"true" cty:"organization_id" hcl:"organization_id"`
Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
ProjectID *string `mapstructure:"project_id" required:"true" cty:"project_id" hcl:"project_id"`
Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"`
APIURL *string `mapstructure:"api_url" cty:"api_url" hcl:"api_url"`
Image *string `mapstructure:"image" required:"true" cty:"image" hcl:"image"`
CommercialType *string `mapstructure:"commercial_type" required:"true" cty:"commercial_type" hcl:"commercial_type"`
SnapshotName *string `mapstructure:"snapshot_name" required:"false" cty:"snapshot_name" hcl:"snapshot_name"`
@@ -74,6 +76,9 @@ type FlatConfig struct {
Bootscript *string `mapstructure:"bootscript" required:"false" cty:"bootscript" hcl:"bootscript"`
BootType *string `mapstructure:"boottype" required:"false" cty:"boottype" hcl:"boottype"`
RemoveVolume *bool `mapstructure:"remove_volume" cty:"remove_volume" hcl:"remove_volume"`
Token *string `mapstructure:"api_token" required:"false" cty:"api_token" hcl:"api_token"`
Organization *string `mapstructure:"organization_id" required:"false" cty:"organization_id" hcl:"organization_id"`
Region *string `mapstructure:"region" required:"false" cty:"region" hcl:"region"`
}
// FlatMapstructure returns a new FlatConfig.
@@ -142,9 +147,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
"project_id": &hcldec.AttrSpec{Name: "project_id", Type: cty.String, Required: false},
"zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false},
"api_url": &hcldec.AttrSpec{Name: "api_url", Type: cty.String, Required: false},
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
"commercial_type": &hcldec.AttrSpec{Name: "commercial_type", Type: cty.String, Required: false},
"snapshot_name": &hcldec.AttrSpec{Name: "snapshot_name", Type: cty.String, Required: false},
@@ -153,6 +160,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"bootscript": &hcldec.AttrSpec{Name: "bootscript", Type: cty.String, Required: false},
"boottype": &hcldec.AttrSpec{Name: "boottype", Type: cty.String, Required: false},
"remove_volume": &hcldec.AttrSpec{Name: "remove_volume", Type: cty.Bool, Required: false},
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
}
return s
}
+18 -9
View File
@@ -7,13 +7,14 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepImage struct{}
func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*Config)
snapshotID := state.Get("snapshot_id").(string)
@@ -21,7 +22,9 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName))
image, err := client.GetImage(c.Image)
imageResp, err := instanceAPI.GetImage(&instance.GetImageRequest{
ImageID: c.Image,
})
if err != nil {
err := fmt.Errorf("Error getting initial image info: %s", err)
state.Put("error", err)
@@ -29,11 +32,16 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
return multistep.ActionHalt
}
if image.DefaultBootscript != nil {
bootscriptID = image.DefaultBootscript.Identifier
if imageResp.Image.DefaultBootscript != nil {
bootscriptID = imageResp.Image.DefaultBootscript.ID
}
imageID, err := client.PostImage(snapshotID, c.ImageName, bootscriptID, image.Arch)
createImageResp, err := instanceAPI.CreateImage(&instance.CreateImageRequest{
Arch: imageResp.Image.Arch,
DefaultBootscript: bootscriptID,
Name: c.ImageName,
RootVolume: snapshotID,
})
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
@@ -41,10 +49,11 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
return multistep.ActionHalt
}
log.Printf("Image ID: %s", imageID)
state.Put("image_id", imageID)
log.Printf("Image ID: %s", createImageResp.Image.ID)
state.Put("image_id", createImageResp.Image.ID)
state.Put("image_name", c.ImageName)
state.Put("region", c.Region)
state.Put("region", c.Zone) // Deprecated
state.Put("zone", c.Zone)
return multistep.ActionContinue
}
+30 -21
View File
@@ -7,7 +7,8 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepCreateServer struct {
@@ -15,7 +16,7 @@ type stepCreateServer struct {
}
func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*Config)
tags := []string{}
@@ -31,16 +32,16 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
tags = []string{fmt.Sprintf("AUTHORIZED_KEY=%s", strings.Replace(strings.TrimSpace(string(c.Comm.SSHPublicKey)), " ", "_", -1))}
}
server, err := client.PostServer(api.ScalewayServerDefinition{
Name: c.ServerName,
Image: &c.Image,
Organization: c.Organization,
CommercialType: c.CommercialType,
Tags: tags,
Bootscript: bootscript,
BootType: c.BootType,
})
bootType := instance.BootType(c.BootType)
createServerResp, err := instanceAPI.CreateServer(&instance.CreateServerRequest{
BootType: &bootType,
Bootscript: bootscript,
CommercialType: c.CommercialType,
Name: c.ServerName,
Image: c.Image,
Tags: tags,
})
if err != nil {
err := fmt.Errorf("Error creating server: %s", err)
state.Put("error", err)
@@ -48,8 +49,10 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
return multistep.ActionHalt
}
err = client.PostServerAction(server, "poweron")
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
Action: instance.ServerActionPoweron,
ServerID: createServerResp.Server.ID,
})
if err != nil {
err := fmt.Errorf("Error starting server: %s", err)
state.Put("error", err)
@@ -57,9 +60,9 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
return multistep.ActionHalt
}
s.serverID = server
s.serverID = createServerResp.Server.ID
state.Put("server_id", server)
state.Put("server_id", createServerResp.Server.ID)
// instance_id is the generic term used so that users can have access to the
// instance id inside of the provisioners, used in step_provision.
state.Put("instance_id", s.serverID)
@@ -72,16 +75,22 @@ func (s *stepCreateServer) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
ui.Say("Destroying server...")
err := client.DeleteServerForce(s.serverID)
err := instanceAPI.DeleteServer(&instance.DeleteServerRequest{
ServerID: s.serverID,
})
if err != nil {
ui.Error(fmt.Sprintf(
"Error destroying server. Please destroy it manually: %s", err))
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
Action: instance.ServerActionTerminate,
ServerID: s.serverID,
})
if err != nil {
ui.Error(fmt.Sprintf(
"Error destroying server. Please destroy it manually: %s", err))
}
}
}
+6 -4
View File
@@ -4,10 +4,10 @@ import (
"context"
"fmt"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepRemoveVolume struct{}
@@ -24,7 +24,7 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*Config)
volumeID := state.Get("root_volume_id").(string)
@@ -35,7 +35,9 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
ui.Say("Removing Volume ...")
err := client.DeleteVolume(volumeID)
err := instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
VolumeID: volumeID,
})
if err != nil {
err := fmt.Errorf("Error removing volume: %s", err)
state.Put("error", err)
+17 -8
View File
@@ -6,19 +6,22 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepServerInfo struct{}
func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
serverID := state.Get("server_id").(string)
ui.Say("Waiting for server to become active...")
_, err := api.WaitForServerState(client, serverID, "running")
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
ServerID: serverID,
})
if err != nil {
err := fmt.Errorf("Error waiting for server to become booted: %s", err)
state.Put("error", err)
@@ -26,16 +29,22 @@ func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) mult
return multistep.ActionHalt
}
server, err := client.GetServer(serverID)
if err != nil {
err := fmt.Errorf("Error retrieving server: %s", err)
if instanceResp.State != instance.ServerStateRunning {
err := fmt.Errorf("Server is in state %s", instanceResp.State.String())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("server_ip", server.PublicAddress.IP)
state.Put("root_volume_id", server.Volumes["0"].Identifier)
if instanceResp.PublicIP == nil {
err := fmt.Errorf("Server does not have a public IP")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("server_ip", instanceResp.PublicIP.Address.String())
state.Put("root_volume_id", instanceResp.Volumes["0"].ID)
return multistep.ActionContinue
}
+17 -6
View File
@@ -6,20 +6,23 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
serverID := state.Get("server_id").(string)
ui.Say("Shutting down server...")
err := client.PostServerAction(serverID, "poweroff")
_, err := instanceAPI.ServerAction(&instance.ServerActionRequest{
Action: instance.ServerActionPoweroff,
ServerID: serverID,
})
if err != nil {
err := fmt.Errorf("Error stopping server: %s", err)
state.Put("error", err)
@@ -27,8 +30,9 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionHalt
}
_, err = api.WaitForServerState(client, serverID, "stopped")
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
ServerID: serverID,
})
if err != nil {
err := fmt.Errorf("Error shutting down server: %s", err)
state.Put("error", err)
@@ -36,6 +40,13 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionHalt
}
if instanceResp.State != instance.ServerStateStopped {
err := fmt.Errorf("Server is in state %s instead of stopped", instanceResp.State.String())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
+11 -6
View File
@@ -7,19 +7,23 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
type stepSnapshot struct{}
func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*Config)
volumeID := state.Get("root_volume_id").(string)
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
snapshot, err := client.PostSnapshot(volumeID, c.SnapshotName)
createSnapshotResp, err := instanceAPI.CreateSnapshot(&instance.CreateSnapshotRequest{
Name: c.SnapshotName,
VolumeID: volumeID,
})
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
@@ -27,10 +31,11 @@ func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionHalt
}
log.Printf("Snapshot ID: %s", snapshot)
state.Put("snapshot_id", snapshot)
log.Printf("Snapshot ID: %s", createSnapshotResp.Snapshot.ID)
state.Put("snapshot_id", createSnapshotResp.Snapshot.ID)
state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region)
state.Put("region", c.Zone) // Deprecated
state.Put("zone", c.Zone)
return multistep.ActionContinue
}
+2
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -125,6 +126,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -146,6 +147,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
@@ -133,6 +134,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
+2
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
@@ -130,6 +131,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
+42
View File
@@ -0,0 +1,42 @@
//go:generate struct-markdown
package common
import (
"fmt"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/template/interpolate"
)
type BootConfigWrapper struct {
bootcommand.VNCConfig `mapstructure:",squash"`
// If set to true, Packer will use USB HID Keyboard scan codes to send the boot command to the VM and
// the [disable_vnc](#disable_vnc) option will be ignored and automatically set to true.
// This option is not supported by hosts with free license.
//
// ~> **Note:** The ESXi 6.5+ removes support to VNC. In this case, the `usb_keyboard` should be set to true
// in order to send boot command keystrokes to the VM.
USBKeyBoard bool `mapstructure:"usb_keyboard"`
}
func (c *BootConfigWrapper) Prepare(ctx *interpolate.Context, driverConfig *DriverConfig) (warnings []string, errs []error) {
if c.USBKeyBoard {
if driverConfig.RemoteType == "" {
warnings = append(warnings, "[WARN] `usb_keyboard` can only be used with remote VMWare builds. "+
"The `usb_keyboard` option will be ignored and automatically set to false.")
c.USBKeyBoard = false
} else if !c.DisableVNC {
warnings = append(warnings, "[WARN] `usb_keyboard` is set to true then the remote VMWare builds "+
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true.")
c.DisableVNC = true
return
}
}
if len(c.BootCommand) > 0 && c.DisableVNC {
errs = append(errs,
fmt.Errorf("A boot command cannot be used when vnc is disabled."))
}
errs = append(errs, c.BootConfig.Prepare(ctx)...)
return
}
+106
View File
@@ -0,0 +1,106 @@
package common
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/template/interpolate"
)
func TestVNCConfigWrapper_Prepare(t *testing.T) {
tc := []struct {
name string
config *BootConfigWrapper
expectedConfig *BootConfigWrapper
driver *DriverConfig
errs []error
warnings []string
}{
{
name: "VNC and boot command for local build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: true,
},
USBKeyBoard: false,
},
expectedConfig: nil,
driver: new(DriverConfig),
errs: []error{fmt.Errorf("A boot command cannot be used when vnc is disabled.")},
warnings: nil,
},
{
name: "Disable VNC warning for remote build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: false,
},
USBKeyBoard: true,
},
expectedConfig: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
DisableVNC: true,
},
USBKeyBoard: true,
},
driver: &DriverConfig{
RemoteType: "esxi",
},
errs: nil,
warnings: []string{"[WARN] `usb_keyboard` is set to true then the remote VMWare builds " +
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true."},
},
{
name: "Disable USBKeyBoard warning for local build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: false,
},
USBKeyBoard: true,
},
expectedConfig: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
DisableVNC: false,
},
USBKeyBoard: false,
},
driver: &DriverConfig{},
errs: nil,
warnings: []string{"[WARN] `usb_keyboard` can only be used with remote VMWare builds. " +
"The `usb_keyboard` option will be ignored and automatically set to false."},
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
warnings, errs := c.config.Prepare(interpolate.NewContext(), c.driver)
if !reflect.DeepEqual(errs, c.errs) {
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
}
if diff := cmp.Diff(warnings, c.warnings); diff != "" {
t.Fatalf("unexpected warnings: %s", diff)
}
if len(c.errs) == 0 {
if diff := cmp.Diff(c.config, c.expectedConfig,
cmpopts.IgnoreFields(bootcommand.VNCConfig{},
"BootConfig",
)); diff != "" {
t.Fatalf("unexpected config: %s", diff)
}
}
})
}
}
+26 -13
View File
@@ -80,6 +80,9 @@ type Driver interface {
// Export the vm to ovf or ova format using ovftool
Export([]string) error
// OvfTool
VerifyOvfTool(bool, bool) error
}
// NewDriver returns a new driver implementation for this operating
@@ -88,20 +91,11 @@ func NewDriver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver,
drivers := []Driver{}
if dconfig.RemoteType != "" {
drivers = []Driver{
&ESX5Driver{
Host: dconfig.RemoteHost,
Port: dconfig.RemotePort,
Username: dconfig.RemoteUser,
Password: dconfig.RemotePassword,
PrivateKeyFile: dconfig.RemotePrivateKey,
Datastore: dconfig.RemoteDatastore,
CacheDatastore: dconfig.RemoteCacheDatastore,
CacheDirectory: dconfig.RemoteCacheDirectory,
VMName: vmName,
CommConfig: config.Comm,
},
esx5Driver, err := NewESX5Driver(dconfig, config, vmName)
if err != nil {
return nil, err
}
drivers = []Driver{esx5Driver}
} else {
switch runtime.GOOS {
@@ -628,3 +622,22 @@ func (d *VmwareDriver) Export(args []string) error {
return nil
}
func (d *VmwareDriver) VerifyOvfTool(SkipExport, _ bool) error {
if SkipExport {
return nil
}
log.Printf("Verifying that ovftool exists...")
// Validate that tool exists, but no need to validate credentials.
ovftool := GetOVFTool()
if ovftool != "" {
return nil
} else {
return fmt.Errorf("Couldn't find ovftool in path! Please either " +
"set `skip_export = true` and remove the `format` option " +
"from your template, or make sure ovftool is installed on " +
"your build system. ")
}
}
+15 -67
View File
@@ -3,14 +3,8 @@
package common
import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/hashicorp/packer/template/interpolate"
)
@@ -58,6 +52,8 @@ type DriverConfig struct {
}
func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
if c.FusionAppPath == "" {
c.FusionAppPath = os.Getenv("FUSION_APP_PATH")
}
@@ -80,7 +76,19 @@ func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
c.RemotePort = 22
}
return nil
if c.RemoteType != "" {
if c.RemoteHost == "" {
errs = append(errs,
fmt.Errorf("remote_host must be specified"))
}
if c.RemoteType != "esx5" {
errs = append(errs,
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
}
}
return errs
}
func (c *DriverConfig) Validate(SkipExport bool) error {
@@ -93,65 +101,5 @@ func (c *DriverConfig) Validate(SkipExport bool) error {
"that you set a value for remote_password")
}
if c.RemoteType == "" {
// Validate that tool exists, but no need to validate credentials.
ovftool := GetOVFTool()
if ovftool != "" {
return nil
} else {
return fmt.Errorf("Couldn't find ovftool in path! Please either " +
"set `skip_export = true` and remove the `format` option " +
"from your template, or make sure ovftool is installed on " +
"your build system. ")
}
}
if c.SkipValidateCredentials {
return nil
}
// check that password is valid by sending a dummy ovftool command
// now, so that we don't fail for a simple mistake after a long
// build
ovftool := GetOVFTool()
// Generate the uri of the host, with embedded credentials
ovftool_uri := fmt.Sprintf("vi://%s", c.RemoteHost)
u, err := url.Parse(ovftool_uri)
if err != nil {
return fmt.Errorf("Couldn't generate uri for ovftool: %s", err)
}
u.User = url.UserPassword(c.RemoteUser, c.RemotePassword)
ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", u.String()}
var out bytes.Buffer
cmdCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exec.CommandContext(cmdCtx, ovftool, ovfToolArgs...)
cmd.Stdout = &out
// Need to manually close stdin or else the ofvtool call will hang
// forever in a situation where the user has provided an invalid
// password or username
stdin, _ := cmd.StdinPipe()
defer stdin.Close()
if err := cmd.Run(); err != nil {
outString := out.String()
// The command *should* fail with this error, if it
// authenticates properly.
if !strings.Contains(outString, "Found wrong kind of object") {
err := fmt.Errorf("ovftool validation error: %s; %s",
err, outString)
if strings.Contains(outString,
"Enter login information for source") {
err = fmt.Errorf("The username or password you " +
"provided to ovftool is invalid.")
}
return err
}
}
return nil
}
+71 -19
View File
@@ -1,32 +1,84 @@
package common
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/packer/template/interpolate"
)
func TestDriverConfigPrepare(t *testing.T) {
var c *DriverConfig
// Test a default boot_wait
c = new(DriverConfig)
errs := c.Prepare(interpolate.NewContext())
if len(errs) > 0 {
t.Fatalf("bad: %#v", errs)
}
if c.FusionAppPath != "/Applications/VMware Fusion.app" {
t.Fatalf("bad value: %s", c.FusionAppPath)
tc := []struct {
name string
config *DriverConfig
expectedConfig *DriverConfig
errs []error
}{
{
name: "Set default values",
config: new(DriverConfig),
expectedConfig: &DriverConfig{
FusionAppPath: "/Applications/VMware Fusion.app",
RemoteDatastore: "datastore1",
RemoteCacheDatastore: "datastore1",
RemoteCacheDirectory: "packer_cache",
RemotePort: 22,
RemoteUser: "root",
},
errs: nil,
},
{
name: "Override default values",
config: &DriverConfig{
FusionAppPath: "foo",
RemoteDatastore: "set-datastore1",
RemoteCacheDatastore: "set-datastore1",
RemoteCacheDirectory: "set_packer_cache",
RemotePort: 443,
RemoteUser: "admin",
},
expectedConfig: &DriverConfig{
FusionAppPath: "foo",
RemoteDatastore: "set-datastore1",
RemoteCacheDatastore: "set-datastore1",
RemoteCacheDirectory: "set_packer_cache",
RemotePort: 443,
RemoteUser: "admin",
},
errs: nil,
},
{
name: "Invalid remote type",
config: &DriverConfig{
RemoteType: "invalid",
RemoteHost: "host",
},
expectedConfig: nil,
errs: []error{fmt.Errorf("Only 'esx5' value is accepted for remote_type")},
},
{
name: "Remote host not set",
config: &DriverConfig{
RemoteType: "esx5",
},
expectedConfig: nil,
errs: []error{fmt.Errorf("remote_host must be specified")},
},
}
// Test with a good one
c = new(DriverConfig)
c.FusionAppPath = "foo"
errs = c.Prepare(interpolate.NewContext())
if len(errs) > 0 {
t.Fatalf("bad: %#v", errs)
}
if c.FusionAppPath != "foo" {
t.Fatalf("bad value: %s", c.FusionAppPath)
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
errs := c.config.Prepare(interpolate.NewContext())
if !reflect.DeepEqual(errs, c.errs) {
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
}
if len(c.errs) == 0 {
if diff := cmp.Diff(c.config, c.expectedConfig); diff != "" {
t.Fatalf("bad value: %s", diff)
}
}
})
}
}
+162
View File
@@ -11,7 +11,9 @@ import (
"io"
"log"
"net"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
@@ -24,7 +26,15 @@ import (
"github.com/hashicorp/packer/helper/multistep"
helperssh "github.com/hashicorp/packer/helper/ssh"
"github.com/hashicorp/packer/packer"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/mobile/event/key"
)
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
@@ -43,11 +53,66 @@ type ESX5Driver struct {
VMName string
CommConfig communicator.Config
ctx context.Context
client *govmomi.Client
finder *find.Finder
comm packer.Communicator
outputDir string
vmId string
}
func NewESX5Driver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver, error) {
ctx := context.TODO()
vsphereUrl, err := url.Parse(fmt.Sprintf("https://%v/sdk", dconfig.RemoteHost))
if err != nil {
return nil, err
}
credentials := url.UserPassword(dconfig.RemoteUser, dconfig.RemotePassword)
vsphereUrl.User = credentials
soapClient := soap.NewClient(vsphereUrl, true)
vimClient, err := vim25.NewClient(ctx, soapClient)
if err != nil {
return nil, err
}
vimClient.RoundTripper = session.KeepAlive(vimClient.RoundTripper, 10*time.Minute)
client := &govmomi.Client{
Client: vimClient,
SessionManager: session.NewManager(vimClient),
}
err = client.SessionManager.Login(ctx, credentials)
if err != nil {
return nil, err
}
finder := find.NewFinder(client.Client, false)
datacenter, err := finder.DefaultDatacenter(ctx)
if err != nil {
return nil, err
}
finder.SetDatacenter(datacenter)
return &ESX5Driver{
Host: dconfig.RemoteHost,
Port: dconfig.RemotePort,
Username: dconfig.RemoteUser,
Password: dconfig.RemotePassword,
PrivateKeyFile: dconfig.RemotePrivateKey,
Datastore: dconfig.RemoteDatastore,
CacheDatastore: dconfig.RemoteCacheDatastore,
CacheDirectory: dconfig.RemoteCacheDirectory,
VMName: vmName,
CommConfig: config.Comm,
ctx: ctx,
client: client,
finder: finder,
}, nil
}
func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") }
@@ -263,6 +328,68 @@ func (d *ESX5Driver) Verify() error {
return nil
}
func (d *ESX5Driver) VerifyOvfTool(SkipExport, skipValidateCredentials bool) error {
err := d.base.VerifyOvfTool(SkipExport, skipValidateCredentials)
if err != nil {
return err
}
log.Printf("Verifying that ovftool credentials are valid...")
// check that password is valid by sending a dummy ovftool command
// now, so that we don't fail for a simple mistake after a long
// build
ovftool := GetOVFTool()
if skipValidateCredentials {
return nil
}
if d.Password == "" {
return fmt.Errorf("exporting the vm from esxi with ovftool requires " +
"that you set a value for remote_password")
}
// Generate the uri of the host, with embedded credentials
ovftool_uri := fmt.Sprintf("vi://%s", d.Host)
u, err := url.Parse(ovftool_uri)
if err != nil {
return fmt.Errorf("Couldn't generate uri for ovftool: %s", err)
}
u.User = url.UserPassword(d.Username, d.Password)
ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", u.String()}
var out bytes.Buffer
cmdCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exec.CommandContext(cmdCtx, ovftool, ovfToolArgs...)
cmd.Stdout = &out
// Need to manually close stdin or else the ofvtool call will hang
// forever in a situation where the user has provided an invalid
// password or username
stdin, _ := cmd.StdinPipe()
defer stdin.Close()
if err := cmd.Run(); err != nil {
outString := out.String()
// The command *should* fail with this error, if it
// authenticates properly.
if !strings.Contains(outString, "Found wrong kind of object") {
err := fmt.Errorf("ovftool validation error: %s; %s",
err, outString)
if strings.Contains(outString,
"Enter login information for source") {
err = fmt.Errorf("The username or password you " +
"provided to ovftool is invalid.")
}
return err
}
}
return nil
}
func (d *ESX5Driver) HostIP(multistep.StateBag) (string, error) {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
if err != nil {
@@ -823,3 +950,38 @@ func (r *esxcliReader) find(key, val string) (map[string]string, error) {
}
}
}
type KeyInput struct {
Scancode key.Code
Alt bool
Ctrl bool
Shift bool
}
func (d *ESX5Driver) TypeOnKeyboard(input KeyInput) (int32, error) {
vm, err := d.finder.VirtualMachine(d.ctx, d.VMName)
if err != nil {
return 0, err
}
var spec types.UsbScanCodeSpec
spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{
UsbHidCode: int32(input.Scancode)<<16 | 7,
Modifiers: &types.UsbScanCodeSpecModifierType{
LeftControl: &input.Ctrl,
LeftAlt: &input.Alt,
LeftShift: &input.Shift,
},
})
req := &types.PutUsbScanCodes{
This: vm.Reference(),
Spec: spec,
}
resp, err := methods.PutUsbScanCodes(d.ctx, d.client.RoundTripper, req)
if err != nil {
return 0, err
}
return resp.Returnval, nil
}
+6
View File
@@ -95,6 +95,8 @@ type DriverMock struct {
VerifyCalled bool
VerifyErr error
VerifyOvftoolCalled bool
}
type NetworkMapperMock struct {
@@ -279,3 +281,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver {
}
return state
}
func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error {
return nil
}
+10 -10
View File
@@ -954,20 +954,20 @@ func createDeclaration(node pDeclaration) configDeclaration {
// update configDeclaration parameters
for _, p := range hierarchy[i].parameters {
switch p.(type) {
switch p := p.(type) {
case pParameterOption:
result.options[p.(pParameterOption).name] = p.(pParameterOption).value
result.options[p.name] = p.value
case pParameterGrant:
Grant := map[string]grant{"ignore": IGNORE, "allow": ALLOW, "deny": DENY}
result.grants[p.(pParameterGrant).attribute] = Grant[p.(pParameterGrant).verb]
result.grants[p.attribute] = Grant[p.verb]
case pParameterBoolean:
result.attributes[p.(pParameterBoolean).parameter] = p.(pParameterBoolean).truancy
result.attributes[p.parameter] = p.truancy
case pParameterClientMatch:
result.hostid = append(result.hostid, p.(pParameterClientMatch))
result.hostid = append(result.hostid, p)
case pParameterExpression:
result.expressions[p.(pParameterExpression).parameter] = p.(pParameterExpression).expression
result.expressions[p.parameter] = p.expression
case pParameterOther:
result.parameters[p.(pParameterOther).parameter] = p.(pParameterOther).value
result.parameters[p.parameter] = p.value
default:
result.address = append(result.address, p)
}
@@ -1196,7 +1196,7 @@ func (e *DhcpConfiguration) HostByName(host string) (configDeclaration, error) {
switch entry.id[0].(type) {
case pDeclarationHost:
id := entry.id[0].(pDeclarationHost)
if strings.ToLower(id.name) == strings.ToLower(host) {
if strings.EqualFold(id.name, host) {
result = append(result, entry)
}
}
@@ -1237,7 +1237,7 @@ func (e NetworkMap) NameIntoDevices(name string) ([]string, error) {
var devices []string
for _, val := range e {
if strings.ToLower(val["name"]) == strings.ToLower(name) {
if strings.EqualFold(val["name"], name) {
devices = append(devices, val["device"])
}
}
@@ -1250,7 +1250,7 @@ func (e NetworkMap) NameIntoDevices(name string) ([]string, error) {
}
func (e NetworkMap) DeviceIntoName(device string) (string, error) {
for _, val := range e {
if strings.ToLower(val["device"]) == strings.ToLower(device) {
if strings.EqualFold(val["device"], device) {
return val["name"], nil
}
}
+18 -16
View File
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/packer/template/interpolate"
)
// ~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored.
type RunConfig struct {
// Packer defaults to building VMware virtual machines
// by launching a GUI that shows the console of the machine being built. When
@@ -39,25 +40,26 @@ type RunConfig struct {
VNCDisablePassword bool `mapstructure:"vnc_disable_password" required:"false"`
}
func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) {
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
func (c *RunConfig) Prepare(_ *interpolate.Context, bootConfig *BootConfigWrapper) (errs []error) {
if !bootConfig.USBKeyBoard {
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
}
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCPortMin > c.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
if c.VNCPortMin > c.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if c.VNCPortMin < 0 {
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
}
}
if c.VNCPortMin < 0 {
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
}
return
}
@@ -0,0 +1,126 @@
package common
import (
"context"
"fmt"
"time"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"golang.org/x/mobile/event/key"
)
// This step "types" the boot command into the VM using USB Scan Codes.
type StepUSBBootCommand struct {
Config bootcommand.BootConfig
KeyInterval time.Duration
VMName string
Ctx interpolate.Context
}
type USBBootCommandTemplateData struct {
HTTPIP string
HTTPPort int
Name string
}
func (s *StepUSBBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
debug := state.Get("debug").(bool)
ui := state.Get("ui").(packer.Ui)
driver := state.Get("driver").(*ESX5Driver)
if s.Config.BootCommand == nil {
return multistep.ActionContinue
}
// Wait the for the vm to boot.
if int64(s.Config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String()))
select {
case <-time.After(s.Config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}
var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}
port := state.Get("http_port").(int)
if port > 0 {
ip := state.Get("http_ip").(string)
s.Ctx.Data = &USBBootCommandTemplateData{
HTTPIP: ip,
HTTPPort: port,
Name: s.VMName,
}
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 {
switch code {
case key.CodeLeftAlt:
keyAlt = down
case key.CodeLeftControl:
keyCtrl = down
case key.CodeLeftShift:
keyShift = down
}
shift := down
if keyShift {
shift = keyShift
}
_, err := driver.TypeOnKeyboard(KeyInput{
Scancode: code,
Ctrl: keyCtrl,
Alt: keyAlt,
Shift: shift,
})
if err != nil {
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
}
return nil
}
d := bootcommand.NewUSBDriver(sendCodes, s.KeyInterval)
ui.Say("Typing boot command...")
flatBootCommand := s.Config.FlatBootCommand()
command, err := interpolate.Render(flatBootCommand, &s.Ctx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
seq, err := bootcommand.GenerateExpressionSequence(command)
if err != nil {
err := fmt.Errorf("Error generating boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := seq.Do(ctx, d); err != nil {
err := fmt.Errorf("Error running boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if pauseFn != nil {
pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state)
}
return multistep.ActionContinue
}
func (*StepUSBBootCommand) Cleanup(multistep.StateBag) {}
@@ -23,22 +23,20 @@ import (
//
// Produces:
// <nothing>
type StepTypeBootCommand struct {
BootCommand string
VNCEnabled bool
BootWait time.Duration
VMName string
Ctx interpolate.Context
KeyInterval time.Duration
type StepVNCBootCommand struct {
Config bootcommand.VNCConfig
VMName string
Ctx interpolate.Context
}
type bootCommandTemplateData struct {
type VNCBootCommandTemplateData struct {
HTTPIP string
HTTPPort int
Name string
}
func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
if !s.VNCEnabled {
func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
if s.Config.DisableVNC {
log.Println("Skipping boot command step...")
return multistep.ActionContinue
}
@@ -51,10 +49,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
vncPassword := state.Get("vnc_password")
// Wait the for the vm to boot.
if int64(s.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.BootWait.String()))
if int64(s.Config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String()))
select {
case <-time.After(s.BootWait):
case <-time.After(s.Config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
@@ -98,16 +96,17 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
hostIP := state.Get("http_ip").(string)
s.Ctx.Data = &bootCommandTemplateData{
hostIP,
httpPort,
s.VMName,
s.Ctx.Data = &VNCBootCommandTemplateData{
HTTPIP: hostIP,
HTTPPort: httpPort,
Name: s.VMName,
}
d := bootcommand.NewVNCDriver(c, s.KeyInterval)
d := bootcommand.NewVNCDriver(c, s.Config.BootKeyInterval)
ui.Say("Typing the boot command over VNC...")
command, err := interpolate.Render(s.BootCommand, &s.Ctx)
flatBootCommand := s.Config.FlatBootCommand()
command, err := interpolate.Render(flatBootCommand, &s.Ctx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
@@ -138,4 +137,4 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionContinue
}
func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {}
func (*StepVNCBootCommand) Cleanup(multistep.StateBag) {}
+30 -8
View File
@@ -35,6 +35,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
if err != nil {
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
}
// Before we get deep into the build, make sure ovftool is present and
// credentials are valid, if we're going to use ovftool.
if err := driver.VerifyOvfTool(b.config.SkipExport, b.config.SkipValidateCredentials); err != nil {
return nil, err
}
// Setup the state bag
state := new(multistep.BasicStateBag)
@@ -47,6 +52,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
state.Put("driverConfig", &b.config.DriverConfig)
state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building
var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{
Config: b.config.VNCConfig,
VMName: b.config.VMName,
Ctx: b.config.ctx,
}
if b.config.USBKeyBoard {
stepBootCommand = &vmwcommon.StepUSBBootCommand{
Config: b.config.VNCConfig.BootConfig,
KeyInterval: b.config.VNCConfig.BootKeyInterval,
VMName: b.config.VMName,
Ctx: b.config.ctx,
}
}
steps := []multistep.Step{
&vmwcommon.StepPrepareTools{
RemoteType: b.config.RemoteType,
@@ -71,12 +90,22 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Directories: b.config.FloppyConfig.FloppyDirectories,
Label: b.config.FloppyConfig.FloppyLabel,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&vmwcommon.StepRemoteUpload{
Key: "floppy_path",
Message: "Uploading Floppy to remote machine...",
DoCleanup: true,
Checksum: "none",
},
&vmwcommon.StepRemoteUpload{
Key: "cd_path",
Message: "Uploading CD to remote machine...",
DoCleanup: true,
Checksum: "none",
},
&vmwcommon.StepRemoteUpload{
Key: "iso_path",
Message: "Uploading ISO to remote machine...",
@@ -122,14 +151,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DurationBeforeStop: 5 * time.Second,
Headless: b.config.Headless,
},
&vmwcommon.StepTypeBootCommand{
BootWait: b.config.BootWait,
VNCEnabled: !b.config.DisableVNC,
BootCommand: b.config.FlatBootCommand(),
VMName: b.config.VMName,
Ctx: b.config.ctx,
KeyInterval: b.config.VNCConfig.BootKeyInterval,
},
stepBootCommand,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
+117
View File
@@ -198,6 +198,123 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
}
}
func TestBuilderPrepare_Export(t *testing.T) {
type testCase struct {
InputConfigVals map[string]string
ExpectedSkipExportValue bool
ExpectedFormat string
ExpectedErr bool
Reason string
}
testCases := []testCase{
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "",
},
ExpectedSkipExportValue: true,
ExpectedFormat: "vmx",
ExpectedErr: false,
Reason: "should have defaulted format to vmx.",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: false,
Reason: "should have defaulted format to ovf with remote set to esx5.",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: true,
Reason: "should have errored because remote host isn't set for remote build.",
},
{
InputConfigVals: map[string]string{
"remote_type": "invalid",
"format": "",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: true,
Reason: "should error with invalid remote type",
},
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "invalid",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "invalid",
ExpectedErr: true,
Reason: "should error with invalid format",
},
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "ova",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ova",
ExpectedErr: false,
Reason: "should set user-given ova format",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "ova",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ova",
ExpectedErr: false,
Reason: "should set user-given ova format",
},
}
for _, tc := range testCases {
config := testConfig()
for k, v := range tc.InputConfigVals {
config[k] = v
}
config["skip_validate_credentials"] = true
outCfg := &Config{}
warns, errs := (outCfg).Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if (errs != nil) != tc.ExpectedErr {
t.Fatalf("received error: \n %s \n but 'expected err' was %t", errs, tc.ExpectedErr)
}
if outCfg.Format != tc.ExpectedFormat {
t.Fatalf("Expected: %s. Actual: %s. Reason: %s", tc.ExpectedFormat,
outCfg.Format, tc.Reason)
}
if outCfg.SkipExport != tc.ExpectedSkipExportValue {
t.Fatalf("For SkipExport expected %t but recieved %t",
tc.ExpectedSkipExportValue, outCfg.SkipExport)
}
}
}
func TestBuilderPrepare_RemoteExport(t *testing.T) {
var b Builder
config := testConfig()
+23 -33
View File
@@ -11,7 +11,6 @@ import (
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/common/shutdowncommand"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
@@ -23,7 +22,8 @@ type Config struct {
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
bootcommand.VNCConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
vmwcommon.BootConfigWrapper `mapstructure:",squash"`
vmwcommon.DriverConfig `mapstructure:",squash"`
vmwcommon.HWConfig `mapstructure:",squash"`
vmwcommon.OutputConfig `mapstructure:",squash"`
@@ -93,24 +93,27 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
// Accumulate any errors and warnings
var warnings []string
var errs *packer.MultiError
warnings := make([]string, 0)
vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig)
warnings = append(warnings, vncWarnings...)
errs = packer.MultiErrorAppend(errs, vncErrs...)
isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx)
warnings = append(warnings, isoWarnings...)
errs = packer.MultiErrorAppend(errs, isoErrs...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HWConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs,
c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...)
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.DiskConfig.Prepare(&c.ctx)...)
@@ -163,32 +166,26 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.HWConfig.Network = "nat"
}
// Remote configuration validation
if c.RemoteType != "" {
if c.RemoteHost == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("remote_host must be specified"))
}
if c.RemoteType != "esx5" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
}
}
if c.Format == "" {
if c.RemoteType != "esx5" {
if c.RemoteType == "" {
c.Format = "vmx"
} else {
c.Format = "ovf"
}
}
if c.RemoteType != "esx5" && c.Format == "vmx" {
// if we're building locally and want a vmx, there's nothing to export.
// Set skip export flag here to keep the export step from attempting
// an unneded export
c.SkipExport = true
if c.RemoteType == "" {
if c.Format == "vmx" {
// if we're building locally and want a vmx, there's nothing to export.
// Set skip export flag here to keep the export step from attempting
// an unneded export
c.SkipExport = true
}
if c.Headless && c.DisableVNC {
warnings = append(warnings,
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
"you won't be able to see any output.")
}
}
err = c.DriverConfig.Validate(c.SkipExport)
@@ -196,19 +193,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, err)
}
// Warnings
if c.ShutdownCommand == "" {
warnings = append(warnings,
"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
"will forcibly halt the virtual machine, which may result in data loss.")
}
if c.Headless && c.DisableVNC {
warnings = append(warnings,
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
"you won't be able to see any output.")
}
if errs != nil && len(errs.Errors) > 0 {
return warnings, errs
}
+8
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
@@ -28,11 +29,14 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"`
CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"`
FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"`
RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"`
@@ -159,6 +163,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
@@ -167,11 +172,14 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false},
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
"usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false},
"cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false},
"fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false},
"remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false},
+17
View File
@@ -34,6 +34,7 @@ type vmxTemplateData struct {
DiskType string
CDROMType string
CDROMType_PrimarySecondary string
CDROM_PATH string
Network_Type string
Network_Device string
@@ -112,6 +113,7 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
ictx := config.ctx
// Mount extra vmdks we created earlier.
if len(config.AdditionalDiskSize) > 0 {
for i := range config.AdditionalDiskSize {
ictx.Data = &additionalDiskTemplateData{
@@ -238,6 +240,15 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
return multistep.ActionHalt
}
// Add our custom CD, if it exists
cd_path, ok := state.Get("cd_path").(string)
if ok {
if cd_path != "" {
vmxTemplate += ExtraCDRomTemplate
templateData.CDROM_PATH = cd_path
}
}
/// Now that we figured out the CDROM device to add, store it
/// to the list of temporary build devices in our statebag
tmpBuildDevices := state.Get("temporaryDevices").([]string)
@@ -596,3 +607,9 @@ scsi0:{{ .DiskNumber }}.fileName = "{{ .DiskName}}-{{ .DiskNumber }}.vmdk"
scsi0:{{ .DiskNumber }}.present = "TRUE"
scsi0:{{ .DiskNumber }}.redo = ""
`
const ExtraCDRomTemplate = `
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.present = "TRUE"
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.fileName = "{{ .CDROM_PATH }}"
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.deviceType = "cdrom-image"
`
+20 -10
View File
@@ -40,6 +40,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
if err != nil {
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
}
// Before we get deep into the build, make sure ovftool is present and
// credentials are valid, if we're going to use ovftool.
if err := driver.VerifyOvfTool(b.config.SkipExport, b.config.SkipValidateCredentials); err != nil {
return nil, err
}
// Set up the state.
state := new(multistep.BasicStateBag)
@@ -51,6 +56,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
state.Put("driverConfig", &b.config.DriverConfig)
state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building
var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{
Config: b.config.VNCConfig,
VMName: b.config.VMName,
Ctx: b.config.ctx,
}
if b.config.USBKeyBoard {
stepBootCommand = &vmwcommon.StepUSBBootCommand{
Config: b.config.VNCConfig.BootConfig,
KeyInterval: b.config.VNCConfig.BootKeyInterval,
VMName: b.config.VMName,
Ctx: b.config.ctx,
}
}
// Build the steps.
steps := []multistep.Step{
&vmwcommon.StepPrepareTools{
@@ -121,14 +140,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DurationBeforeStop: 5 * time.Second,
Headless: b.config.Headless,
},
&vmwcommon.StepTypeBootCommand{
BootWait: b.config.BootWait,
VNCEnabled: !b.config.DisableVNC,
BootCommand: b.config.FlatBootCommand(),
VMName: b.config.VMName,
Ctx: b.config.ctx,
KeyInterval: b.config.VNCConfig.BootKeyInterval,
},
stepBootCommand,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
@@ -198,5 +210,3 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
}
// Cancel.
+14 -33
View File
@@ -9,7 +9,6 @@ import (
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/common/shutdowncommand"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
@@ -21,7 +20,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
bootcommand.VNCConfig `mapstructure:",squash"`
vmwcommon.BootConfigWrapper `mapstructure:",squash"`
vmwcommon.DriverConfig `mapstructure:",squash"`
vmwcommon.OutputConfig `mapstructure:",squash"`
vmwcommon.RunConfig `mapstructure:",squash"`
@@ -77,16 +76,20 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
"packer-%s-%d", c.PackerBuildName, interpolate.InitTime.Unix())
}
// Prepare the errors
// Accumulate any errors and warnings
var warnings []string
var errs *packer.MultiError
vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig)
warnings = append(warnings, vncWarnings...)
errs = packer.MultiErrorAppend(errs, vncErrs...)
errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...)
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
@@ -101,16 +104,10 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
fmt.Errorf("source_path is invalid: %s", err))
}
}
} else {
// Remote configuration validation
if c.RemoteHost == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("remote_host must be specified"))
}
if c.RemoteType != "esx5" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
if c.Headless && c.DisableVNC {
warnings = append(warnings,
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
"you won't be able to see any output.")
}
}
@@ -124,14 +121,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
if c.Format == "" {
if c.RemoteType != "esx5" {
if c.RemoteType == "" {
c.Format = "vmx"
} else {
c.Format = "ovf"
}
}
if c.RemoteType != "esx5" && c.Format == "vmx" {
if c.RemoteType == "" && c.Format == "vmx" {
// if we're building locally and want a vmx, there's nothing to export.
// Set skip export flag here to keep the export step from attempting
// an unneded export
@@ -143,28 +140,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, err)
}
if c.Format == "" {
if c.RemoteType != "esx5" {
c.Format = "vmx"
} else {
c.Format = "ovf"
}
}
// Warnings
var warnings []string
if c.ShutdownCommand == "" {
warnings = append(warnings,
"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
"will forcibly halt the virtual machine, which may result in data loss.")
}
if c.Headless && c.DisableVNC {
warnings = append(warnings,
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
"you won't be able to see any output.")
}
// Check for any errors.
if errs != nil && len(errs.Errors) > 0 {
return warnings, errs
+4
View File
@@ -20,6 +20,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
@@ -28,6 +29,7 @@ type FlatConfig struct {
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"`
CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"`
FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"`
RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"`
@@ -140,6 +142,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
@@ -148,6 +151,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false},
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
"usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false},
"cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false},
"fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false},
"remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false},
+117
View File
@@ -58,3 +58,120 @@ func TestNewConfig_sourcePath(t *testing.T) {
warns, errs = (&Config{}).Prepare(cfg)
testConfigOk(t, warns, errs)
}
func TestNewConfig_exportConfig(t *testing.T) {
type testCase struct {
InputConfigVals map[string]string
ExpectedSkipExportValue bool
ExpectedFormat string
ExpectedErr bool
Reason string
}
testCases := []testCase{
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "",
},
ExpectedSkipExportValue: true,
ExpectedFormat: "vmx",
ExpectedErr: false,
Reason: "should have defaulted format to vmx.",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: false,
Reason: "should have defaulted format to ovf with remote set to esx5.",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: true,
Reason: "should have errored because remote host isn't set for remote build.",
},
{
InputConfigVals: map[string]string{
"remote_type": "invalid",
"format": "",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ovf",
ExpectedErr: true,
Reason: "should error with invalid remote type",
},
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "invalid",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "invalid",
ExpectedErr: true,
Reason: "should error with invalid format",
},
{
InputConfigVals: map[string]string{
"remote_type": "",
"format": "ova",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ova",
ExpectedErr: false,
Reason: "should set user-given ova format",
},
{
InputConfigVals: map[string]string{
"remote_type": "esx5",
"format": "ova",
"remote_host": "fakehost.com",
"remote_password": "fakepassword",
"remote_username": "fakeuser",
},
ExpectedSkipExportValue: false,
ExpectedFormat: "ova",
ExpectedErr: false,
Reason: "should set user-given ova format",
},
}
for _, tc := range testCases {
cfg := testConfig(t)
for k, v := range tc.InputConfigVals {
cfg[k] = v
}
cfg["skip_validate_credentials"] = true
outCfg := &Config{}
warns, errs := (outCfg).Prepare(cfg)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if (errs != nil) != tc.ExpectedErr {
t.Fatalf("received error: \n %s \n but 'expected err' was %t", errs, tc.ExpectedErr)
}
if outCfg.Format != tc.ExpectedFormat {
t.Fatalf("Expected: %s. Actual: %s. Reason: %s", tc.ExpectedFormat,
outCfg.Format, tc.Reason)
}
if outCfg.SkipExport != tc.ExpectedSkipExportValue {
t.Fatalf("For SkipExport expected %t but recieved %t",
tc.ExpectedSkipExportValue, outCfg.SkipExport)
}
}
}
+1 -1
View File
@@ -139,7 +139,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
artifact := &common.Artifact{
Name: b.config.VMName,
VM: state.Get("vm").(*driver.VirtualMachine),
VM: state.Get("vm").(*driver.VirtualMachineDriver),
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}
if b.config.Export != nil {
+4
View File
@@ -21,6 +21,7 @@ type FlatConfig struct {
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
VCenterServer *string `mapstructure:"vcenter_server" cty:"vcenter_server" hcl:"vcenter_server"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
@@ -30,6 +31,7 @@ type FlatConfig struct {
DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"`
Network *string `mapstructure:"network" cty:"network" hcl:"network"`
MacAddress *string `mapstructure:"mac_address" cty:"mac_address" hcl:"mac_address"`
Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"`
VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
@@ -144,6 +146,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"vcenter_server": &hcldec.AttrSpec{Name: "vcenter_server", Type: cty.String, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
@@ -153,6 +156,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false},
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
"mac_address": &hcldec.AttrSpec{Name: "mac_address", Type: cty.String, Required: false},
"notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false},
"vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
+9 -2
View File
@@ -37,6 +37,8 @@ type CloneConfig struct {
// 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"`
// Sets a custom Mac Address to the network adapter. If set, the [network](#network) must be also specified.
MacAddress string `mapstructure:"mac_address"`
// VM notes.
Notes string `mapstructure:"notes"`
// Set the vApp Options to a virtual machine.
@@ -56,6 +58,10 @@ func (c *CloneConfig) Prepare() []error {
errs = append(errs, fmt.Errorf("'linked_clone' and 'disk_size' cannot be used together"))
}
if c.MacAddress != "" && c.Network == "" {
errs = append(errs, fmt.Errorf("'network' is required when 'mac_address' is specified"))
}
return errs
}
@@ -67,7 +73,7 @@ type StepCloneVM struct {
func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
d := state.Get("driver").(*driver.Driver)
d := state.Get("driver").(*driver.VCenterDriver)
vmPath := path.Join(s.Location.Folder, s.Location.VMName)
err := d.PreCleanVM(ui, vmPath, s.Force)
@@ -92,6 +98,7 @@ func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multist
Datastore: s.Location.Datastore,
LinkedClone: s.Config.LinkedClone,
Network: s.Config.Network,
MacAddress: s.Config.MacAddress,
Annotation: s.Config.Notes,
VAppProperties: s.Config.VAppConfig.Properties,
})
@@ -128,7 +135,7 @@ func (s *StepCloneVM) Cleanup(state multistep.StateBag) {
if st == nil {
return
}
vm := st.(*driver.VirtualMachine)
vm := st.(*driver.VirtualMachineDriver)
ui.Say("Destroying VM...")
@@ -13,6 +13,7 @@ type FlatCloneConfig struct {
DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"`
Network *string `mapstructure:"network" cty:"network" hcl:"network"`
MacAddress *string `mapstructure:"mac_address" cty:"mac_address" hcl:"mac_address"`
Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"`
VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"`
}
@@ -33,6 +34,7 @@ func (*FlatCloneConfig) HCL2Spec() map[string]hcldec.Spec {
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false},
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
"mac_address": &hcldec.AttrSpec{Name: "mac_address", Type: cty.String, Required: false},
"notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false},
"vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())},
}
+1 -1
View File
@@ -123,7 +123,7 @@ func (c *CustomizeConfig) Prepare() []error {
}
func (s *StepCustomize) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
ui := state.Get("ui").(packer.Ui)
identity, err := s.identitySettings()
+1 -1
View File
@@ -11,7 +11,7 @@ const BuilderId = "jetbrains.vsphere"
type Artifact struct {
Outconfig *OutputConfig
Name string
VM *driver.VirtualMachine
VM *driver.VirtualMachineDriver
// StateData should store data such as GeneratedData
// to be shared with post-processors
+19
View File
@@ -0,0 +1,19 @@
package common
import (
"bytes"
"strings"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func basicStateBag(errorBuffer *strings.Builder) *multistep.BasicStateBag {
state := new(multistep.BasicStateBag)
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: errorBuffer,
})
return state
}
+2 -2
View File
@@ -21,8 +21,8 @@ type LocationConfig struct {
// the host is in a folder. For example `folder/host`. See the
// `Specifying Clusters and Hosts` section above for more details.
Host string `mapstructure:"host"`
// VMWare resource pool. Defaults to the root resource pool of the
// `host` or `cluster`.
// VMWare resource pool. If not set, it will look for the root resource pool of the `host` or `cluster`.
// If a root resource is not found, it will then look for a default resource pool.
ResourcePool string `mapstructure:"resource_pool"`
// VMWare datastore. Required if `host` is a cluster, or if `host` has
// multiple datastores.
+1 -1
View File
@@ -43,7 +43,7 @@ type StepBootCommand struct {
func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
debug := state.Get("debug").(bool)
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if s.Config.BootCommand == nil {
return multistep.ActionContinue
+1 -1
View File
@@ -33,7 +33,7 @@ type StepConfigParams struct {
func (s *StepConfigParams) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
configParams := make(map[string]string)
if s.Config.ConfigParams != nil {
+1 -1
View File
@@ -154,7 +154,7 @@ func (s *StepExport) Cleanup(multistep.StateBag) {
func (s *StepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
ui.Message("Starting export...")
lease, err := vm.Export()
+2 -2
View File
@@ -13,7 +13,7 @@ import (
)
type HardwareConfig struct {
// Number of CPU sockets.
// Number of CPU cores.
CPUs int32 `mapstructure:"CPUs"`
// Number of CPU cores per socket.
CpuCores int32 `mapstructure:"cpu_cores"`
@@ -65,7 +65,7 @@ type StepConfigureHardware struct {
func (s *StepConfigureHardware) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(driver.VirtualMachine)
if *s.Config != (HardwareConfig{}) {
ui.Say("Customizing hardware...")
@@ -0,0 +1,181 @@
package common
import (
"context"
"errors"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/packer/builder/vsphere/driver"
"github.com/hashicorp/packer/helper/multistep"
)
func TestHardwareConfig_Prepare(t *testing.T) {
tc := []struct {
name string
config *HardwareConfig
fail bool
expectedErrMsg string
}{
{
name: "Validate empty config",
config: &HardwareConfig{},
fail: false,
},
{
name: "Validate RAMReservation RAMReserveAll cannot be used together",
config: &HardwareConfig{
RAMReservation: 2,
RAMReserveAll: true,
},
fail: true,
expectedErrMsg: "'RAM_reservation' and 'RAM_reserve_all' cannot be used together",
},
{
name: "Invalid firmware",
config: &HardwareConfig{
Firmware: "invalid",
},
fail: true,
expectedErrMsg: "'firmware' must be '', 'bios', 'efi' or 'efi-secure'",
},
{
name: "Validate 'bios' firmware",
config: &HardwareConfig{
Firmware: "bios",
},
fail: false,
},
{
name: "Validate 'efi' firmware",
config: &HardwareConfig{
Firmware: "efi",
},
fail: false,
},
{
name: "Validate 'efi-secure' firmware",
config: &HardwareConfig{
Firmware: "efi-secure",
},
fail: false,
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
errs := c.config.Prepare()
if c.fail {
if len(errs) == 0 {
t.Fatalf("Config preprare should fail")
}
if errs[0].Error() != c.expectedErrMsg {
t.Fatalf("Expected error message: %s but was '%s'", c.expectedErrMsg, errs[0].Error())
}
} else {
if len(errs) != 0 {
t.Fatalf("Config preprare should not fail")
}
}
})
}
}
func TestStepConfigureHardware_Run(t *testing.T) {
tc := []struct {
name string
step *StepConfigureHardware
action multistep.StepAction
configureError error
configureCalled bool
hardwareConfig *driver.HardwareConfig
}{
{
name: "Configure hardware",
step: basicStepConfigureHardware(),
action: multistep.ActionContinue,
configureError: nil,
configureCalled: true,
hardwareConfig: driverHardwareConfigFromConfig(basicStepConfigureHardware().Config),
},
{
name: "Don't configure hardware when config is empty",
step: &StepConfigureHardware{Config: &HardwareConfig{}},
action: multistep.ActionContinue,
configureError: nil,
configureCalled: false,
},
{
name: "Halt when configure return error",
step: basicStepConfigureHardware(),
action: multistep.ActionHalt,
configureError: errors.New("failed to configure"),
configureCalled: true,
hardwareConfig: driverHardwareConfigFromConfig(basicStepConfigureHardware().Config),
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
state := basicStateBag(nil)
vmMock := new(driver.VirtualMachineMock)
vmMock.ConfigureError = c.configureError
state.Put("vm", vmMock)
action := c.step.Run(context.TODO(), state)
if action != c.action {
t.Fatalf("expected action '%v' but actual action was '%v'", c.action, action)
}
if vmMock.ConfigureCalled != c.configureCalled {
t.Fatalf("expecting vm.Configure called to %t but was %t", c.configureCalled, vmMock.ConfigureCalled)
}
if diff := cmp.Diff(vmMock.ConfigureHardwareConfig, c.hardwareConfig); diff != "" {
t.Fatalf("wrong driver.HardwareConfig: %s", diff)
}
err, ok := state.GetOk("error")
containsError := c.configureError != nil
if containsError != ok {
t.Fatalf("Contain error - expecting %t but was %t", containsError, ok)
}
if containsError {
if !strings.Contains(err.(error).Error(), c.configureError.Error()) {
t.Fatalf("Destroy should fail with error message '%s' but failed with '%s'", c.configureError.Error(), err.(error).Error())
}
}
})
}
}
func basicStepConfigureHardware() *StepConfigureHardware {
return &StepConfigureHardware{
Config: &HardwareConfig{
CPUs: 1,
CpuCores: 1,
CPUReservation: 1,
CPULimit: 4000,
RAM: 1024,
RAMReserveAll: true,
Firmware: "efi-secure",
ForceBIOSSetup: true,
},
}
}
func driverHardwareConfigFromConfig(config *HardwareConfig) *driver.HardwareConfig {
return &driver.HardwareConfig{
CPUs: config.CPUs,
CpuCores: config.CpuCores,
CPUReservation: config.CPUReservation,
CPULimit: config.CPULimit,
RAM: config.RAM,
RAMReservation: config.RAMReservation,
RAMReserveAll: config.RAMReserveAll,
NestedHV: config.NestedHV,
CpuHotAddEnabled: config.CpuHotAddEnabled,
MemoryHotAddEnabled: config.MemoryHotAddEnabled,
VideoRAM: config.VideoRAM,
VGPUProfile: config.VGPUProfile,
Firmware: config.Firmware,
ForceBIOSSetup: config.ForceBIOSSetup,
}
}
@@ -118,7 +118,7 @@ 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)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
var err error
if s.ContentLibConfig.Ovf {
@@ -142,7 +142,7 @@ func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.Stat
return multistep.ActionContinue
}
func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachine) error {
func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachineDriver) error {
ovf := vcenter.OVF{
Spec: vcenter.CreateSpec{
Name: s.ContentLibConfig.Name,
@@ -154,7 +154,7 @@ func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachine
return vm.ImportOvfToContentLibrary(ovf)
}
func (s *StepImportToContentLibrary) importVmTemplate(vm *driver.VirtualMachine) error {
func (s *StepImportToContentLibrary) importVmTemplate(vm *driver.VirtualMachineDriver) error {
template := vcenter.Template{
Name: s.ContentLibConfig.Name,
Description: s.ContentLibConfig.Description,
+2 -2
View File
@@ -24,7 +24,7 @@ type StepRun struct {
func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if s.Config.BootOrder != "" {
ui.Say("Set boot order...")
@@ -55,7 +55,7 @@ func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.Ste
func (s *StepRun) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if s.Config.BootOrder == "" && s.SetOrder {
ui.Say("Clear boot order...")
+1 -1
View File
@@ -48,7 +48,7 @@ type StepShutdown struct {
func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
comm := state.Get("communicator").(packer.Communicator)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if off, _ := vm.IsPoweredOff(); off {
// Probably power off initiated by last provisioner, though disable_shutdown is not set
+1 -1
View File
@@ -14,7 +14,7 @@ type StepCreateSnapshot struct {
func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if s.CreateSnapshot {
ui.Say("Creating snapshot...")
+1 -1
View File
@@ -81,7 +81,7 @@ func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) mult
s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
s.Comm.SSHClearAuthorizedKeys = true
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
err = vm.AddPublicKeys(ctx, string(s.Comm.SSHPublicKey))
if err != nil {
state.Put("error", fmt.Errorf("error saving temporary keypair in the vm: %s", err))
+1 -1
View File
@@ -14,7 +14,7 @@ type StepConvertToTemplate struct {
func (s *StepConvertToTemplate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if s.ConvertToTemplate {
ui.Say("Convert VM into template...")
+2 -2
View File
@@ -75,7 +75,7 @@ func (c *WaitIpConfig) GetIPNet() *net.IPNet {
func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*driver.VirtualMachine)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
var ip string
var err error
@@ -128,7 +128,7 @@ func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multi
}
}
func doGetIp(vm *driver.VirtualMachine, ctx context.Context, c *WaitIpConfig) (string, error) {
func doGetIp(vm *driver.VirtualMachineDriver, ctx context.Context, c *WaitIpConfig) (string, error) {
var prevIp = ""
var stopTime time.Time
var interval time.Duration
+2 -2
View File
@@ -34,7 +34,7 @@ func RenderConfig(config map[string]interface{}) string {
return string(j)
}
func TestConn(t *testing.T) *driver.Driver {
func TestConn(t *testing.T) driver.Driver {
username := os.Getenv("VSPHERE_USERNAME")
if username == "" {
username = "root"
@@ -56,7 +56,7 @@ func TestConn(t *testing.T) *driver.Driver {
return d
}
func GetVM(t *testing.T, d *driver.Driver, artifacts []packer.Artifact) *driver.VirtualMachine {
func GetVM(t *testing.T, d driver.Driver, artifacts []packer.Artifact) driver.VirtualMachine {
artifactRaw := artifacts[0]
artifact, _ := artifactRaw.(*common.Artifact)
+2 -2
View File
@@ -3,11 +3,11 @@ package driver
import "github.com/vmware/govmomi/object"
type Cluster struct {
driver *Driver
driver *VCenterDriver
cluster *object.ClusterComputeResource
}
func (d *Driver) FindCluster(name string) (*Cluster, error) {
func (d *VCenterDriver) FindCluster(name string) (*Cluster, error) {
c, err := d.finder.ClusterComputeResource(d.ctx, name)
if err != nil {
return nil, err
+36 -20
View File
@@ -13,20 +13,31 @@ import (
"github.com/vmware/govmomi/vim25/types"
)
type Datastore struct {
ds *object.Datastore
driver *Driver
type Datastore interface {
Info(params ...string) (*mo.Datastore, error)
FileExists(path string) bool
Name() string
ResolvePath(path string) string
UploadFile(src, dst, host string, setHost bool) error
Delete(path string) error
MakeDirectory(path string) error
Reference() types.ManagedObjectReference
}
func (d *Driver) NewDatastore(ref *types.ManagedObjectReference) *Datastore {
return &Datastore{
type DatastoreDriver struct {
ds *object.Datastore
driver *VCenterDriver
}
func (d *VCenterDriver) NewDatastore(ref *types.ManagedObjectReference) Datastore {
return &DatastoreDriver{
ds: object.NewDatastore(d.client.Client, *ref),
driver: d,
}
}
// If name is an empty string, then resolve host's one
func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
func (d *VCenterDriver) FindDatastore(name string, host string) (Datastore, error) {
if name == "" {
h, err := d.FindHost(host)
if err != nil {
@@ -55,13 +66,13 @@ func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
return nil, err
}
return &Datastore{
return &DatastoreDriver{
ds: ds,
driver: d,
}, nil
}
func (d *Driver) GetDatastoreName(id string) (string, error) {
func (d *VCenterDriver) GetDatastoreName(id string) (string, error) {
obj := types.ManagedObjectReference{
Type: "Datastore",
Value: id,
@@ -76,7 +87,7 @@ func (d *Driver) GetDatastoreName(id string) (string, error) {
return me.Name, nil
}
func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
func (ds *DatastoreDriver) Info(params ...string) (*mo.Datastore, error) {
var p []string
if len(params) == 0 {
p = []string{"*"}
@@ -91,21 +102,25 @@ func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
return &info, nil
}
func (ds *Datastore) FileExists(path string) bool {
func (ds *DatastoreDriver) FileExists(path string) bool {
_, err := ds.ds.Stat(ds.driver.ctx, path)
return err == nil
}
func (ds *Datastore) Name() string {
func (ds *DatastoreDriver) Name() string {
return ds.ds.Name()
}
func (ds *Datastore) ResolvePath(path string) string {
func (ds *DatastoreDriver) Reference() types.ManagedObjectReference {
return ds.ds.Reference()
}
func (ds *DatastoreDriver) 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) {
func (d *VCenterDriver) GetDatastoreFilePath(datastoreID, dir, filename string) (string, error) {
ref := types.ManagedObjectReference{Type: "Datastore", Value: datastoreID}
ds := object.NewDatastore(d.vimClient, ref)
@@ -140,11 +155,11 @@ func (d *Driver) GetDatastoreFilePath(datastoreID, dir, filename string) (string
return res.File[0].GetFileInfo().Path, nil
}
func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_uploads bool) error {
func (ds *DatastoreDriver) UploadFile(src, dst, host string, setHost bool) error {
p := soap.DefaultUpload
ctx := ds.driver.ctx
if set_host_for_datastore_uploads && host != "" {
if setHost && host != "" {
h, err := ds.driver.FindHost(host)
if err != nil {
return err
@@ -155,7 +170,7 @@ func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_up
return ds.ds.UploadFile(ctx, src, dst, &p)
}
func (ds *Datastore) Delete(path string) error {
func (ds *DatastoreDriver) Delete(path string) error {
dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
if err != nil {
return err
@@ -164,7 +179,7 @@ func (ds *Datastore) Delete(path string) error {
return fm.Delete(ds.driver.ctx, path)
}
func (ds *Datastore) MakeDirectory(path string) error {
func (ds *DatastoreDriver) MakeDirectory(path string) error {
dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
if err != nil {
return err
@@ -193,8 +208,9 @@ func (d *DatastoreIsoPath) Validate() bool {
// [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)
// [data-store] /dir/subdir/file
// dir/subdir/file or dir/subdir/file
matched, _ := regexp.MatchString(`^\s*(\[[^\[\]\/]*\])?\s*[^\[\]]+\s*$`, d.path)
return matched
}
@@ -204,7 +220,7 @@ func (d *DatastoreIsoPath) GetFilePath() string {
if len(parts) > 1 {
// removes datastore name from path
filePath = parts[1]
filePath = strings.TrimLeft(filePath, " ")
filePath = strings.TrimSpace(filePath)
}
return filePath
}
+47
View File
@@ -0,0 +1,47 @@
package driver
import (
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)
type DatastoreMock struct {
FileExistsCalled bool
MakeDirectoryCalled bool
UploadFileCalled bool
}
func (ds *DatastoreMock) Info(params ...string) (*mo.Datastore, error) {
return nil, nil
}
func (ds *DatastoreMock) FileExists(path string) bool {
ds.FileExistsCalled = true
return false
}
func (ds *DatastoreMock) Name() string {
return "datastore-mock"
}
func (ds *DatastoreMock) Reference() types.ManagedObjectReference {
return types.ManagedObjectReference{}
}
func (ds *DatastoreMock) ResolvePath(path string) string {
return ""
}
func (ds *DatastoreMock) UploadFile(src, dst, host string, setHost bool) error {
ds.UploadFileCalled = true
return nil
}
func (ds *DatastoreMock) Delete(path string) error {
return nil
}
func (ds *DatastoreMock) MakeDirectory(path string) error {
ds.MakeDirectoryCalled = true
return nil
}

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