Merge remote-tracking branch 'origin/master' into dynamic-source-ami
This commit is contained in:
commit
1b4895c684
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -5,19 +5,26 @@ BACKWARDS INCOMPATIBILITIES:
|
|||
* VNC and VRDP-like features in VirtualBox, VMware, and QEMU now configurable
|
||||
but bind to 127.0.0.1 by default to improve security. See the relevant
|
||||
builder docs for more info.
|
||||
* Docker builder requires Docker > 1.3
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **New Checksum post-processor**: Create a checksum file from your build artifacts as part of your build. [GH-3492]
|
||||
* **New Checksum post-processor**: Create a checksum file from your build artifacts as part of your build. [GH-3492]
|
||||
[GH-3790]
|
||||
* **New build flag** `-on-error` to allow inspection and keeping artifacts on builder errors. [GH-3885]
|
||||
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: Test floppy disk files actually exist [GH-3756]
|
||||
* builder/amazon: Added `disable_stop_instance` option to prevent automatic
|
||||
shutdown when the build is complete [GH-3352]
|
||||
* builder/amazon: Added `skip_region_validation` option to allow newer or
|
||||
custom AWS regions [GH-3598]
|
||||
* builder/amazon: Added `shutdown_behavior` option to support `stop` or
|
||||
`terminate` at the end of the build [GH-3556]
|
||||
* builder/amazon: Support building from scratch with amazon-chroot builder.
|
||||
[GH-3855] [GH-3895]
|
||||
* builder/azure: Now pre-validates `capture_container_name` and
|
||||
`capture_name_prefix` [GH-3537]
|
||||
* builder/azure: Support for custom images [GH-3575]
|
||||
|
@ -25,51 +32,90 @@ IMPROVEMENTS:
|
|||
* builder/azure: Made `tenant_id` optional [GH-3643]
|
||||
* builder/digitalocean: Use `state_timeout` for unlock and off transitions.
|
||||
[GH-3444]
|
||||
* builder/digitalocean: Fixes timeout waiting for snapshot [GH-3868]
|
||||
* builder/digitalocean: Added `user_data_file` support. [GH-3933]
|
||||
* builder/docker: Improved support for Docker pull from Amazon ECR. [GH-3856]
|
||||
* builder/google: Added support for `image_family` [GH-3503]
|
||||
* builder/google: Use gcloud application default credentials. [GH-3655]
|
||||
* builder/google: Signal that startup script fished via metadata. [GH-3873]
|
||||
* builder/google: Add image license metadata. [GH-3873]
|
||||
* builder/google: Enable to select NVMe images. [GH-3338]
|
||||
* builder/google: Create passwords for Windows instances. [GH-3932]
|
||||
* builder/null: Can now be used with WinRM [GH-2525]
|
||||
* builder/parallels: Now pauses between `boot_command` entries when running
|
||||
with `-debug` [GH-3547]
|
||||
* builder/parallels: Support future versions of Parallels by using the latest
|
||||
driver [GH-3673]
|
||||
* builder/parallels: Add support for ctrl, shift and alt keys in `boot_command`.
|
||||
[GH-3767]
|
||||
* builder/qemu: Added `vnc_bind_address` option [GH-3574]
|
||||
* builder/qemu: Specify disk format when starting qemu [GH-3888]
|
||||
* builder/qemu: Now pauses between `boot_command` entries when running with
|
||||
`-debug` [GH-3547]
|
||||
* builder/qemu: Add support for ctrl, shift and alt keys in `boot_command`.
|
||||
[GH-3767]
|
||||
* builder/virtualbox: Now pauses between `boot_command` entries when running
|
||||
with `-debug` [GH-3542]
|
||||
* builder/virtualbox: Added `vrdp_bind_address` option [GH-3566]
|
||||
* builder/virtualbox: Add support for ctrl, shift and alt keys in `boot_command`.
|
||||
[GH-3767]
|
||||
* builder/vmware: Now paused between `boot_command` entries when running with
|
||||
`-debug` [GH-3542]
|
||||
* builder/vmware: Added `vnc_bind_address` option [GH-3565]
|
||||
* builder/vmware: Adds passwords for VNC [GH-2325]
|
||||
* builder/vmware: Handle connection to VM with more than one NIC on ESXi
|
||||
[GH-3347]
|
||||
* builder/qemu: Now pauses between `boot_command` entries when running with
|
||||
`-debug` [GH-3547]
|
||||
* builder/vmware: Add support for ctrl, shift and alt keys in `boot_command`.
|
||||
[GH-3767]
|
||||
* provisioner/ansible: Improved logging and error handling [GH-3477]
|
||||
* provisioner/ansible: Support scp [GH-3861]
|
||||
* provisioner/ansible-local: Support for ansible-galaxy [GH-3350] [GH-3836]
|
||||
* provisioner/chef: Added `knife_command` option and added a correct default
|
||||
value for Windows [GH-3622]
|
||||
* provisioner/chef: Installs 64bit chef on Windows if available [GH-3848]
|
||||
* provisioner/puppet: Added `execute_command` option [GH-3614]
|
||||
* post-processor/amazon-import: Support `ami_name` for naming imported AMI.
|
||||
[GH-3941]
|
||||
* post-processor/compress: Added support for bgzf compression [GH-3501]
|
||||
* post-processor/docker: Preserve tags when running docker push [GH-3631]
|
||||
* post-processor/docker: Improved support for Docker push to Amazon ECR [GH-3856]
|
||||
* scripts: Added `help` target to Makefile [GH-3290]
|
||||
* builder/googlecompute: Add `-force` option to delete old image before
|
||||
creating new one. [GH-3918]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* post-processor/shell-local: Do not set execute bit on artifact file [GH-3505]
|
||||
* post-processor/vsphere: Fix upload failures with vsphere [GH-3321]
|
||||
* provisioner/ansible: Properly set host key checking even when a custom ENV
|
||||
is specified [GH-3568]
|
||||
* builder/amazon: Use `temporary_key_pair_name` when specified. [GH-3739]
|
||||
* builder/amazon: Add 0.5 cents to discovered spot price. [GH-3662]
|
||||
* builder/amazon: Fix packer crash when waiting for SSH. [GH-3865]
|
||||
* builder/amazon: Honor ssh_private_ip flag in EC2-Classic. [GH-3752]
|
||||
* builder/azure: check for empty resource group [GH-3606]
|
||||
* builder/azure: fix token validity test [GH-3609]
|
||||
* builder/docker: fix docker builder with ansible provisioner. [GH-3476]
|
||||
* builder/docker: Fix file provisioner dotfile matching. [GH-3800]
|
||||
* builder/virtualbox: Respect `ssh_host` [GH-3617]
|
||||
* builder/virtualbox: Make `ssh_host_port_max` an inclusive bound. [GH-2784]
|
||||
* builder/vmware: Re-introduce case sensitive VMX keys [GH-2707]
|
||||
* builder/vmware: Don't check for poweron errors on ESXi [GH-3195]
|
||||
* builder/vmware: Respect `ssh_host`/`winrm_host` on ESXi [GH-3738]
|
||||
* builder/vmware: Do not add remotedisplay.vnc.ip to VMX data on ESXi
|
||||
[GH-3740]
|
||||
* builder/qemu: Don't fail on communicator set to `none`. [GH-3681]
|
||||
* builder/qemu: Make `ssh_host_port_max` an inclusive bound. [GH-2784]
|
||||
* post-processor/shell-local: Do not set execute bit on artifact file [GH-3505]
|
||||
* post-processor/vsphere: Fix upload failures with vsphere [GH-3321]
|
||||
* provisioner/ansible: Properly set host key checking even when a custom ENV
|
||||
is specified [GH-3568]
|
||||
* website: improved rendering on iPad [GH-3780]
|
||||
* provisioner/file: Fix directory download. [GH-3899]
|
||||
* command/push: Allows dot (`.`) in image names. [GH-3937]
|
||||
* builder/amazon: add retry logic when creating tags.
|
||||
|
||||
## 0.10.2 (September 20, 2016)
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* Rebuilding with OS X Sierra and go 1.7.1 to fix bug in Sierra
|
||||
|
||||
## 0.10.1 (May 7, 2016)
|
||||
|
||||
|
|
|
@ -80,138 +80,163 @@
|
|||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/awserr",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/awsutil",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/client",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/client/metadata",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/corehandlers",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/stscreds",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/defaults",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/ec2metadata",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/request",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/session",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws/signer/v4",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/jsonrpc",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/restxml",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/ec2",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/ecr",
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/s3",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/s3/s3iface",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/s3/s3manager",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/sts",
|
||||
"Comment": "v1.1.2",
|
||||
"Rev": "8041be5461786460d86b4358305fbdf32d37cfb2"
|
||||
"Comment": "v1.4.6",
|
||||
"Rev": "6ac30507cca29249f4d49af45a8efc98b84088ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/bgentry/speakeasy",
|
||||
|
@ -221,6 +246,10 @@
|
|||
"ImportPath": "github.com/biogo/hts/bgzf",
|
||||
"Rev": "50da7d4131a3b5c9d063932461cab4d1fafb20b0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/davecgh/go-spew/spew",
|
||||
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/dgrijalva/jwt-go",
|
||||
"Comment": "v3.0.0",
|
||||
|
@ -427,6 +456,11 @@
|
|||
"ImportPath": "github.com/pkg/sftp",
|
||||
"Rev": "e84cc8c755ca39b7b64f510fe1fffc1b51f210a5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/pmezard/go-difflib/difflib",
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/rackspace/gophercloud",
|
||||
"Comment": "v1.0.0-810-g53d1dc4",
|
||||
|
@ -516,6 +550,11 @@
|
|||
"ImportPath": "github.com/satori/go.uuid",
|
||||
"Rev": "d41af8bb6a7704f00bc3b7cba9355ae6a5a80048"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/stretchr/testify/assert",
|
||||
"Comment": "v1.1.3-19-gd77da35",
|
||||
"Rev": "d77da356e56a7428ad25149ca77381849a6a5232"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/tent/http-link-go",
|
||||
"Rev": "ac974c61c2f990f4115b119354b5e0b47550e888"
|
||||
|
@ -648,31 +687,6 @@
|
|||
{
|
||||
"ImportPath": "gopkg.in/xmlpath.v2",
|
||||
"Rev": "860cbeca3ebcc600db0b213c0e83ad6ce91f5739"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Azure/azure-sdk-for-go/vendor/github.com/Azure/go-autorest/autorest",
|
||||
"Comment": "v7.0.5-4-g0a0ee7d",
|
||||
"Rev": "0a0ee7d5b9b1b3d980434cbb0afff33e9ca9e907"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Azure/azure-sdk-for-go/vendor/github.com/Azure/go-autorest/autorest/azure",
|
||||
"Comment": "v7.0.5-4-g0a0ee7d",
|
||||
"Rev": "0a0ee7d5b9b1b3d980434cbb0afff33e9ca9e907"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Azure/azure-sdk-for-go/vendor/github.com/Azure/go-autorest/autorest/date",
|
||||
"Comment": "v7.0.5-4-g0a0ee7d",
|
||||
"Rev": "0a0ee7d5b9b1b3d980434cbb0afff33e9ca9e907"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Azure/azure-sdk-for-go/vendor/github.com/Azure/go-autorest/autorest/to",
|
||||
"Comment": "v7.0.5-4-g0a0ee7d",
|
||||
"Rev": "0a0ee7d5b9b1b3d980434cbb0afff33e9ca9e907"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Azure/azure-sdk-for-go/vendor/github.com/dgrijalva/jwt-go",
|
||||
"Comment": "v3.0.0-2-gf077707",
|
||||
"Rev": "f0777076321ab64f6efc15a82d9d23b98539b943"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
2
Makefile
2
Makefile
|
@ -53,7 +53,7 @@ fmt-examples:
|
|||
# source files.
|
||||
generate: deps ## Generate dynamically generated code
|
||||
go generate .
|
||||
go fmt command/plugin.go
|
||||
gofmt -w command/plugin.go
|
||||
|
||||
test: deps ## Run unit tests
|
||||
@go test $(TEST) $(TESTARGS) -timeout=2m
|
||||
|
|
|
@ -25,19 +25,24 @@ const BuilderId = "mitchellh.amazon.chroot"
|
|||
// Config is the configuration that is chained through the steps and
|
||||
// settable from the template.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AMIBlockDevices `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CommandWrapper string `mapstructure:"command_wrapper"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
RootVolumeSize int64 `mapstructure:"root_volume_size"`
|
||||
MountOptions []string `mapstructure:"mount_options"`
|
||||
MountPartition int `mapstructure:"mount_partition"`
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CommandWrapper string `mapstructure:"command_wrapper"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
FromScratch bool `mapstructure:"from_scratch"`
|
||||
MountOptions []string `mapstructure:"mount_options"`
|
||||
MountPartition int `mapstructure:"mount_partition"`
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
PostMountCommands []string `mapstructure:"post_mount_commands"`
|
||||
PreMountCommands []string `mapstructure:"pre_mount_commands"`
|
||||
RootDeviceName string `mapstructure:"root_device_name"`
|
||||
RootVolumeSize int64 `mapstructure:"root_volume_size"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
@ -59,6 +64,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"command_wrapper",
|
||||
"post_mount_commands",
|
||||
"pre_mount_commands",
|
||||
"mount_path",
|
||||
},
|
||||
},
|
||||
|
@ -86,7 +93,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(b.config.CopyFiles) == 0 {
|
||||
if len(b.config.CopyFiles) == 0 && !b.config.FromScratch {
|
||||
b.config.CopyFiles = []string{"/etc/resolv.conf"}
|
||||
}
|
||||
|
||||
|
@ -102,8 +109,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.MountPartition = 1
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
// Accumulate any errors or warnings
|
||||
var errs *packer.MultiError
|
||||
var warns []string
|
||||
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
|
@ -115,16 +124,49 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if b.config.SourceAmi == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
|
||||
if b.config.FromScratch {
|
||||
if b.config.SourceAmi != "" {
|
||||
warns = append(warns, "source_ami is unused when from_scratch is true")
|
||||
}
|
||||
if b.config.RootVolumeSize == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("root_volume_size is required with from_scratch."))
|
||||
}
|
||||
if len(b.config.PreMountCommands) == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("pre_mount_commands is required with from_scratch."))
|
||||
}
|
||||
if b.config.AMIVirtType == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_virtualization_type is required with from_scratch."))
|
||||
}
|
||||
if b.config.RootDeviceName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("root_device_name is required with from_scratch."))
|
||||
}
|
||||
if len(b.config.AMIMappings) == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_block_device_mappings is required with from_scratch."))
|
||||
}
|
||||
} else {
|
||||
if b.config.SourceAmi == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("source_ami is required."))
|
||||
}
|
||||
if len(b.config.AMIMappings) != 0 {
|
||||
warns = append(warns, "ami_block_device_mappings are unused when from_scratch is false")
|
||||
}
|
||||
if b.config.RootDeviceName != "" {
|
||||
warns = append(warns, "root_device_name is unused when from_scratch is false")
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
return warns, errs
|
||||
}
|
||||
|
||||
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
|
||||
return nil, nil
|
||||
return warns, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
|
@ -161,11 +203,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&StepInstanceInfo{},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&StepCheckRootDevice{},
|
||||
}
|
||||
|
||||
if !b.config.FromScratch {
|
||||
steps = append(steps,
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&StepCheckRootDevice{},
|
||||
)
|
||||
}
|
||||
|
||||
steps = append(steps,
|
||||
&StepFlock{},
|
||||
&StepPrepareDevice{},
|
||||
&StepCreateVolume{
|
||||
|
@ -173,10 +223,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
},
|
||||
&StepAttachVolume{},
|
||||
&StepEarlyUnflock{},
|
||||
&StepPreMountCommands{
|
||||
Commands: b.config.PreMountCommands,
|
||||
},
|
||||
&StepMountDevice{
|
||||
MountOptions: b.config.MountOptions,
|
||||
MountPartition: b.config.MountPartition,
|
||||
},
|
||||
&StepPostMountCommands{
|
||||
Commands: b.config.PostMountCommands,
|
||||
},
|
||||
&StepMountExtra{},
|
||||
&StepCopyFiles{},
|
||||
&StepChrootProvision{},
|
||||
|
@ -203,18 +259,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/post-processor/shell-local"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
func RunLocalCommands(commands []string, wrappedCommand CommandWrapper, ctx interpolate.Context, ui packer.Ui) error {
|
||||
for _, rawCmd := range commands {
|
||||
intCmd, err := interpolate.Render(rawCmd, &ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error interpolating: %s", err)
|
||||
}
|
||||
|
||||
command, err := wrappedCommand(intCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error wrapping command: %s", err)
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Executing command: %s", command))
|
||||
comm := &shell_local.Communicator{}
|
||||
cmd := &packer.RemoteCmd{Command: command}
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return fmt.Errorf("Error executing command: %s", err)
|
||||
}
|
||||
if cmd.ExitStatus != 0 {
|
||||
return fmt.Errorf(
|
||||
"Received non-zero exit code %d from command: %s",
|
||||
cmd.ExitStatus,
|
||||
command)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -22,40 +22,52 @@ type StepCreateVolume struct {
|
|||
}
|
||||
|
||||
func (s *StepCreateVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Determine the root device snapshot
|
||||
log.Printf("Searching for root device of the image (%s)", *image.RootDeviceName)
|
||||
var rootDevice *ec2.BlockDeviceMapping
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if *device.DeviceName == *image.RootDeviceName {
|
||||
rootDevice = device
|
||||
break
|
||||
var createVolume *ec2.CreateVolumeInput
|
||||
if config.FromScratch {
|
||||
createVolume = &ec2.CreateVolumeInput{
|
||||
AvailabilityZone: instance.Placement.AvailabilityZone,
|
||||
Size: aws.Int64(s.RootVolumeSize),
|
||||
VolumeType: aws.String(ec2.VolumeTypeGp2),
|
||||
}
|
||||
} else {
|
||||
// Determine the root device snapshot
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
log.Printf("Searching for root device of the image (%s)", *image.RootDeviceName)
|
||||
var rootDevice *ec2.BlockDeviceMapping
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if *device.DeviceName == *image.RootDeviceName {
|
||||
rootDevice = device
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rootDevice == nil {
|
||||
err := fmt.Errorf("Couldn't find root device!")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Creating the root volume...")
|
||||
vs := *rootDevice.Ebs.VolumeSize
|
||||
if s.RootVolumeSize > *rootDevice.Ebs.VolumeSize {
|
||||
vs = s.RootVolumeSize
|
||||
}
|
||||
|
||||
createVolume = &ec2.CreateVolumeInput{
|
||||
AvailabilityZone: instance.Placement.AvailabilityZone,
|
||||
Size: aws.Int64(vs),
|
||||
SnapshotId: rootDevice.Ebs.SnapshotId,
|
||||
VolumeType: rootDevice.Ebs.VolumeType,
|
||||
Iops: rootDevice.Ebs.Iops,
|
||||
}
|
||||
}
|
||||
|
||||
if rootDevice == nil {
|
||||
err := fmt.Errorf("Couldn't find root device!")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Creating the root volume...")
|
||||
vs := *rootDevice.Ebs.VolumeSize
|
||||
if s.RootVolumeSize > *rootDevice.Ebs.VolumeSize {
|
||||
vs = s.RootVolumeSize
|
||||
}
|
||||
createVolume := &ec2.CreateVolumeInput{
|
||||
AvailabilityZone: instance.Placement.AvailabilityZone,
|
||||
Size: aws.Int64(vs),
|
||||
SnapshotId: rootDevice.Ebs.SnapshotId,
|
||||
VolumeType: rootDevice.Ebs.VolumeType,
|
||||
Iops: rootDevice.Ebs.Iops,
|
||||
}
|
||||
log.Printf("Create args: %+v", createVolume)
|
||||
|
||||
createVolumeResp, err := ec2conn.CreateVolume(createVolume)
|
||||
|
|
|
@ -33,10 +33,18 @@ type StepMountDevice struct {
|
|||
func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
device := state.Get("device").(string)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
var virtualizationType string
|
||||
if config.FromScratch {
|
||||
virtualizationType = config.AMIVirtType
|
||||
} else {
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
virtualizationType = *image.VirtualizationType
|
||||
log.Printf("Source image virtualization type is: %s", virtualizationType)
|
||||
}
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &mountPathData{Device: filepath.Base(device)}
|
||||
mountPath, err := interpolate.Render(config.MountPath, &ctx)
|
||||
|
@ -65,9 +73,8 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Source image virtualization type is: %s", *image.VirtualizationType)
|
||||
deviceMount := device
|
||||
if *image.VirtualizationType == "hvm" {
|
||||
if virtualizationType == "hvm" {
|
||||
deviceMount = fmt.Sprintf("%s%d", device, s.MountPartition)
|
||||
}
|
||||
state.Put("deviceMount", deviceMount)
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type postMountCommandsData struct {
|
||||
Device string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
// StepPostMountCommands allows running arbitrary commands after mounting the
|
||||
// device, but prior to the bind mount and copy steps.
|
||||
type StepPostMountCommands struct {
|
||||
Commands []string
|
||||
}
|
||||
|
||||
func (s *StepPostMountCommands) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
device := state.Get("device").(string)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
if len(s.Commands) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &postMountCommandsData{
|
||||
Device: device,
|
||||
MountPath: mountPath,
|
||||
}
|
||||
|
||||
ui.Say("Running post-mount commands...")
|
||||
if err := RunLocalCommands(s.Commands, wrappedCommand, ctx, ui); err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPostMountCommands) Cleanup(state multistep.StateBag) {}
|
|
@ -0,0 +1,39 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type preMountCommandsData struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
// StepPreMountCommands sets up the a new block device when building from scratch
|
||||
type StepPreMountCommands struct {
|
||||
Commands []string
|
||||
}
|
||||
|
||||
func (s *StepPreMountCommands) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
device := state.Get("device").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
if len(s.Commands) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &preMountCommandsData{Device: device}
|
||||
|
||||
ui.Say("Running device setup commands...")
|
||||
if err := RunLocalCommands(s.Commands, wrappedCommand, ctx, ui); err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPreMountCommands) Cleanup(state multistep.StateBag) {}
|
|
@ -18,22 +18,38 @@ type StepRegisterAMI struct {
|
|||
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
snapshotId := state.Get("snapshot_id").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
blockDevices := make([]*ec2.BlockDeviceMapping, len(image.BlockDeviceMappings))
|
||||
for i, device := range image.BlockDeviceMappings {
|
||||
|
||||
var (
|
||||
registerOpts *ec2.RegisterImageInput
|
||||
mappings []*ec2.BlockDeviceMapping
|
||||
image *ec2.Image
|
||||
rootDeviceName string
|
||||
)
|
||||
|
||||
if config.FromScratch {
|
||||
mappings = config.AMIBlockDevices.BuildAMIDevices()
|
||||
rootDeviceName = config.RootDeviceName
|
||||
} else {
|
||||
image = state.Get("source_image").(*ec2.Image)
|
||||
mappings = image.BlockDeviceMappings
|
||||
rootDeviceName = *image.RootDeviceName
|
||||
}
|
||||
|
||||
newMappings := make([]*ec2.BlockDeviceMapping, len(mappings))
|
||||
for i, device := range mappings {
|
||||
newDevice := device
|
||||
if *newDevice.DeviceName == *image.RootDeviceName {
|
||||
if *newDevice.DeviceName == rootDeviceName {
|
||||
if newDevice.Ebs != nil {
|
||||
newDevice.Ebs.SnapshotId = aws.String(snapshotId)
|
||||
} else {
|
||||
newDevice.Ebs = &ec2.EbsBlockDevice{SnapshotId: aws.String(snapshotId)}
|
||||
}
|
||||
|
||||
if s.RootVolumeSize > *newDevice.Ebs.VolumeSize {
|
||||
if config.FromScratch || s.RootVolumeSize > *newDevice.Ebs.VolumeSize {
|
||||
newDevice.Ebs.VolumeSize = aws.Int64(s.RootVolumeSize)
|
||||
}
|
||||
}
|
||||
|
@ -44,10 +60,20 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
newDevice.Ebs.Encrypted = nil
|
||||
}
|
||||
|
||||
blockDevices[i] = newDevice
|
||||
newMappings[i] = newDevice
|
||||
}
|
||||
|
||||
registerOpts := buildRegisterOpts(config, image, blockDevices)
|
||||
if config.FromScratch {
|
||||
registerOpts = &ec2.RegisterImageInput{
|
||||
Name: &config.AMIName,
|
||||
Architecture: aws.String(ec2.ArchitectureValuesX8664),
|
||||
RootDeviceName: aws.String(rootDeviceName),
|
||||
VirtualizationType: aws.String(config.AMIVirtType),
|
||||
BlockDeviceMappings: newMappings,
|
||||
}
|
||||
} else {
|
||||
registerOpts = buildRegisterOpts(config, image, newMappings)
|
||||
}
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
|
@ -88,12 +114,12 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) {}
|
||||
|
||||
func buildRegisterOpts(config *Config, image *ec2.Image, blockDevices []*ec2.BlockDeviceMapping) *ec2.RegisterImageInput {
|
||||
func buildRegisterOpts(config *Config, image *ec2.Image, mappings []*ec2.BlockDeviceMapping) *ec2.RegisterImageInput {
|
||||
registerOpts := &ec2.RegisterImageInput{
|
||||
Name: &config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
RootDeviceName: image.RootDeviceName,
|
||||
BlockDeviceMappings: blockDevices,
|
||||
BlockDeviceMappings: mappings,
|
||||
VirtualizationType: image.VirtualizationType,
|
||||
}
|
||||
|
||||
|
@ -105,6 +131,5 @@ func buildRegisterOpts(config *Config, image *ec2.Image, blockDevices []*ec2.Blo
|
|||
registerOpts.KernelId = image.KernelId
|
||||
registerOpts.RamdiskId = image.RamdiskId
|
||||
}
|
||||
|
||||
return registerOpts
|
||||
}
|
||||
|
|
|
@ -22,7 +22,15 @@ type BlockDevice struct {
|
|||
}
|
||||
|
||||
type BlockDevices struct {
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
|
||||
AMIBlockDevices `mapstructure:",squash"`
|
||||
LaunchBlockDevices `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type AMIBlockDevices struct {
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
|
||||
}
|
||||
|
||||
type LaunchBlockDevices struct {
|
||||
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings"`
|
||||
}
|
||||
|
||||
|
@ -77,10 +85,10 @@ func (b *BlockDevices) Prepare(ctx *interpolate.Context) []error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildAMIDevices() []*ec2.BlockDeviceMapping {
|
||||
func (b *AMIBlockDevices) BuildAMIDevices() []*ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.AMIMappings)
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildLaunchDevices() []*ec2.BlockDeviceMapping {
|
||||
func (b *LaunchBlockDevices) BuildLaunchDevices() []*ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.LaunchMappings)
|
||||
}
|
||||
|
|
|
@ -124,22 +124,26 @@ func TestBlockDevice(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
blockDevices := BlockDevices{
|
||||
AMIMappings: []BlockDevice{*tc.Config},
|
||||
amiBlockDevices := AMIBlockDevices{
|
||||
AMIMappings: []BlockDevice{*tc.Config},
|
||||
}
|
||||
|
||||
launchBlockDevices := LaunchBlockDevices{
|
||||
LaunchMappings: []BlockDevice{*tc.Config},
|
||||
}
|
||||
|
||||
expected := []*ec2.BlockDeviceMapping{tc.Result}
|
||||
got := blockDevices.BuildAMIDevices()
|
||||
if !reflect.DeepEqual(expected, got) {
|
||||
|
||||
amiResults := amiBlockDevices.BuildAMIDevices()
|
||||
if !reflect.DeepEqual(expected, amiResults) {
|
||||
t.Fatalf("Bad block device, \nexpected: %#v\n\ngot: %#v",
|
||||
expected, got)
|
||||
expected, amiResults)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
|
||||
launchResults := launchBlockDevices.BuildLaunchDevices()
|
||||
if !reflect.DeepEqual(expected, launchResults) {
|
||||
t.Fatalf("Bad block device, \nexpected: %#v\n\ngot: %#v",
|
||||
expected,
|
||||
blockDevices.BuildLaunchDevices())
|
||||
expected, launchResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,19 +10,32 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ec2Describer interface {
|
||||
DescribeInstances(*ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error)
|
||||
}
|
||||
|
||||
var (
|
||||
// modified in tests
|
||||
sshHostSleepDuration = time.Second
|
||||
)
|
||||
|
||||
// SSHHost returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the instance DNS name.
|
||||
func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error) {
|
||||
func SSHHost(e ec2Describer, private bool) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
for j := 0; j < 2; j++ {
|
||||
const tries = 2
|
||||
// <= with current structure to check result of describing `tries` times
|
||||
for j := 0; j <= tries; j++ {
|
||||
var host string
|
||||
i := state.Get("instance").(*ec2.Instance)
|
||||
if i.VpcId != nil && *i.VpcId != "" {
|
||||
if i.PublicIpAddress != nil && *i.PublicIpAddress != "" && !private {
|
||||
host = *i.PublicIpAddress
|
||||
} else {
|
||||
} else if i.PrivateIpAddress != nil && *i.PrivateIpAddress != "" {
|
||||
host = *i.PrivateIpAddress
|
||||
}
|
||||
} else if private && i.PrivateIpAddress != nil && *i.PrivateIpAddress != "" {
|
||||
host = *i.PrivateIpAddress
|
||||
} else if i.PublicDnsName != nil && *i.PublicDnsName != "" {
|
||||
host = *i.PublicDnsName
|
||||
}
|
||||
|
@ -42,8 +55,8 @@ func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error)
|
|||
return "", fmt.Errorf("instance not found: %s", *i.InstanceId)
|
||||
}
|
||||
|
||||
state.Put("instance", &r.Reservations[0].Instances[0])
|
||||
time.Sleep(1 * time.Second)
|
||||
state.Put("instance", r.Reservations[0].Instances[0])
|
||||
time.Sleep(sshHostSleepDuration)
|
||||
}
|
||||
|
||||
return "", errors.New("couldn't determine IP address for instance")
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
const (
|
||||
privateIP = "10.0.0.1"
|
||||
publicIP = "192.168.1.1"
|
||||
publicDNS = "public.dns.test"
|
||||
)
|
||||
|
||||
func TestSSHHost(t *testing.T) {
|
||||
origSshHostSleepDuration := sshHostSleepDuration
|
||||
defer func() { sshHostSleepDuration = origSshHostSleepDuration }()
|
||||
sshHostSleepDuration = 0
|
||||
|
||||
var cases = []struct {
|
||||
allowTries int
|
||||
vpcId string
|
||||
private bool
|
||||
|
||||
ok bool
|
||||
wantHost string
|
||||
}{
|
||||
{1, "", false, true, publicDNS},
|
||||
{1, "", true, true, privateIP},
|
||||
{1, "vpc-id", false, true, publicIP},
|
||||
{1, "vpc-id", true, true, privateIP},
|
||||
{2, "", false, true, publicDNS},
|
||||
{2, "", true, true, privateIP},
|
||||
{2, "vpc-id", false, true, publicIP},
|
||||
{2, "vpc-id", true, true, privateIP},
|
||||
{3, "", false, false, ""},
|
||||
{3, "", true, false, ""},
|
||||
{3, "vpc-id", false, false, ""},
|
||||
{3, "vpc-id", true, false, ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
testSSHHost(t, c.allowTries, c.vpcId, c.private, c.ok, c.wantHost)
|
||||
}
|
||||
}
|
||||
|
||||
func testSSHHost(t *testing.T, allowTries int, vpcId string, private, ok bool, wantHost string) {
|
||||
t.Logf("allowTries=%d vpcId=%s private=%t ok=%t wantHost=%q", allowTries, vpcId, private, ok, wantHost)
|
||||
|
||||
e := &fakeEC2Describer{
|
||||
allowTries: allowTries,
|
||||
vpcId: vpcId,
|
||||
privateIP: privateIP,
|
||||
publicIP: publicIP,
|
||||
publicDNS: publicDNS,
|
||||
}
|
||||
|
||||
f := SSHHost(e, private)
|
||||
st := &multistep.BasicStateBag{}
|
||||
st.Put("instance", &ec2.Instance{
|
||||
InstanceId: aws.String("instance-id"),
|
||||
})
|
||||
|
||||
host, err := f(st)
|
||||
|
||||
if e.tries > allowTries {
|
||||
t.Fatalf("got %d ec2 DescribeInstances tries, want %d", e.tries, allowTries)
|
||||
}
|
||||
|
||||
switch {
|
||||
case ok && err != nil:
|
||||
t.Fatalf("expected no error, got %+v", err)
|
||||
case !ok && err == nil:
|
||||
t.Fatalf("expected error, got none and host %s", host)
|
||||
}
|
||||
|
||||
if host != wantHost {
|
||||
t.Fatalf("got host %s, want %s", host, wantHost)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEC2Describer struct {
|
||||
allowTries int
|
||||
tries int
|
||||
|
||||
vpcId string
|
||||
privateIP, publicIP, publicDNS string
|
||||
}
|
||||
|
||||
func (d *fakeEC2Describer) DescribeInstances(in *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
|
||||
d.tries++
|
||||
|
||||
instance := &ec2.Instance{
|
||||
InstanceId: aws.String("instance-id"),
|
||||
}
|
||||
|
||||
if d.vpcId != "" {
|
||||
instance.VpcId = aws.String(d.vpcId)
|
||||
}
|
||||
|
||||
if d.tries >= d.allowTries {
|
||||
instance.PublicIpAddress = aws.String(d.publicIP)
|
||||
instance.PrivateIpAddress = aws.String(d.privateIP)
|
||||
instance.PublicDnsName = aws.String(d.publicDNS)
|
||||
}
|
||||
|
||||
out := &ec2.DescribeInstancesOutput{
|
||||
Reservations: []*ec2.Reservation{
|
||||
{
|
||||
Instances: []*ec2.Instance{instance},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
|
@ -4,9 +4,11 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
retry "github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
|
@ -71,9 +73,22 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
}
|
||||
|
||||
_, err = regionconn.CreateTags(&ec2.CreateTagsInput{
|
||||
Resources: resourceIds,
|
||||
Tags: ec2Tags,
|
||||
// Retry creating tags for about 2.5 minutes
|
||||
err = retry.Retry(0.2, 30, 11, func() (bool, error) {
|
||||
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{
|
||||
Resources: resourceIds,
|
||||
Tags: ec2Tags,
|
||||
})
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
if awsErr.Code() == "InvalidAMIID.NotFound" ||
|
||||
awsErr.Code() == "InvalidSnapshot.NotFound" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -180,15 +180,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -262,15 +262,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -157,7 +157,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
ui.Message(fmt.Sprintf("temp admin password: '%s'", b.config.Password))
|
||||
}
|
||||
|
||||
b.runner = b.createRunner(&steps, ui)
|
||||
b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(b.stateBag)
|
||||
|
||||
// Report any errors.
|
||||
|
@ -198,19 +198,6 @@ func (b *Builder) Cancel() {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *Builder) createRunner(steps *[]multistep.Step, ui packer.Ui) multistep.Runner {
|
||||
if b.config.PackerDebug {
|
||||
return &multistep.DebugRunner{
|
||||
Steps: *steps,
|
||||
PauseFn: packerCommon.MultistepDebugFn(ui),
|
||||
}
|
||||
}
|
||||
|
||||
return &multistep.BasicRunner{
|
||||
Steps: *steps,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) getBlobEndpoint(client *AzureClient, resourceGroupName string, storageAccountName string) (string, error) {
|
||||
account, err := client.AccountsClient.GetProperties(resourceGroupName, storageAccountName)
|
||||
if err != nil {
|
||||
|
|
|
@ -76,7 +76,7 @@ func findVirtualNetworkResourceGroup(client *AzureClient, name string) (string,
|
|||
}
|
||||
|
||||
if len(resourceGroupNames) > 1 {
|
||||
return "", fmt.Errorf("Found multiple resource groups with a virtual network called %q, please use virtual_network_resource_group_name to disambiguate", name)
|
||||
return "", fmt.Errorf("Found multiple resource groups with a virtual network called %q, please use virtual_network_subnet_name and virtual_network_resource_group_name to disambiguate", name)
|
||||
}
|
||||
|
||||
return resourceGroupNames[0], nil
|
||||
|
@ -93,7 +93,7 @@ func findVirtualNetworkSubnet(client *AzureClient, resourceGroupName string, nam
|
|||
}
|
||||
|
||||
if len(*subnets.Value) > 1 {
|
||||
return "", fmt.Errorf("Found multiple subnets in the resource group %q associated with the virtual network called %q, please use virtual_network_subnet_name to disambiguate", resourceGroupName, name)
|
||||
return "", fmt.Errorf("Found multiple subnets in the resource group %q associated with the virtual network called %q, please use virtual_network_subnet_name and virtual_network_resource_group_name to disambiguate", resourceGroupName, name)
|
||||
}
|
||||
|
||||
subnet := (*subnets.Value)[0]
|
||||
|
|
|
@ -73,15 +73,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run the steps
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -27,7 +27,9 @@ const testBuilderAccBasic = `
|
|||
"type": "test",
|
||||
"region": "nyc2",
|
||||
"size": "512mb",
|
||||
"image": "ubuntu-12-04-x64"
|
||||
"image": "ubuntu-12-04-x64",
|
||||
"user_date": "",
|
||||
"user_date_file": ""
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -31,6 +31,7 @@ type Config struct {
|
|||
StateTimeout time.Duration `mapstructure:"state_timeout"`
|
||||
DropletName string `mapstructure:"droplet_name"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
@ -113,6 +114,16 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
errs, errors.New("image is required"))
|
||||
}
|
||||
|
||||
if c.UserData != "" && c.UserDataFile != "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("only one of user_data or user_data_file can be specified"))
|
||||
} else if c.UserDataFile != "" {
|
||||
if _, err := os.Stat(c.UserDataFile); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New(fmt.Sprintf("user_data_file not found: %s", c.UserDataFile)))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, nil, errs
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type stepCreateDroplet struct {
|
||||
|
@ -20,6 +21,18 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
// Create the droplet based on configuration
|
||||
ui.Say("Creating droplet...")
|
||||
|
||||
userData := c.UserData
|
||||
if c.UserDataFile != "" {
|
||||
contents, err := ioutil.ReadFile(c.UserDataFile)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
}
|
||||
|
||||
droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{
|
||||
Name: c.DropletName,
|
||||
Region: c.Region,
|
||||
|
@ -31,7 +44,7 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
|||
godo.DropletCreateSSHKey{ID: int(sshKeyId)},
|
||||
},
|
||||
PrivateNetworking: c.PrivateNetworking,
|
||||
UserData: c.UserData,
|
||||
UserData: userData,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating droplet: %s", err)
|
||||
|
|
|
@ -20,7 +20,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
dropletId := state.Get("droplet_id").(int)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
|
||||
_, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
|
||||
action, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -28,6 +28,17 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// With the pending state over, verify that we're in the active state
|
||||
ui.Say("Waiting for snapshot to complete...")
|
||||
if err := waitForActionState(godo.ActionCompleted, dropletId, action.ID,
|
||||
client, 20*time.Minute); err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error waiting for snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the droplet to become unlocked first. For snapshots
|
||||
// this can end up taking quite a long time, so we hardcode this to
|
||||
// 20 minutes.
|
||||
|
@ -39,16 +50,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// With the pending state over, verify that we're in the active state
|
||||
ui.Say("Waiting for snapshot to complete...")
|
||||
err = waitForDropletState("active", dropletId, client, c.StateTimeout)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
|
||||
images, _, err := client.Droplets.Snapshots(dropletId, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -57,7 +57,7 @@ func waitForDropletUnlocked(
|
|||
}
|
||||
}
|
||||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// waitForDropletState simply blocks until the droplet is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForDropletState(
|
||||
desiredState string, dropletId int,
|
||||
|
@ -106,3 +106,53 @@ func waitForDropletState(
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// waitForActionState simply blocks until the droplet action is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForActionState(
|
||||
desiredState string, dropletId, actionId int,
|
||||
client *godo.Client, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
attempts := 0
|
||||
for {
|
||||
attempts += 1
|
||||
|
||||
log.Printf("Checking action status... (attempt: %d)", attempts)
|
||||
action, _, err := client.DropletActions.Get(dropletId, actionId)
|
||||
if err != nil {
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
|
||||
if action.Status == desiredState {
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// Wait 3 seconds in between
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Verify we shouldn't exit
|
||||
select {
|
||||
case <-done:
|
||||
// We finished, so just exit the goroutine
|
||||
return
|
||||
default:
|
||||
// Keep going
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for up to %d seconds for action to become %s", timeout/time.Second, desiredState)
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
err := fmt.Errorf("Timeout while waiting to for action to become '%s'", desiredState)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,15 +78,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
state.Put("driver", driver)
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -2,7 +2,6 @@ package docker
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -10,12 +9,10 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ActiveState/tail"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -30,42 +27,35 @@ type Communicator struct {
|
|||
}
|
||||
|
||||
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
|
||||
// Create a temporary file to store the output. Because of a bug in
|
||||
// Docker, sometimes all the output doesn't properly show up. This
|
||||
// file will capture ALL of the output, and we'll read that.
|
||||
//
|
||||
// https://github.com/dotcloud/docker/issues/2625
|
||||
outputFile, err := ioutil.TempFile(c.HostDir, "cmd")
|
||||
var cmd *exec.Cmd
|
||||
if c.Config.Pty {
|
||||
cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
|
||||
} else {
|
||||
cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
|
||||
}
|
||||
|
||||
var (
|
||||
stdin_w io.WriteCloser
|
||||
err error
|
||||
)
|
||||
|
||||
stdin_w, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
|
||||
// This file will store the exit code of the command once it is complete.
|
||||
exitCodePath := outputFile.Name() + "-exit"
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if c.canExec() {
|
||||
if c.Config.Pty {
|
||||
cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh")
|
||||
} else {
|
||||
cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh")
|
||||
}
|
||||
} else {
|
||||
cmd = exec.Command("docker", "attach", c.ContainerId)
|
||||
stderr_r, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdin_w, err := cmd.StdinPipe()
|
||||
stdout_r, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
// We have to do some cleanup since run was never called
|
||||
os.Remove(outputFile.Name())
|
||||
os.Remove(exitCodePath)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Run the actual command in a goroutine so that Start doesn't block
|
||||
go c.run(cmd, remote, stdin_w, outputFile, exitCodePath)
|
||||
go c.run(cmd, remote, stdin_w, stdout_r, stderr_r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -179,7 +169,7 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error
|
|||
|
||||
// Make the directory, then copy into it
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/* %s",
|
||||
Command: fmt.Sprintf("set -e; mkdir -p %s; cd %s; command cp -R `ls -A .` %s",
|
||||
containerDst, containerSrc, containerDst),
|
||||
}
|
||||
if err := c.Start(cmd); err != nil {
|
||||
|
@ -237,105 +227,51 @@ func (c *Communicator) DownloadDir(src string, dst string, exclude []string) err
|
|||
return fmt.Errorf("DownloadDir is not implemented for docker")
|
||||
}
|
||||
|
||||
// canExec tells us whether `docker exec` is supported
|
||||
func (c *Communicator) canExec() bool {
|
||||
execConstraint, err := version.NewConstraint(">= 1.4.0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return execConstraint.Check(c.Version)
|
||||
}
|
||||
|
||||
// Runs the given command and blocks until completion
|
||||
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
|
||||
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
|
||||
// For Docker, remote communication must be serialized since it
|
||||
// only supports single execution.
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Clean up after ourselves by removing our temporary files
|
||||
defer os.Remove(outputFile.Name())
|
||||
defer os.Remove(exitCodePath)
|
||||
|
||||
// Tail the output file and send the data to the stdout listener
|
||||
tail, err := tail.TailFile(outputFile.Name(), tail.Config{
|
||||
Poll: true,
|
||||
ReOpen: true,
|
||||
Follow: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error tailing output file: %s", err)
|
||||
remote.SetExited(254)
|
||||
return
|
||||
wg := sync.WaitGroup{}
|
||||
repeat := func(w io.Writer, r io.ReadCloser) {
|
||||
io.Copy(w, r)
|
||||
r.Close()
|
||||
wg.Done()
|
||||
}
|
||||
defer tail.Stop()
|
||||
|
||||
// Modify the remote command so that all the output of the commands
|
||||
// go to a single file and so that the exit code is redirected to
|
||||
// a single file. This lets us determine both when the command
|
||||
// is truly complete (because the file will have data), what the
|
||||
// exit status is (because Docker loses it because of the pty, not
|
||||
// Docker's fault), and get the output (Docker bug).
|
||||
remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
|
||||
remote.Command,
|
||||
filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
|
||||
filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
|
||||
if remote.Stdout != nil {
|
||||
wg.Add(1)
|
||||
go repeat(remote.Stdout, stdout)
|
||||
}
|
||||
|
||||
if remote.Stderr != nil {
|
||||
wg.Add(1)
|
||||
go repeat(remote.Stderr, stderr)
|
||||
}
|
||||
|
||||
// Start the command
|
||||
log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
|
||||
log.Printf("Executing %s:", strings.Join(cmd.Args, " "))
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("Error executing: %s", err)
|
||||
remote.SetExited(254)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin_w.Close()
|
||||
|
||||
// This sleep needs to be here because of the issue linked to below.
|
||||
// Basically, without it, Docker will hang on reading stdin forever,
|
||||
// and won't see what we write, for some reason.
|
||||
//
|
||||
// https://github.com/dotcloud/docker/issues/2628
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
stdin_w.Write([]byte(remoteCmd + "\n"))
|
||||
}()
|
||||
|
||||
// Start a goroutine to read all the lines out of the logs. These channels
|
||||
// allow us to stop the go-routine and wait for it to be stopped.
|
||||
stopTailCh := make(chan struct{})
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tail.Dead():
|
||||
return
|
||||
case line := <-tail.Lines:
|
||||
if remote.Stdout != nil {
|
||||
remote.Stdout.Write([]byte(line.Text + "\n"))
|
||||
} else {
|
||||
log.Printf("Command stdout: %#v", line.Text)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
// If we're done, then return. Otherwise, keep grabbing
|
||||
// data. This gives us a chance to flush all the lines
|
||||
// out of the tailed file.
|
||||
select {
|
||||
case <-stopTailCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var exitRaw []byte
|
||||
var exitStatus int
|
||||
var exitStatusRaw int64
|
||||
err = cmd.Wait()
|
||||
|
||||
if remote.Stdin != nil {
|
||||
go func() {
|
||||
io.Copy(stdin, remote.Stdin)
|
||||
// close stdin to support commands that wait for stdin to be closed before exiting.
|
||||
stdin.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
err := cmd.Wait()
|
||||
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitStatus = 1
|
||||
|
||||
|
@ -344,45 +280,8 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
|
|||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||
exitStatus = status.ExitStatus()
|
||||
}
|
||||
|
||||
// Say that we ended, since if Docker itself failed, then
|
||||
// the command must've not run, or so we assume
|
||||
goto REMOTE_EXIT
|
||||
}
|
||||
|
||||
// Wait for the exit code to appear in our file...
|
||||
log.Println("Waiting for exit code to appear for remote command...")
|
||||
for {
|
||||
fi, err := os.Stat(exitCodePath)
|
||||
if err == nil && fi.Size() > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// Read the exit code
|
||||
exitRaw, err = ioutil.ReadFile(exitCodePath)
|
||||
if err != nil {
|
||||
log.Printf("Error executing: %s", err)
|
||||
exitStatus = 254
|
||||
goto REMOTE_EXIT
|
||||
}
|
||||
|
||||
exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
|
||||
if err != nil {
|
||||
log.Printf("Error executing: %s", err)
|
||||
exitStatus = 254
|
||||
goto REMOTE_EXIT
|
||||
}
|
||||
exitStatus = int(exitStatusRaw)
|
||||
log.Printf("Executed command exit status: %d", exitStatus)
|
||||
|
||||
REMOTE_EXIT:
|
||||
// Wait for the tail to finish
|
||||
close(stopTailCh)
|
||||
<-doneCh
|
||||
|
||||
// Set the exit status which triggers waiters
|
||||
remote.SetExited(exitStatus)
|
||||
}
|
||||
|
|
|
@ -35,11 +35,13 @@ type Config struct {
|
|||
|
||||
// This is used to login to dockerhub to pull a private base container. For
|
||||
// pushing to dockerhub, see the docker post-processors
|
||||
Login bool
|
||||
LoginEmail string `mapstructure:"login_email"`
|
||||
LoginPassword string `mapstructure:"login_password"`
|
||||
LoginServer string `mapstructure:"login_server"`
|
||||
LoginUsername string `mapstructure:"login_username"`
|
||||
Login bool
|
||||
LoginEmail string `mapstructure:"login_email"`
|
||||
LoginPassword string `mapstructure:"login_password"`
|
||||
LoginServer string `mapstructure:"login_server"`
|
||||
LoginUsername string `mapstructure:"login_username"`
|
||||
EcrLogin bool `mapstructure:"ecr_login"`
|
||||
AwsAccessConfig `mapstructure:",squash"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
@ -107,6 +109,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if c.EcrLogin && c.LoginServer == "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("ECR login requires login server to be provided."))
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, nil, errs
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ecr"
|
||||
)
|
||||
|
||||
type AwsAccessConfig struct {
|
||||
AccessKey string `mapstructure:"aws_access_key"`
|
||||
SecretKey string `mapstructure:"aws_secret_key"`
|
||||
Token string `mapstructure:"aws_token"`
|
||||
}
|
||||
|
||||
// Config returns a valid aws.Config object for access to AWS services, or
|
||||
// an error if the authentication and region couldn't be resolved
|
||||
func (c *AwsAccessConfig) config(region string) (*aws.Config, error) {
|
||||
var creds *credentials.Credentials
|
||||
|
||||
config := aws.NewConfig().WithRegion(region).WithMaxRetries(11)
|
||||
sess := session.New(config)
|
||||
creds = credentials.NewChainCredentials([]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: c.AccessKey,
|
||||
SecretAccessKey: c.SecretKey,
|
||||
SessionToken: c.Token,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: ""},
|
||||
&ec2rolecreds.EC2RoleProvider{
|
||||
Client: ec2metadata.New(sess),
|
||||
},
|
||||
})
|
||||
return config.WithCredentials(creds), nil
|
||||
}
|
||||
|
||||
// Get a login token for Amazon AWS ECR. Returns username and password
|
||||
// or an error.
|
||||
func (c *AwsAccessConfig) EcrGetLogin(ecrUrl string) (string, string, error) {
|
||||
|
||||
exp := regexp.MustCompile("(?:http://|https://|)([0-9]*)\\.dkr\\.ecr\\.(.*)\\.amazonaws\\.com.*")
|
||||
splitUrl := exp.FindStringSubmatch(ecrUrl)
|
||||
accountId := splitUrl[1]
|
||||
region := splitUrl[2]
|
||||
|
||||
log.Println(fmt.Sprintf("Getting ECR token for account: %s in %s..", accountId, region))
|
||||
|
||||
awsConfig, err := c.config(region)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
session, err := session.NewSession(awsConfig)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create session: %s", err)
|
||||
}
|
||||
|
||||
service := ecr.New(session)
|
||||
|
||||
params := &ecr.GetAuthorizationTokenInput{
|
||||
RegistryIds: []*string{
|
||||
aws.String(accountId),
|
||||
},
|
||||
}
|
||||
resp, err := service.GetAuthorizationToken(params)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(err.Error())
|
||||
}
|
||||
|
||||
auth, err := base64.StdEncoding.DecodeString(*resp.AuthorizationData[0].AuthorizationToken)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Error decoding ECR AuthorizationToken: %s", err)
|
||||
}
|
||||
|
||||
authParts := strings.SplitN(string(auth), ":", 2)
|
||||
log.Printf("Successfully got login for ECR: %s", ecrUrl)
|
||||
|
||||
return authParts[0], authParts[1], nil
|
||||
}
|
|
@ -21,7 +21,22 @@ func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image))
|
||||
|
||||
if config.Login {
|
||||
if config.EcrLogin {
|
||||
ui.Message("Fetching ECR credentials...")
|
||||
|
||||
username, password, err := config.EcrGetLogin(config.LoginServer)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error fetching ECR credentials: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
config.LoginUsername = username
|
||||
config.LoginPassword = password
|
||||
}
|
||||
|
||||
if config.Login || config.EcrLogin {
|
||||
ui.Message("Logging in...")
|
||||
err := driver.Login(
|
||||
config.LoginServer,
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
// Artifact represents a GCE image as the result of a Packer build.
|
||||
type Artifact struct {
|
||||
image Image
|
||||
image *Image
|
||||
driver Driver
|
||||
config *Config
|
||||
}
|
||||
|
@ -41,16 +41,16 @@ func (a *Artifact) String() string {
|
|||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
switch name {
|
||||
case "ImageName":
|
||||
return a.image.Name
|
||||
case "ImageSizeGb":
|
||||
return a.image.SizeGb
|
||||
case "AccountFilePath":
|
||||
return a.config.AccountFile
|
||||
case "ProjectId":
|
||||
return a.config.ProjectId
|
||||
case "BuildZone":
|
||||
return a.config.Zone
|
||||
case "ImageName":
|
||||
return a.image.Name
|
||||
case "ImageSizeGb":
|
||||
return a.image.SizeGb
|
||||
case "AccountFilePath":
|
||||
return a.config.AccountFile
|
||||
case "ProjectId":
|
||||
return a.config.ProjectId
|
||||
case "BuildZone":
|
||||
return a.config.Zone
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -58,13 +58,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepCreateInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&StepCreateWindowsPassword{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("gce_windows_%s.pem", b.config.PackerBuildName),
|
||||
},
|
||||
&StepInstanceInfo{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
WinRMConfig: winrmConfig,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(StepWaitInstanceStartup),
|
||||
|
@ -73,14 +78,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run the steps.
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
|
@ -93,7 +91,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
image: state.Get("image").(Image),
|
||||
image: state.Get("image").(*Image),
|
||||
driver: driver,
|
||||
config: b.config,
|
||||
}
|
||||
|
|
|
@ -49,10 +49,11 @@ type Config struct {
|
|||
UseInternalIP bool `mapstructure:"use_internal_ip"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
Account AccountFile
|
||||
privateKeyBytes []byte
|
||||
stateTimeout time.Duration
|
||||
ctx interpolate.Context
|
||||
Account AccountFile
|
||||
privateKeyBytes []byte
|
||||
stateTimeout time.Duration
|
||||
imageAlreadyExists bool
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
|
@ -191,4 +192,4 @@ func (c *Config) CalcTimeout() error {
|
|||
}
|
||||
c.stateTimeout = stateTimeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Driver is the interface that has to be implemented to communicate
|
||||
// with GCE. The Driver interface exists mostly to allow a mock implementation
|
||||
// to be used to test the steps.
|
||||
type Driver interface {
|
||||
// ImageExists returns true if the specified image exists. If an error
|
||||
// occurs calling the API, this method returns false.
|
||||
ImageExists(name string) bool
|
||||
|
||||
// CreateImage creates an image from the given disk in Google Compute
|
||||
// Engine.
|
||||
CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error)
|
||||
CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error)
|
||||
|
||||
// DeleteImage deletes the image with the given name.
|
||||
DeleteImage(name string) <-chan error
|
||||
|
@ -21,26 +22,36 @@ type Driver interface {
|
|||
// DeleteDisk deletes the disk with the given name.
|
||||
DeleteDisk(zone, name string) (<-chan error, error)
|
||||
|
||||
// GetImage gets an image; tries the default and public projects.
|
||||
GetImage(name string) (*Image, error)
|
||||
|
||||
// GetImageFromProject gets an image from a specific project.
|
||||
GetImageFromProject(project, name string) (*Image, error)
|
||||
|
||||
// GetInstanceMetadata gets a metadata variable for the instance, name.
|
||||
GetInstanceMetadata(zone, name, key string) (string, error)
|
||||
|
||||
// GetInternalIP gets the GCE-internal IP address for the instance.
|
||||
GetInternalIP(zone, name string) (string, error)
|
||||
|
||||
// GetNatIP gets the NAT IP address for the instance.
|
||||
GetNatIP(zone, name string) (string, error)
|
||||
|
||||
|
||||
// GetSerialPortOutput gets the Serial Port contents for the instance.
|
||||
GetSerialPortOutput(zone, name string) (string, error)
|
||||
|
||||
// ImageExists returns true if the specified image exists. If an error
|
||||
// occurs calling the API, this method returns false.
|
||||
ImageExists(name string) bool
|
||||
|
||||
// RunInstance takes the given config and launches an instance.
|
||||
RunInstance(*InstanceConfig) (<-chan error, error)
|
||||
|
||||
// WaitForInstance waits for an instance to reach the given state.
|
||||
WaitForInstance(state, zone, name string) <-chan error
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Name string
|
||||
ProjectId string
|
||||
SizeGb int64
|
||||
// CreateOrResetWindowsPassword creates or resets the password for a user on an Windows instance.
|
||||
CreateOrResetWindowsPassword(zone, name string, config *WindowsPasswordConfig) (<-chan error, error)
|
||||
}
|
||||
|
||||
type InstanceConfig struct {
|
||||
|
@ -48,7 +59,7 @@ type InstanceConfig struct {
|
|||
Description string
|
||||
DiskSizeGb int64
|
||||
DiskType string
|
||||
Image Image
|
||||
Image *Image
|
||||
MachineType string
|
||||
Metadata map[string]string
|
||||
Name string
|
||||
|
@ -61,3 +72,24 @@ type InstanceConfig struct {
|
|||
Tags []string
|
||||
Zone string
|
||||
}
|
||||
|
||||
// WindowsPasswordConfig is the data structue that GCE needs to encrypt the created
|
||||
// windows password.
|
||||
type WindowsPasswordConfig struct {
|
||||
key *rsa.PrivateKey
|
||||
password string
|
||||
UserName string `json:"userName"`
|
||||
Modulus string `json:"modulus"`
|
||||
Exponent string `json:"exponent"`
|
||||
Email string `json:"email"`
|
||||
ExpireOn time.Time `json:"expireOn"`
|
||||
}
|
||||
|
||||
type windowsPasswordResponse struct {
|
||||
UserName string `json:"userName"`
|
||||
PasswordFound bool `json:"passwordFound"`
|
||||
EncryptedPassword string `json:"encryptedPassword"`
|
||||
Modulus string `json:"modulus"`
|
||||
Exponent string `json:"exponent"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/version"
|
||||
|
||||
|
@ -13,7 +22,6 @@ import (
|
|||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// driverGCE is a Driver implementation that actually talks to GCE.
|
||||
|
@ -88,15 +96,8 @@ func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) ImageExists(name string) bool {
|
||||
_, err := d.service.Images.Get(d.projectId, name).Do()
|
||||
// The API may return an error for reasons other than the image not
|
||||
// existing, but this heuristic is sufficient for now.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
|
||||
image := &compute.Image{
|
||||
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
|
||||
gce_image := &compute.Image{
|
||||
Description: description,
|
||||
Name: name,
|
||||
Family: family,
|
||||
|
@ -104,9 +105,9 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
|
|||
SourceType: "RAW",
|
||||
}
|
||||
|
||||
imageCh := make(chan Image, 1)
|
||||
imageCh := make(chan *Image, 1)
|
||||
errCh := make(chan error, 1)
|
||||
op, err := d.service.Images.Insert(d.projectId, image).Do()
|
||||
op, err := d.service.Images.Insert(d.projectId, gce_image).Do()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
|
@ -114,17 +115,17 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
|
|||
err = waitForState(errCh, "DONE", d.refreshGlobalOp(op))
|
||||
if err != nil {
|
||||
close(imageCh)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
image, err = d.getImage(name, d.projectId)
|
||||
var image *Image
|
||||
image, err = d.GetImageFromProject(d.projectId, name)
|
||||
if err != nil {
|
||||
close(imageCh)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
imageCh <- Image{
|
||||
Name: name,
|
||||
ProjectId: d.projectId,
|
||||
SizeGb: image.DiskSizeGb,
|
||||
}
|
||||
imageCh <- image
|
||||
close(imageCh)
|
||||
}()
|
||||
}
|
||||
|
@ -166,6 +167,57 @@ func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) {
|
|||
return errCh, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) GetImage(name string) (*Image, error) {
|
||||
projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud", "gce-nvme"}
|
||||
var errs error
|
||||
for _, project := range projects {
|
||||
image, err := d.GetImageFromProject(project, name)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
if image != nil {
|
||||
return image, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"Could not find image, %s, in projects, %s: %s", name,
|
||||
projects, errs)
|
||||
}
|
||||
|
||||
func (d *driverGCE) GetImageFromProject(project, name string) (*Image, error) {
|
||||
image, err := d.service.Images.Get(project, name).Do()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if image == nil || image.SelfLink == "" {
|
||||
return nil, fmt.Errorf("Image, %s, could not be found in project: %s", name, project)
|
||||
} else {
|
||||
return &Image{
|
||||
Licenses: image.Licenses,
|
||||
Name: image.Name,
|
||||
ProjectId: project,
|
||||
SelfLink: image.SelfLink,
|
||||
SizeGb: image.DiskSizeGb,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverGCE) GetInstanceMetadata(zone, name, key string) (string, error) {
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, item := range instance.Metadata.Items {
|
||||
if item.Key == key {
|
||||
return *item.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Instance metadata key, %s, not found.", key)
|
||||
}
|
||||
|
||||
func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
if err != nil {
|
||||
|
@ -207,10 +259,17 @@ func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
return output.Contents, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) ImageExists(name string) bool {
|
||||
_, err := d.GetImageFromProject(d.projectId, name)
|
||||
// The API may return an error for reasons other than the image not
|
||||
// existing, but this heuristic is sufficient for now.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||
// Get the zone
|
||||
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
|
||||
|
@ -219,13 +278,6 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Get the image
|
||||
d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId))
|
||||
image, err := d.getImage(c.Image.Name, c.Image.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the machine type
|
||||
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
|
||||
machineType, err := d.service.MachineTypes.Get(
|
||||
|
@ -302,7 +354,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
|||
Boot: true,
|
||||
AutoDelete: false,
|
||||
InitializeParams: &compute.AttachedDiskInitializeParams{
|
||||
SourceImage: image.SelfLink,
|
||||
SourceImage: c.Image.SelfLink,
|
||||
DiskSizeGb: c.DiskSizeGb,
|
||||
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, c.DiskType),
|
||||
},
|
||||
|
@ -349,26 +401,118 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
|||
return errCh, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) CreateOrResetWindowsPassword(instance, zone string, c *WindowsPasswordConfig) (<-chan error, error) {
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go d.createWindowsPassword(errCh, instance, zone, c)
|
||||
|
||||
return errCh, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) createWindowsPassword(errCh chan<- error, name, zone string, c *WindowsPasswordConfig) {
|
||||
|
||||
data, err := json.Marshal(c)
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
dCopy := string(data)
|
||||
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{Key: "windows-keys", Value: &dCopy})
|
||||
|
||||
op, err := d.service.Instances.SetMetadata(d.projectId, zone, name, &compute.Metadata{
|
||||
Fingerprint: instance.Metadata.Fingerprint,
|
||||
Items: instance.Metadata.Items,
|
||||
}).Do()
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
newErrCh := make(chan error, 1)
|
||||
go waitForState(newErrCh, "DONE", d.refreshZoneOp(zone, op))
|
||||
|
||||
select {
|
||||
case err = <-newErrCh:
|
||||
case <-time.After(time.Second * 30):
|
||||
err = errors.New("time out while waiting for instance to create")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
timeout := time.Now().Add(time.Minute * 3)
|
||||
hash := sha1.New()
|
||||
random := rand.Reader
|
||||
|
||||
for time.Now().Before(timeout) {
|
||||
if passwordResponses, err := d.getPasswordResponses(zone, name); err == nil {
|
||||
for _, response := range passwordResponses {
|
||||
if response.Modulus == c.Modulus {
|
||||
|
||||
decodedPassword, err := base64.StdEncoding.DecodeString(response.EncryptedPassword)
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
password, err := rsa.DecryptOAEP(hash, random, c.key, decodedPassword, nil)
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
c.password = string(password)
|
||||
errCh <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
err = errors.New("Could not retrieve password. Timed out.")
|
||||
|
||||
errCh <- err
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (d *driverGCE) getPasswordResponses(zone, instance string) ([]windowsPasswordResponse, error) {
|
||||
output, err := d.service.Instances.GetSerialPortOutput(d.projectId, zone, instance).Port(4).Do()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := strings.Split(output.Contents, "\n")
|
||||
|
||||
passwordResponses := make([]windowsPasswordResponse, 0, len(responses))
|
||||
|
||||
for _, response := range responses {
|
||||
var passwordResponse windowsPasswordResponse
|
||||
if err := json.Unmarshal([]byte(response), &passwordResponse); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
passwordResponses = append(passwordResponses, passwordResponse)
|
||||
}
|
||||
|
||||
return passwordResponses, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
|
||||
errCh := make(chan error, 1)
|
||||
go waitForState(errCh, state, d.refreshInstanceState(zone, name))
|
||||
return errCh
|
||||
}
|
||||
|
||||
func (d *driverGCE) getImage(name, projectId string) (image *compute.Image, err error) {
|
||||
projects := []string{projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
|
||||
for _, project := range projects {
|
||||
image, err = d.service.Images.Get(project, name).Do()
|
||||
if err == nil && image != nil && image.SelfLink != "" {
|
||||
return
|
||||
}
|
||||
image = nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf("Image %s could not be found in any of these projects: %s", name, projects)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
|
||||
return func() (string, error) {
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
|
@ -428,7 +572,7 @@ type stateRefreshFunc func() (string, error)
|
|||
// waitForState will spin in a loop forever waiting for state to
|
||||
// reach a certain target.
|
||||
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error {
|
||||
err := Retry(2, 2, 0, func() (bool, error) {
|
||||
err := common.Retry(2, 2, 0, func() (bool, error) {
|
||||
state, err := refresh()
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
package googlecompute
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DriverMock is a Driver implementation that is a mocked out so that
|
||||
// it can be used for tests.
|
||||
type DriverMock struct {
|
||||
ImageExistsName string
|
||||
ImageExistsResult bool
|
||||
|
||||
CreateImageName string
|
||||
CreateImageDesc string
|
||||
CreateImageFamily string
|
||||
CreateImageZone string
|
||||
CreateImageDisk string
|
||||
CreateImageProjectId string
|
||||
CreateImageSizeGb int64
|
||||
CreateImageErrCh <-chan error
|
||||
CreateImageResultCh <-chan Image
|
||||
CreateImageName string
|
||||
CreateImageDesc string
|
||||
CreateImageFamily string
|
||||
CreateImageZone string
|
||||
CreateImageDisk string
|
||||
CreateImageResultLicenses []string
|
||||
CreateImageResultProjectId string
|
||||
CreateImageResultSelfLink string
|
||||
CreateImageResultSizeGb int64
|
||||
CreateImageErrCh <-chan error
|
||||
CreateImageResultCh <-chan *Image
|
||||
|
||||
DeleteImageName string
|
||||
DeleteImageErrCh <-chan error
|
||||
|
@ -29,6 +30,21 @@ type DriverMock struct {
|
|||
DeleteDiskErrCh <-chan error
|
||||
DeleteDiskErr error
|
||||
|
||||
GetImageName string
|
||||
GetImageResult *Image
|
||||
GetImageErr error
|
||||
|
||||
GetImageFromProjectProject string
|
||||
GetImageFromProjectName string
|
||||
GetImageFromProjectResult *Image
|
||||
GetImageFromProjectErr error
|
||||
|
||||
GetInstanceMetadataZone string
|
||||
GetInstanceMetadataName string
|
||||
GetInstanceMetadataKey string
|
||||
GetInstanceMetadataResult string
|
||||
GetInstanceMetadataErr error
|
||||
|
||||
GetNatIPZone string
|
||||
GetNatIPName string
|
||||
GetNatIPResult string
|
||||
|
@ -38,47 +54,58 @@ type DriverMock struct {
|
|||
GetInternalIPName string
|
||||
GetInternalIPResult string
|
||||
GetInternalIPErr error
|
||||
|
||||
|
||||
GetSerialPortOutputZone string
|
||||
GetSerialPortOutputName string
|
||||
GetSerialPortOutputResult string
|
||||
GetSerialPortOutputErr error
|
||||
|
||||
ImageExistsName string
|
||||
ImageExistsResult bool
|
||||
|
||||
RunInstanceConfig *InstanceConfig
|
||||
RunInstanceErrCh <-chan error
|
||||
RunInstanceErr error
|
||||
|
||||
CreateOrResetWindowsPasswordZone string
|
||||
CreateOrResetWindowsPasswordInstance string
|
||||
CreateOrResetWindowsPasswordConfig *WindowsPasswordConfig
|
||||
CreateOrResetWindowsPasswordErr error
|
||||
CreateOrResetWindowsPasswordErrCh <-chan error
|
||||
|
||||
WaitForInstanceState string
|
||||
WaitForInstanceZone string
|
||||
WaitForInstanceName string
|
||||
WaitForInstanceErrCh <-chan error
|
||||
}
|
||||
|
||||
func (d *DriverMock) ImageExists(name string) bool {
|
||||
d.ImageExistsName = name
|
||||
return d.ImageExistsResult
|
||||
}
|
||||
|
||||
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
|
||||
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
|
||||
d.CreateImageName = name
|
||||
d.CreateImageDesc = description
|
||||
d.CreateImageFamily = family
|
||||
d.CreateImageZone = zone
|
||||
d.CreateImageDisk = disk
|
||||
if d.CreateImageSizeGb == 0 {
|
||||
d.CreateImageSizeGb = 10
|
||||
if d.CreateImageResultProjectId == "" {
|
||||
d.CreateImageResultProjectId = "test"
|
||||
}
|
||||
if d.CreateImageProjectId == "" {
|
||||
d.CreateImageProjectId = "test"
|
||||
if d.CreateImageResultSelfLink == "" {
|
||||
d.CreateImageResultSelfLink = fmt.Sprintf(
|
||||
"http://content.googleapis.com/compute/v1/%s/global/licenses/test",
|
||||
d.CreateImageResultProjectId)
|
||||
}
|
||||
if d.CreateImageResultSizeGb == 0 {
|
||||
d.CreateImageResultSizeGb = 10
|
||||
}
|
||||
|
||||
resultCh := d.CreateImageResultCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan Image, 1)
|
||||
ch <- Image{
|
||||
ch := make(chan *Image, 1)
|
||||
ch <- &Image{
|
||||
Licenses: d.CreateImageResultLicenses,
|
||||
Name: name,
|
||||
ProjectId: d.CreateImageProjectId,
|
||||
SizeGb: d.CreateImageSizeGb,
|
||||
ProjectId: d.CreateImageResultProjectId,
|
||||
SelfLink: d.CreateImageResultSelfLink,
|
||||
SizeGb: d.CreateImageResultSizeGb,
|
||||
}
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
|
@ -135,6 +162,24 @@ func (d *DriverMock) DeleteDisk(zone, name string) (<-chan error, error) {
|
|||
return resultCh, d.DeleteDiskErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetImage(name string) (*Image, error) {
|
||||
d.GetImageName = name
|
||||
return d.GetImageResult, d.GetImageErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetImageFromProject(project, name string) (*Image, error) {
|
||||
d.GetImageFromProjectProject = project
|
||||
d.GetImageFromProjectName = name
|
||||
return d.GetImageFromProjectResult, d.GetImageFromProjectErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetInstanceMetadata(zone, name, key string) (string, error) {
|
||||
d.GetInstanceMetadataZone = zone
|
||||
d.GetInstanceMetadataName = name
|
||||
d.GetInstanceMetadataKey = key
|
||||
return d.GetInstanceMetadataResult, d.GetInstanceMetadataErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
|
||||
d.GetNatIPZone = zone
|
||||
d.GetNatIPName = name
|
||||
|
@ -153,6 +198,11 @@ func (d *DriverMock) GetSerialPortOutput(zone, name string) (string, error) {
|
|||
return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) ImageExists(name string) bool {
|
||||
d.ImageExistsName = name
|
||||
return d.ImageExistsResult
|
||||
}
|
||||
|
||||
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||
d.RunInstanceConfig = c
|
||||
|
||||
|
@ -180,3 +230,25 @@ func (d *DriverMock) WaitForInstance(state, zone, name string) <-chan error {
|
|||
|
||||
return resultCh
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetWindowsPassword() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) CreateOrResetWindowsPassword(instance, zone string, c *WindowsPasswordConfig) (<-chan error, error) {
|
||||
|
||||
d.CreateOrResetWindowsPasswordInstance = instance
|
||||
d.CreateOrResetWindowsPasswordZone = zone
|
||||
d.CreateOrResetWindowsPasswordConfig = c
|
||||
|
||||
c.password = "MOCK_PASSWORD"
|
||||
|
||||
resultCh := d.CreateOrResetWindowsPasswordErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh, d.CreateOrResetWindowsPasswordErr
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Licenses []string
|
||||
Name string
|
||||
ProjectId string
|
||||
SelfLink string
|
||||
SizeGb int64
|
||||
}
|
||||
|
||||
func (i *Image) IsWindows() bool {
|
||||
for _, license := range i.Licenses {
|
||||
if strings.Contains(license, "windows") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func StubImage(name, project string, licenses []string, sizeGb int64) *Image {
|
||||
return &Image{
|
||||
Licenses: licenses,
|
||||
Name: name,
|
||||
ProjectId: project,
|
||||
SelfLink: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", project, name),
|
||||
SizeGb: sizeGb,
|
||||
}
|
||||
}
|
||||
|
||||
func TestImage_IsWindows(t *testing.T) {
|
||||
i := StubImage("foo", "foo-project", []string{"license-foo", "license-bar"}, 100)
|
||||
assert.False(t, i.IsWindows())
|
||||
|
||||
i = StubImage("foo", "foo-project", []string{"license-foo", "windows-license"}, 100)
|
||||
assert.True(t, i.IsWindows())
|
||||
}
|
|
@ -1,37 +1,40 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const StartupScriptStartLog string = "Packer startup script starting."
|
||||
const StartupScriptDoneLog string = "Packer startup script done."
|
||||
const StartupScriptKey string = "startup-script"
|
||||
const StartupScriptStatusKey string = "startup-script-status"
|
||||
const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
|
||||
|
||||
// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate
|
||||
// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it
|
||||
// will be printed early in the instance console log (before the startup script even runs;
|
||||
// we print out instance creation metadata which contains this wrapper script).
|
||||
var StartupScriptDoneLogBase64 string = base64.StdEncoding.EncodeToString([]byte(StartupScriptDoneLog))
|
||||
const StartupScriptStatusDone string = "done"
|
||||
const StartupScriptStatusError string = "error"
|
||||
const StartupScriptStatusNotDone string = "notdone"
|
||||
|
||||
var StartupScript string = fmt.Sprintf(`#!/bin/bash
|
||||
echo %s
|
||||
var StartupScriptLinux string = fmt.Sprintf(`#!/bin/bash
|
||||
echo "Packer startup script starting."
|
||||
RETVAL=0
|
||||
BASEMETADATAURL=http://metadata/computeMetadata/v1/instance/
|
||||
|
||||
GetMetadata () {
|
||||
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)"
|
||||
echo "$(curl -f -H "Metadata-Flavor: Google" ${BASEMETADATAURL}/${1} 2> /dev/null)"
|
||||
}
|
||||
|
||||
STARTUPSCRIPT=$(GetMetadata %s)
|
||||
ZONE=$(GetMetadata zone | grep -oP "[^/]*$")
|
||||
|
||||
SetMetadata () {
|
||||
gcloud compute instances add-metadata ${HOSTNAME} --metadata ${1}=${2} --zone ${ZONE}
|
||||
}
|
||||
|
||||
STARTUPSCRIPT=$(GetMetadata attributes/%s)
|
||||
STARTUPSCRIPTPATH=/packer-wrapped-startup-script
|
||||
if [ -f "/var/log/startupscript.log" ]; then
|
||||
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
|
||||
else
|
||||
STARTUPSCRIPTLOGPATH=/var/log/daemon.log
|
||||
fi
|
||||
STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest)
|
||||
STARTUPSCRIPTLOGDEST=$(GetMetadata attributes/startup-script-log-dest)
|
||||
|
||||
if [[ ! -z $STARTUPSCRIPT ]]; then
|
||||
echo "Executing user-provided startup script..."
|
||||
|
@ -48,6 +51,9 @@ if [[ ! -z $STARTUPSCRIPT ]]; then
|
|||
rm ${STARTUPSCRIPTPATH}
|
||||
fi
|
||||
|
||||
echo $(echo %s | base64 --decode)
|
||||
echo "Packer startup script done."
|
||||
SetMetadata %s %s
|
||||
exit $RETVAL
|
||||
`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64)
|
||||
`, StartupWrappedScriptKey, StartupScriptStatusKey, StartupScriptStatusDone)
|
||||
|
||||
var StartupScriptWindows string = ""
|
||||
|
|
|
@ -13,19 +13,19 @@ type StepCheckExistingImage int
|
|||
|
||||
// Run executes the Packer build step that checks if the image already exists.
|
||||
func (s *StepCheckExistingImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Checking image does not exist...")
|
||||
exists := driver.ImageExists(config.ImageName)
|
||||
if exists {
|
||||
err := fmt.Errorf("Image %s already exists", config.ImageName)
|
||||
c.imageAlreadyExists = d.ImageExists(c.ImageName)
|
||||
if !c.PackerForce && c.imageAlreadyExists {
|
||||
err := fmt.Errorf("Image %s already exists.\n"+
|
||||
"Use the force flag to delete it prior to building.", c.ImageName)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
|
|
@ -22,9 +22,24 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
|||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if config.PackerForce && config.imageAlreadyExists {
|
||||
ui.Say("Deleting previous image...")
|
||||
|
||||
errCh := driver.DeleteImage(config.ImageName)
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error deleting image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("Creating image...")
|
||||
|
||||
imageCh, errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone, config.DiskName)
|
||||
imageCh, errCh := driver.CreateImage(
|
||||
config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone,
|
||||
config.DiskName)
|
||||
var err error
|
||||
select {
|
||||
case err = <-errCh:
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStepCreateImage_impl(t *testing.T) {
|
||||
|
@ -16,52 +17,35 @@ func TestStepCreateImage(t *testing.T) {
|
|||
step := new(StepCreateImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.CreateImageProjectId = "createimage-project"
|
||||
driver.CreateImageSizeGb = 100
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
|
||||
// These are the values of the image the driver will return.
|
||||
d.CreateImageResultLicenses = []string{"test-license"}
|
||||
d.CreateImageResultProjectId = "test-project"
|
||||
d.CreateImageResultSizeGb = 100
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
action := step.Run(state)
|
||||
assert.Equal(t, action, multistep.ActionContinue, "Step did not pass.")
|
||||
|
||||
uncastImage, ok := state.GetOk("image")
|
||||
if !ok {
|
||||
t.Fatal("should have image")
|
||||
}
|
||||
image, ok := uncastImage.(Image)
|
||||
if !ok {
|
||||
t.Fatal("image is not an Image")
|
||||
}
|
||||
|
||||
assert.True(t, ok, "State does not have resulting image.")
|
||||
image, ok := uncastImage.(*Image)
|
||||
assert.True(t, ok, "Image in state is not an Image.")
|
||||
|
||||
// Verify created Image results.
|
||||
if image.Name != config.ImageName {
|
||||
t.Fatalf("Created image name, %s, does not match config name, %s.", image.Name, config.ImageName)
|
||||
}
|
||||
if driver.CreateImageProjectId != image.ProjectId {
|
||||
t.Fatalf("Created image project ID, %s, does not match driver project ID, %s.", image.ProjectId, driver.CreateImageProjectId)
|
||||
}
|
||||
if driver.CreateImageSizeGb != image.SizeGb {
|
||||
t.Fatalf("Created image size, %d, does not match the expected test value, %d.", image.SizeGb, driver.CreateImageSizeGb)
|
||||
}
|
||||
assert.Equal(t, image.Licenses, d.CreateImageResultLicenses, "Created image licenses don't match the licenses returned by the driver.")
|
||||
assert.Equal(t, image.Name, c.ImageName, "Created image does not match config name.")
|
||||
assert.Equal(t, image.ProjectId, d.CreateImageResultProjectId, "Created image project does not match driver project.")
|
||||
assert.Equal(t, image.SizeGb, d.CreateImageResultSizeGb, "Created image size does not match the size returned by the driver.")
|
||||
|
||||
// Verify proper args passed to driver.CreateImage.
|
||||
if driver.CreateImageName != config.ImageName {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageName)
|
||||
}
|
||||
if driver.CreateImageDesc != config.ImageDescription {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageDesc)
|
||||
}
|
||||
if driver.CreateImageFamily != config.ImageFamily {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageFamily)
|
||||
}
|
||||
if driver.CreateImageZone != config.Zone {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageZone)
|
||||
}
|
||||
if driver.CreateImageDisk != config.DiskName {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageDisk)
|
||||
}
|
||||
assert.Equal(t, d.CreateImageName, c.ImageName, "Incorrect image name passed to driver.")
|
||||
assert.Equal(t, d.CreateImageDesc, c.ImageDescription, "Incorrect image description passed to driver.")
|
||||
assert.Equal(t, d.CreateImageFamily, c.ImageFamily, "Incorrect image family passed to driver.")
|
||||
assert.Equal(t, d.CreateImageZone, c.Zone, "Incorrect image zone passed to driver.")
|
||||
assert.Equal(t, d.CreateImageDisk, c.DiskName, "Incorrect disk passed to driver.")
|
||||
}
|
||||
|
||||
func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
||||
|
@ -76,14 +60,10 @@ func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
|||
driver.CreateImageErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("image_name"); ok {
|
||||
t.Fatal("should NOT have image")
|
||||
}
|
||||
action := step.Run(state)
|
||||
assert.Equal(t, action, multistep.ActionHalt, "Step should not have passed.")
|
||||
_, ok := state.GetOk("error")
|
||||
assert.True(t, ok, "State should have an error.")
|
||||
_, ok = state.GetOk("image_name")
|
||||
assert.False(t, ok, "State should not have a resulting image.")
|
||||
}
|
||||
|
|
|
@ -15,82 +15,101 @@ type StepCreateInstance struct {
|
|||
Debug bool
|
||||
}
|
||||
|
||||
func (config *Config) getImage() Image {
|
||||
project := config.ProjectId
|
||||
if config.SourceImageProjectId != "" {
|
||||
project = config.SourceImageProjectId
|
||||
}
|
||||
return Image{Name: config.SourceImage, ProjectId: project}
|
||||
}
|
||||
|
||||
func (config *Config) getInstanceMetadata(sshPublicKey string) (map[string]string, error) {
|
||||
func (c *Config) createInstanceMetadata(sourceImage *Image, sshPublicKey string) (map[string]string, error) {
|
||||
instanceMetadata := make(map[string]string)
|
||||
var err error
|
||||
|
||||
// Copy metadata from config.
|
||||
for k, v := range config.Metadata {
|
||||
for k, v := range c.Metadata {
|
||||
instanceMetadata[k] = v
|
||||
}
|
||||
|
||||
// Merge any existing ssh keys with our public key.
|
||||
sshMetaKey := "sshKeys"
|
||||
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
|
||||
sshKeys := fmt.Sprintf("%s:%s", c.Comm.SSHUsername, sshPublicKey)
|
||||
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
|
||||
}
|
||||
instanceMetadata[sshMetaKey] = sshKeys
|
||||
|
||||
|
||||
// Wrap any startup script with our own startup script.
|
||||
if config.StartupScriptFile != "" {
|
||||
if c.StartupScriptFile != "" {
|
||||
var content []byte
|
||||
content, err = ioutil.ReadFile(config.StartupScriptFile)
|
||||
content, err = ioutil.ReadFile(c.StartupScriptFile)
|
||||
instanceMetadata[StartupWrappedScriptKey] = string(content)
|
||||
} else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists {
|
||||
instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript
|
||||
}
|
||||
instanceMetadata[StartupScriptKey] = StartupScript
|
||||
if sourceImage.IsWindows() {
|
||||
// Windows startup script support is not yet implemented.
|
||||
// Mark the startup script as done.
|
||||
instanceMetadata[StartupScriptKey] = StartupScriptWindows
|
||||
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusDone
|
||||
} else {
|
||||
instanceMetadata[StartupScriptKey] = StartupScriptLinux
|
||||
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusNotDone
|
||||
}
|
||||
|
||||
return instanceMetadata, err
|
||||
}
|
||||
|
||||
func getImage(c *Config, d Driver) (*Image, error) {
|
||||
if c.SourceImageProjectId == "" {
|
||||
return d.GetImage(c.SourceImage)
|
||||
} else {
|
||||
return d.GetImageFromProject(c.SourceImageProjectId, c.SourceImage)
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the Packer build step that creates a GCE instance.
|
||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(Driver)
|
||||
sshPublicKey := state.Get("ssh_public_key").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
sourceImage, err := getImage(c, d)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting source image for instance creation: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if sourceImage.IsWindows() && c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" {
|
||||
state.Put("create_windows_password", true)
|
||||
}
|
||||
|
||||
ui.Say("Creating instance...")
|
||||
name := config.InstanceName
|
||||
name := c.InstanceName
|
||||
|
||||
var errCh <-chan error
|
||||
var err error
|
||||
var metadata map[string]string
|
||||
metadata, err = config.getInstanceMetadata(sshPublicKey)
|
||||
errCh, err = driver.RunInstance(&InstanceConfig{
|
||||
Address: config.Address,
|
||||
metadata, err = c.createInstanceMetadata(sourceImage, sshPublicKey)
|
||||
errCh, err = d.RunInstance(&InstanceConfig{
|
||||
Address: c.Address,
|
||||
Description: "New instance created by Packer",
|
||||
DiskSizeGb: config.DiskSizeGb,
|
||||
DiskType: config.DiskType,
|
||||
Image: config.getImage(),
|
||||
MachineType: config.MachineType,
|
||||
DiskSizeGb: c.DiskSizeGb,
|
||||
DiskType: c.DiskType,
|
||||
Image: sourceImage,
|
||||
MachineType: c.MachineType,
|
||||
Metadata: metadata,
|
||||
Name: name,
|
||||
Network: config.Network,
|
||||
OmitExternalIP: config.OmitExternalIP,
|
||||
Preemptible: config.Preemptible,
|
||||
Region: config.Region,
|
||||
ServiceAccountEmail: config.Account.ClientEmail,
|
||||
Subnetwork: config.Subnetwork,
|
||||
Tags: config.Tags,
|
||||
Zone: config.Zone,
|
||||
Network: c.Network,
|
||||
OmitExternalIP: c.OmitExternalIP,
|
||||
Preemptible: c.Preemptible,
|
||||
Region: c.Region,
|
||||
ServiceAccountEmail: c.Account.ClientEmail,
|
||||
Subnetwork: c.Subnetwork,
|
||||
Tags: c.Tags,
|
||||
Zone: c.Zone,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
ui.Message("Waiting for creation operation to complete...")
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(config.stateTimeout):
|
||||
case <-time.After(c.stateTimeout):
|
||||
err = errors.New("time out while waiting for instance to create")
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +125,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
|||
|
||||
if s.Debug {
|
||||
if name != "" {
|
||||
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, config.Zone))
|
||||
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, c.Zone))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ package googlecompute
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStepCreateInstance_impl(t *testing.T) {
|
||||
|
@ -18,8 +20,86 @@ func TestStepCreateInstance(t *testing.T) {
|
|||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||
|
||||
// run the step
|
||||
assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
|
||||
|
||||
// Verify state
|
||||
nameRaw, ok := state.GetOk("instance_name")
|
||||
assert.True(t, ok, "State should have an instance name.")
|
||||
|
||||
// cleanup
|
||||
step.Cleanup(state)
|
||||
|
||||
// Check args passed to the driver.
|
||||
assert.Equal(t, d.DeleteInstanceName, nameRaw.(string), "Incorrect instance name passed to driver.")
|
||||
assert.Equal(t, d.DeleteInstanceZone, c.Zone, "Incorrect instance zone passed to driver.")
|
||||
assert.Equal(t, d.DeleteDiskName, c.InstanceName, "Incorrect disk name passed to driver.")
|
||||
assert.Equal(t, d.DeleteDiskZone, c.Zone, "Incorrect disk zone passed to driver.")
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_windowsNeedsPassword(t *testing.T) {
|
||||
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
d.GetImageResult = StubImage("test-image", "test-project", []string{"windows"}, 100)
|
||||
c.Comm.Type = "winrm"
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
nameRaw, ok := state.GetOk("instance_name")
|
||||
if !ok {
|
||||
t.Fatal("should have instance name")
|
||||
}
|
||||
|
||||
createPassword, ok := state.GetOk("create_windows_password")
|
||||
|
||||
if !ok || !createPassword.(bool) {
|
||||
t.Fatal("should need to create a windows password")
|
||||
}
|
||||
|
||||
// cleanup
|
||||
step.Cleanup(state)
|
||||
|
||||
if d.DeleteInstanceName != nameRaw.(string) {
|
||||
t.Fatal("should've deleted instance")
|
||||
}
|
||||
if d.DeleteInstanceZone != c.Zone {
|
||||
t.Fatalf("bad instance zone: %#v", d.DeleteInstanceZone)
|
||||
}
|
||||
|
||||
if d.DeleteDiskName != c.InstanceName {
|
||||
t.Fatal("should've deleted disk")
|
||||
}
|
||||
if d.DeleteDiskZone != c.Zone {
|
||||
t.Fatalf("bad disk zone: %#v", d.DeleteDiskZone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_windowsPasswordSet(t *testing.T) {
|
||||
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.GetImageResult = StubImage("test-image", "test-project", []string{"windows"}, 100)
|
||||
config.Comm.Type = "winrm"
|
||||
config.Comm.WinRMPassword = "password"
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
|
@ -32,6 +112,12 @@ func TestStepCreateInstance(t *testing.T) {
|
|||
t.Fatal("should have instance name")
|
||||
}
|
||||
|
||||
_, ok = state.GetOk("create_windows_password")
|
||||
|
||||
if ok {
|
||||
t.Fatal("should not need to create windows password")
|
||||
}
|
||||
|
||||
// cleanup
|
||||
step.Cleanup(state)
|
||||
|
||||
|
@ -57,21 +143,18 @@ func TestStepCreateInstance_error(t *testing.T) {
|
|||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErr = errors.New("error")
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
d.RunInstanceErr = errors.New("error")
|
||||
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
_, ok := state.GetOk("error")
|
||||
assert.True(t, ok, "State should have an error.")
|
||||
_, ok = state.GetOk("instance_name")
|
||||
assert.False(t, ok, "State should not have an instance name.")
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
||||
|
@ -79,26 +162,23 @@ func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
|||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
errCh <- errors.New("error")
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErrCh = errCh
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
d.RunInstanceErrCh = errCh
|
||||
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
_, ok := state.GetOk("error")
|
||||
assert.True(t, ok, "State should have an error.")
|
||||
_, ok = state.GetOk("instance_name")
|
||||
assert.False(t, ok, "State should not have an instance name.")
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
||||
|
@ -106,30 +186,27 @@ func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
|||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
<-time.After(10 * time.Millisecond)
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.stateTimeout = 1 * time.Microsecond
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErrCh = errCh
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
d.RunInstanceErrCh = errCh
|
||||
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
_, ok := state.GetOk("error")
|
||||
assert.True(t, ok, "State should have an error.")
|
||||
_, ok = state.GetOk("instance_name")
|
||||
assert.False(t, ok, "State should not have an instance name.")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCreateWindowsPassword represents a Packer build step that sets the windows password on a Windows GCE instance.
|
||||
type StepCreateWindowsPassword struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
}
|
||||
|
||||
// Run executes the Packer build step that sets the windows password on a Windows GCE instance.
|
||||
func (s *StepCreateWindowsPassword) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
d := state.Get("driver").(Driver)
|
||||
c := state.Get("config").(*Config)
|
||||
name := state.Get("instance_name").(string)
|
||||
|
||||
if c.Comm.WinRMPassword != "" {
|
||||
state.Put("winrm_password", c.Comm.WinRMPassword)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
create, ok := state.GetOk("create_windows_password")
|
||||
|
||||
if !ok || !create.(bool) {
|
||||
return multistep.ActionContinue
|
||||
|
||||
}
|
||||
ui.Say("Creating windows user for instance...")
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary key: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
buf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(buf, uint32(priv.E))
|
||||
|
||||
data := WindowsPasswordConfig{
|
||||
key: priv,
|
||||
UserName: c.Comm.WinRMUser,
|
||||
Modulus: base64.StdEncoding.EncodeToString(priv.N.Bytes()),
|
||||
Exponent: base64.StdEncoding.EncodeToString(buf[1:]),
|
||||
Email: c.Account.ClientEmail,
|
||||
ExpireOn: time.Now().Add(time.Minute * 5),
|
||||
}
|
||||
|
||||
if s.Debug {
|
||||
|
||||
priv_blk := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.Create(s.DebugKeyPath)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Write out the key
|
||||
err = pem.Encode(f, &priv_blk)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
errCh, err := d.CreateOrResetWindowsPassword(name, c.Zone, &data)
|
||||
|
||||
if err == nil {
|
||||
ui.Message("Waiting for windows password to complete...")
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(c.stateTimeout):
|
||||
err = errors.New("time out while waiting for the password to be created")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating windows password: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Message("Created password.")
|
||||
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Password (since debug is enabled): %s", data.password))
|
||||
}
|
||||
|
||||
state.Put("winrm_password", data.password)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Nothing to clean up. The windows password is only created on the single instance.
|
||||
func (s *StepCreateWindowsPassword) Cleanup(state multistep.StateBag) {}
|
|
@ -0,0 +1,162 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword(t *testing.T) {
|
||||
state := testState(t)
|
||||
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
state.Put("create_windows_password", true)
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "MOCK_PASSWORD" {
|
||||
t.Fatal("should have a password", password, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword_passwordSet(t *testing.T) {
|
||||
state := testState(t)
|
||||
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
|
||||
c := state.Get("config").(*Config)
|
||||
|
||||
c.Comm.WinRMPassword = "password"
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "password" {
|
||||
t.Fatal("should have used existing password", password, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword_dontNeedPassword(t *testing.T) {
|
||||
state := testState(t)
|
||||
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword_debug(t *testing.T) {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
tf.Close()
|
||||
|
||||
state := testState(t)
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
state.Put("create_windows_password", true)
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
|
||||
step.Debug = true
|
||||
step.DebugKeyPath = tf.Name()
|
||||
|
||||
defer step.Cleanup(state)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "MOCK_PASSWORD" {
|
||||
t.Fatal("should have a password", password, ok)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(tf.Name()); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword_error(t *testing.T) {
|
||||
state := testState(t)
|
||||
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
state.Put("create_windows_password", true)
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.CreateOrResetWindowsPasswordErr = errors.New("error")
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("winrm_password"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateOrResetWindowsPassword_errorOnChannel(t *testing.T) {
|
||||
state := testState(t)
|
||||
|
||||
// Step is run after the instance is created so we will have an instance name set
|
||||
state.Put("instance_name", "mock_instance")
|
||||
state.Put("create_windows_password", true)
|
||||
|
||||
step := new(StepCreateWindowsPassword)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
errCh <- errors.New("error")
|
||||
|
||||
driver.CreateOrResetWindowsPasswordErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("winrm_password"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
}
|
|
@ -71,8 +71,8 @@ func (s *StepTeardownInstance) Cleanup(state multistep.StateBag) {
|
|||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error deleting disk. Please delete it manually.\n\n"+
|
||||
"DiskName: %s\n" +
|
||||
"Zone: %s\n" +
|
||||
"DiskName: %s\n"+
|
||||
"Zone: %s\n"+
|
||||
"Error: %s", config.DiskName, config.Zone, err))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ package googlecompute
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testState(t *testing.T) multistep.StateBag {
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
package googlecompute
|
||||
|
||||
import(
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepWaitInstanceStartup int
|
||||
|
||||
// Run reads the instance serial port output and looks for the log entry indicating the startup script finished.
|
||||
// Run reads the instance metadata and looks for the log entry
|
||||
// indicating the startup script finished.
|
||||
func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
instanceName := state.Get("instance_name").(string)
|
||||
|
||||
|
||||
ui.Say("Waiting for any running startup script to finish...")
|
||||
|
||||
// Keep checking the serial port output to see if the startup script is done.
|
||||
err := Retry(10, 60, 0, func() (bool, error) {
|
||||
output, err := driver.GetSerialPortOutput(config.Zone, instanceName)
|
||||
err := common.Retry(10, 60, 0, func() (bool, error) {
|
||||
status, err := driver.GetInstanceMetadata(config.Zone,
|
||||
instanceName, StartupScriptStatusKey)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting serial port output: %s", err)
|
||||
err := fmt.Errorf("Error getting startup script status: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
done := strings.Contains(output, StartupScriptDoneLog)
|
||||
|
||||
if status == StartupScriptStatusError {
|
||||
err = errors.New("Startup script error.")
|
||||
return false, err
|
||||
}
|
||||
|
||||
done := status == StartupScriptStatusDone
|
||||
if !done {
|
||||
ui.Say("Startup script not finished yet. Waiting...")
|
||||
}
|
||||
|
|
|
@ -2,37 +2,29 @@ package googlecompute
|
|||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepWaitInstanceStartup(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepWaitInstanceStartup)
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
c := state.Get("config").(*Config)
|
||||
d := state.Get("driver").(*DriverMock)
|
||||
|
||||
testZone := "test-zone"
|
||||
testInstanceName := "test-instance-name"
|
||||
|
||||
config.Zone = testZone
|
||||
c.Zone = testZone
|
||||
state.Put("instance_name", testInstanceName)
|
||||
// The done log triggers step completion.
|
||||
driver.GetSerialPortOutputResult = StartupScriptDoneLog
|
||||
|
||||
|
||||
// This step stops when it gets Done back from the metadata.
|
||||
d.GetInstanceMetadataResult = StartupScriptStatusDone
|
||||
|
||||
// Run the step.
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action)
|
||||
}
|
||||
|
||||
// Check that GetSerialPortOutput was called properly.
|
||||
if driver.GetSerialPortOutputZone != testZone {
|
||||
t.Fatalf(
|
||||
"GetSerialPortOutput wrong zone. Expected: %s, Actual: %s", driver.GetSerialPortOutputZone,
|
||||
testZone)
|
||||
}
|
||||
if driver.GetSerialPortOutputName != testInstanceName {
|
||||
t.Fatalf(
|
||||
"GetSerialPortOutput wrong instance name. Expected: %s, Actual: %s", driver.GetSerialPortOutputName,
|
||||
testInstanceName)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
|
||||
|
||||
// Check that GetInstanceMetadata was called properly.
|
||||
assert.Equal(t, d.GetInstanceMetadataZone, testZone, "Incorrect zone passed to GetInstanceMetadata.")
|
||||
assert.Equal(t, d.GetInstanceMetadataName, testInstanceName, "Incorrect instance name passed to GetInstanceMetadata.")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
// winrmConfig returns the WinRM configuration.
|
||||
func winrmConfig(state multistep.StateBag) (*communicator.WinRMConfig, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
password := state.Get("winrm_password").(string)
|
||||
|
||||
return &communicator.WinRMConfig{
|
||||
Username: config.Comm.WinRMUser,
|
||||
Password: password,
|
||||
}, nil
|
||||
}
|
|
@ -46,15 +46,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
state.Put("ui", ui)
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -116,15 +116,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// FloppyConfig is configuration related to created floppy disks and attaching
|
||||
// them to a Parallels virtual machine.
|
||||
type FloppyConfig struct {
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
}
|
||||
|
||||
func (c *FloppyConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
if c.FloppyFiles == nil {
|
||||
c.FloppyFiles = make([]string, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloppyConfigPrepare(t *testing.T) {
|
||||
c := new(FloppyConfig)
|
||||
|
||||
errs := c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("err: %#v", errs)
|
||||
}
|
||||
|
||||
if len(c.FloppyFiles) > 0 {
|
||||
t.Fatal("should not have floppy files")
|
||||
}
|
||||
}
|
|
@ -186,6 +186,13 @@ func scancodes(message string) []string {
|
|||
special["<pageUp>"] = []string{"49", "c9"}
|
||||
special["<pageDown>"] = []string{"51", "d1"}
|
||||
|
||||
special["<leftAlt>"] = []string{"38", "b8"}
|
||||
special["<leftCtrl>"] = []string{"1d", "9d"}
|
||||
special["<leftShift>"] = []string{"2a", "aa"}
|
||||
special["<rightAlt>"] = []string{"e038", "e0b8"}
|
||||
special["<rightCtrl>"] = []string{"e01d", "e09d"}
|
||||
special["<rightShift>"] = []string{"36", "b6"}
|
||||
|
||||
shiftedChars := "!@#$%^&*()_+{}:\"~|<>?"
|
||||
|
||||
scancodeIndex := make(map[string]uint)
|
||||
|
@ -214,6 +221,78 @@ func scancodes(message string) []string {
|
|||
for len(message) > 0 {
|
||||
var scancode []string
|
||||
|
||||
if strings.HasPrefix(message, "<leftAltOn>") {
|
||||
scancode = []string{"38"}
|
||||
message = message[len("<leftAltOn>"):]
|
||||
log.Printf("Special code '<leftAltOn>' found, replacing with: 38")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftCtrlOn>") {
|
||||
scancode = []string{"1d"}
|
||||
message = message[len("<leftCtrlOn>"):]
|
||||
log.Printf("Special code '<leftCtrlOn>' found, replacing with: 1d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftShiftOn>") {
|
||||
scancode = []string{"2a"}
|
||||
message = message[len("<leftShiftOn>"):]
|
||||
log.Printf("Special code '<leftShiftOn>' found, replacing with: 2a")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftAltOff>") {
|
||||
scancode = []string{"b8"}
|
||||
message = message[len("<leftAltOff>"):]
|
||||
log.Printf("Special code '<leftAltOff>' found, replacing with: b8")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftCtrlOff>") {
|
||||
scancode = []string{"9d"}
|
||||
message = message[len("<leftCtrlOff>"):]
|
||||
log.Printf("Special code '<leftCtrlOff>' found, replacing with: 9d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftShiftOff>") {
|
||||
scancode = []string{"aa"}
|
||||
message = message[len("<leftShiftOff>"):]
|
||||
log.Printf("Special code '<leftShiftOff>' found, replacing with: aa")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightAltOn>") {
|
||||
scancode = []string{"e038"}
|
||||
message = message[len("<rightAltOn>"):]
|
||||
log.Printf("Special code '<rightAltOn>' found, replacing with: e038")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightCtrlOn>") {
|
||||
scancode = []string{"e01d"}
|
||||
message = message[len("<rightCtrlOn>"):]
|
||||
log.Printf("Special code '<rightCtrlOn>' found, replacing with: e01d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightShiftOn>") {
|
||||
scancode = []string{"36"}
|
||||
message = message[len("<rightShiftOn>"):]
|
||||
log.Printf("Special code '<rightShiftOn>' found, replacing with: 36")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightAltOff>") {
|
||||
scancode = []string{"e0b8"}
|
||||
message = message[len("<rightAltOff>"):]
|
||||
log.Printf("Special code '<rightAltOff>' found, replacing with: e0b8")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightCtrlOff>") {
|
||||
scancode = []string{"e09d"}
|
||||
message = message[len("<rightCtrlOff>"):]
|
||||
log.Printf("Special code '<rightCtrlOff>' found, replacing with: e09d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightShiftOff>") {
|
||||
scancode = []string{"b6"}
|
||||
message = message[len("<rightShiftOff>"):]
|
||||
log.Printf("Special code '<rightShiftOff>' found, replacing with: b6")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<wait>") {
|
||||
log.Printf("Special code <wait> found, will sleep 1 second at this point.")
|
||||
scancode = []string{"wait"}
|
||||
|
|
|
@ -25,7 +25,7 @@ type Config struct {
|
|||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
parallelscommon.FloppyConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
parallelscommon.OutputConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlPostConfig `mapstructure:",squash"`
|
||||
|
@ -215,17 +215,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
state.Put("ui", ui)
|
||||
|
||||
// Run
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package iso
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
|
@ -46,6 +49,55 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "floppy_files")
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if len(b.config.FloppyFiles) != 0 {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
config["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
b = Builder{}
|
||||
_, errs := b.Prepare(config)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_DiskSize(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
|
|
@ -108,16 +108,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run the steps.
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
// Config is the configuration structure for the builder.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
parallelscommon.FloppyConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
parallelscommon.OutputConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlPostConfig `mapstructure:",squash"`
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package pvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) map[string]interface{} {
|
||||
|
@ -11,6 +14,7 @@ func testConfig(t *testing.T) map[string]interface{} {
|
|||
"ssh_username": "foo",
|
||||
"shutdown_command": "foo",
|
||||
"parallels_tools_flavor": "lin",
|
||||
"source_path": "config_test.go",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +72,29 @@ func TestNewConfig_sourcePath(t *testing.T) {
|
|||
testConfigOk(t, warns, errs)
|
||||
}
|
||||
|
||||
func TestNewConfig_FloppyFiles(t *testing.T) {
|
||||
c := testConfig(t)
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
c["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
_, _, err := NewConfig(c)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfig_InvalidFloppies(t *testing.T) {
|
||||
c := testConfig(t)
|
||||
c["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
_, _, errs := NewConfig(c)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfig_shutdown_timeout(t *testing.T) {
|
||||
c := testConfig(t)
|
||||
tf := getTempFile(t)
|
||||
|
|
|
@ -82,6 +82,7 @@ type Config struct {
|
|||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
|
||||
ISOSkipCache bool `mapstructure:"iso_skip_cache"`
|
||||
Accelerator string `mapstructure:"accelerator"`
|
||||
|
@ -139,6 +140,9 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if b.config.DiskSize == 0 {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
@ -215,9 +219,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.Format = "qcow2"
|
||||
}
|
||||
|
||||
if b.config.FloppyFiles == nil {
|
||||
b.config.FloppyFiles = make([]string, 0)
|
||||
}
|
||||
errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
if b.config.NetDevice == "" {
|
||||
b.config.NetDevice = "virtio-net"
|
||||
|
@ -232,9 +234,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if b.config.ISOSkipCache {
|
||||
b.config.ISOChecksumType = "none"
|
||||
}
|
||||
|
@ -376,19 +375,40 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
HTTPPortMin: b.config.HTTPPortMin,
|
||||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
},
|
||||
new(stepForwardSSH),
|
||||
)
|
||||
|
||||
if b.config.Comm.Type != "none" {
|
||||
steps = append(steps,
|
||||
new(stepForwardSSH),
|
||||
)
|
||||
}
|
||||
|
||||
steps = append(steps,
|
||||
new(stepConfigureVNC),
|
||||
steprun,
|
||||
&stepBootWait{},
|
||||
&stepTypeBootCommand{},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
SSHPort: commPort,
|
||||
},
|
||||
)
|
||||
|
||||
if b.config.Comm.Type != "none" {
|
||||
steps = append(steps,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
SSHPort: commPort,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
steps = append(steps,
|
||||
new(common.StepProvision),
|
||||
)
|
||||
steps = append(steps,
|
||||
new(stepShutdown),
|
||||
)
|
||||
|
||||
steps = append(steps,
|
||||
new(stepConvertDisk),
|
||||
)
|
||||
|
||||
|
@ -402,17 +422,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
state.Put("ui", ui)
|
||||
|
||||
// Run
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
var testPem = `
|
||||
|
@ -262,6 +264,55 @@ func TestBuilderPrepare_Format(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "floppy_files")
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if len(b.config.FloppyFiles) != 0 {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
|
||||
floppies_path := "../../common/test-fixtures/floppies"
|
||||
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
config["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
b = Builder{}
|
||||
_, errs := b.Prepare(config)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
|
|
@ -24,20 +24,12 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
|
||||
var sshHostPort uint
|
||||
var offset uint = 0
|
||||
|
||||
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = uint(rand.Intn(portRange))
|
||||
}
|
||||
portRange := config.SSHHostPortMax - config.SSHHostPortMin + 1
|
||||
offset := uint(rand.Intn(int(portRange)))
|
||||
|
||||
for {
|
||||
sshHostPort = offset + config.SSHHostPortMin
|
||||
if sshHostPort >= config.SSHHostPortMax {
|
||||
offset = 0
|
||||
sshHostPort = config.SSHHostPortMin
|
||||
}
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
||||
if err == nil {
|
||||
|
@ -45,6 +37,9 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
break
|
||||
}
|
||||
offset++
|
||||
if offset == portRange {
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
ui.Say(fmt.Sprintf("Found port for communicator (SSH, WinRM, etc): %d.", sshHostPort))
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
command, err := getCommandArgs(s.BootDrive, state)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing QemuArggs: %s", err)
|
||||
err := fmt.Errorf("Error processing QemuArgs: %s", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
@ -63,7 +63,6 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
isoPath := state.Get("iso_path").(string)
|
||||
vncIP := state.Get("vnc_ip").(string)
|
||||
vncPort := state.Get("vnc_port").(uint)
|
||||
sshHostPort := state.Get("sshHostPort").(uint)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
driver := state.Get("driver").(Driver)
|
||||
|
||||
|
@ -74,10 +73,16 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
defaultArgs := make(map[string]interface{})
|
||||
var deviceArgs []string
|
||||
var driveArgs []string
|
||||
var sshHostPort uint
|
||||
|
||||
defaultArgs["-name"] = vmName
|
||||
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
|
||||
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
|
||||
if config.Comm.Type != "none" {
|
||||
sshHostPort = state.Get("sshHostPort").(uint)
|
||||
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
|
||||
} else {
|
||||
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0")
|
||||
}
|
||||
|
||||
qemuVersion, err := driver.Version()
|
||||
if err != nil {
|
||||
|
@ -91,12 +96,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
if qemuMajor >= 2 {
|
||||
if config.DiskInterface == "virtio-scsi" {
|
||||
deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0", "scsi-hd,bus=scsi0.0,drive=drive0")
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("if=none,file=%s,id=drive0,cache=%s,discard=%s", imgPath, config.DiskCache, config.DiskDiscard))
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("if=none,file=%s,id=drive0,cache=%s,discard=%s,format=%s", imgPath, config.DiskCache, config.DiskDiscard, config.Format))
|
||||
} else {
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard))
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format))
|
||||
}
|
||||
} else {
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s", imgPath, config.DiskInterface, config.DiskCache))
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format))
|
||||
}
|
||||
deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
|
||||
|
||||
|
@ -157,13 +162,23 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
|
||||
httpPort := state.Get("http_port").(uint)
|
||||
ctx := config.ctx
|
||||
ctx.Data = qemuArgsTemplateData{
|
||||
"10.0.2.2",
|
||||
httpPort,
|
||||
config.HTTPDir,
|
||||
config.OutputDir,
|
||||
config.VMName,
|
||||
sshHostPort,
|
||||
if config.Comm.Type != "none" {
|
||||
ctx.Data = qemuArgsTemplateData{
|
||||
"10.0.2.2",
|
||||
httpPort,
|
||||
config.HTTPDir,
|
||||
config.OutputDir,
|
||||
config.VMName,
|
||||
sshHostPort,
|
||||
}
|
||||
} else {
|
||||
ctx.Data = qemuArgsTemplateData{
|
||||
HTTPIP: "10.0.2.2",
|
||||
HTTPPort: httpPort,
|
||||
HTTPDir: config.HTTPDir,
|
||||
OutputDir: config.OutputDir,
|
||||
Name: config.VMName,
|
||||
}
|
||||
}
|
||||
newQemuArgs, err := processArgs(config.QemuArgs, &ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,10 +3,11 @@ package qemu
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// This step shuts down the machine. It first attempts to do so gracefully,
|
||||
|
@ -23,11 +24,29 @@ import (
|
|||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if state.Get("communicator") == nil {
|
||||
cancelCh := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer close(cancelCh)
|
||||
<-time.After(config.shutdownTimeout)
|
||||
}()
|
||||
ui.Say("Waiting for shutdown...")
|
||||
if ok := driver.WaitForShutdown(cancelCh); ok {
|
||||
log.Println("VM shut down.")
|
||||
return multistep.ActionContinue
|
||||
} else {
|
||||
err := fmt.Errorf("Failed to shutdown")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
if config.ShutdownCommand != "" {
|
||||
ui.Say("Gracefully halting virtual machine...")
|
||||
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
|
||||
|
|
|
@ -136,6 +136,12 @@ func vncSendString(c *vnc.ClientConn, original string) {
|
|||
special["<end>"] = 0xFF57
|
||||
special["<pageUp>"] = 0xFF55
|
||||
special["<pageDown>"] = 0xFF56
|
||||
special["<leftAlt>"] = 0xFFE9
|
||||
special["<leftCtrl>"] = 0xFFE3
|
||||
special["<leftShift>"] = 0xFFE1
|
||||
special["<rightAlt>"] = 0xFFEA
|
||||
special["<rightCtrl>"] = 0xFFE4
|
||||
special["<rightShift>"] = 0xFFE2
|
||||
|
||||
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
|
||||
|
||||
|
@ -144,6 +150,174 @@ func vncSendString(c *vnc.ClientConn, original string) {
|
|||
var keyCode uint32
|
||||
keyShift := false
|
||||
|
||||
if strings.HasPrefix(original, "<leftAltOn>") {
|
||||
keyCode = special["<leftAlt>"]
|
||||
original = original[len("<leftAltOn>"):]
|
||||
log.Printf("Special code '<leftAltOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftCtrlOn>") {
|
||||
keyCode = special["<leftCtrlOn>"]
|
||||
original = original[len("<leftCtrlOn>"):]
|
||||
log.Printf("Special code '<leftCtrlOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftShiftOn>") {
|
||||
keyCode = special["<leftShiftOn>"]
|
||||
original = original[len("<leftShiftOn>"):]
|
||||
log.Printf("Special code '<leftShiftOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftAltOff>") {
|
||||
keyCode = special["<leftAltOff>"]
|
||||
original = original[len("<leftAltOff>"):]
|
||||
log.Printf("Special code '<leftAltOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftCtrlOff>") {
|
||||
keyCode = special["<leftCtrlOff>"]
|
||||
original = original[len("<leftCtrlOff>"):]
|
||||
log.Printf("Special code '<leftCtrlOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftShiftOff>") {
|
||||
keyCode = special["<leftShiftOff>"]
|
||||
original = original[len("<leftShiftOff>"):]
|
||||
log.Printf("Special code '<leftShiftOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightAltOn>") {
|
||||
keyCode = special["<rightAltOn>"]
|
||||
original = original[len("<rightAltOn>"):]
|
||||
log.Printf("Special code '<rightAltOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightCtrlOn>") {
|
||||
keyCode = special["<rightCtrlOn>"]
|
||||
original = original[len("<rightCtrlOn>"):]
|
||||
log.Printf("Special code '<rightCtrlOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightShiftOn>") {
|
||||
keyCode = special["<rightShiftOn>"]
|
||||
original = original[len("<rightShiftOn>"):]
|
||||
log.Printf("Special code '<rightShiftOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightAltOff>") {
|
||||
keyCode = special["<rightAltOff>"]
|
||||
original = original[len("<rightAltOff>"):]
|
||||
log.Printf("Special code '<rightAltOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightCtrlOff>") {
|
||||
keyCode = special["<rightCtrlOff>"]
|
||||
original = original[len("<rightCtrlOff>"):]
|
||||
log.Printf("Special code '<rightCtrlOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightShiftOff>") {
|
||||
keyCode = special["<rightShiftOff>"]
|
||||
original = original[len("<rightShiftOff>"):]
|
||||
log.Printf("Special code '<rightShiftOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
// qemu is picky, so no matter what, wait a small period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<wait>") {
|
||||
log.Printf("Special code '<wait>' found, sleeping one second")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloppyConfigPrepare(t *testing.T) {
|
||||
c := new(FloppyConfig)
|
||||
|
||||
errs := c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("err: %#v", errs)
|
||||
}
|
||||
|
||||
if len(c.FloppyFiles) > 0 {
|
||||
t.Fatal("should not have floppy files")
|
||||
}
|
||||
}
|
|
@ -37,20 +37,12 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
if !s.SkipNatMapping {
|
||||
log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d",
|
||||
s.HostPortMin, s.HostPortMax)
|
||||
offset := 0
|
||||
|
||||
portRange := int(s.HostPortMax - s.HostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = rand.Intn(portRange)
|
||||
}
|
||||
portRange := int(s.HostPortMax - s.HostPortMin + 1)
|
||||
offset := rand.Intn(portRange)
|
||||
|
||||
for {
|
||||
sshHostPort = offset + int(s.HostPortMin)
|
||||
if sshHostPort >= int(s.HostPortMax) {
|
||||
offset = 0
|
||||
sshHostPort = int(s.HostPortMin)
|
||||
}
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
|
||||
if err == nil {
|
||||
|
@ -58,6 +50,9 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
break
|
||||
}
|
||||
offset++
|
||||
if offset == portRange {
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Create a forwarded port mapping to the VM
|
||||
|
|
|
@ -141,6 +141,12 @@ func scancodes(message string) []string {
|
|||
special["<end>"] = []string{"4f", "cf"}
|
||||
special["<pageUp>"] = []string{"49", "c9"}
|
||||
special["<pageDown>"] = []string{"51", "d1"}
|
||||
special["<leftAlt>"] = []string{"38", "b8"}
|
||||
special["<leftCtrl>"] = []string{"1d", "9d"}
|
||||
special["<leftShift>"] = []string{"2a", "aa"}
|
||||
special["<rightAlt>"] = []string{"e038", "e0b8"}
|
||||
special["<rightCtrl>"] = []string{"e01d", "e09d"}
|
||||
special["<rightShift>"] = []string{"36", "b6"}
|
||||
|
||||
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
|
||||
|
||||
|
@ -170,6 +176,78 @@ func scancodes(message string) []string {
|
|||
for len(message) > 0 {
|
||||
var scancode []string
|
||||
|
||||
if strings.HasPrefix(message, "<leftAltOn>") {
|
||||
scancode = []string{"38"}
|
||||
message = message[len("<leftAltOn>"):]
|
||||
log.Printf("Special code '<leftAltOn>' found, replacing with: 38")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftCtrlOn>") {
|
||||
scancode = []string{"1d"}
|
||||
message = message[len("<leftCtrlOn>"):]
|
||||
log.Printf("Special code '<leftCtrlOn>' found, replacing with: 1d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftShiftOn>") {
|
||||
scancode = []string{"2a"}
|
||||
message = message[len("<leftShiftOn>"):]
|
||||
log.Printf("Special code '<leftShiftOn>' found, replacing with: 2a")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftAltOff>") {
|
||||
scancode = []string{"b8"}
|
||||
message = message[len("<leftAltOff>"):]
|
||||
log.Printf("Special code '<leftAltOff>' found, replacing with: b8")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftCtrlOff>") {
|
||||
scancode = []string{"9d"}
|
||||
message = message[len("<leftCtrlOff>"):]
|
||||
log.Printf("Special code '<leftCtrlOff>' found, replacing with: 9d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<leftShiftOff>") {
|
||||
scancode = []string{"aa"}
|
||||
message = message[len("<leftShiftOff>"):]
|
||||
log.Printf("Special code '<leftShiftOff>' found, replacing with: aa")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightAltOn>") {
|
||||
scancode = []string{"e038"}
|
||||
message = message[len("<rightAltOn>"):]
|
||||
log.Printf("Special code '<rightAltOn>' found, replacing with: e038")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightCtrlOn>") {
|
||||
scancode = []string{"e01d"}
|
||||
message = message[len("<rightCtrlOn>"):]
|
||||
log.Printf("Special code '<rightCtrlOn>' found, replacing with: e01d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightShiftOn>") {
|
||||
scancode = []string{"36"}
|
||||
message = message[len("<rightShiftOn>"):]
|
||||
log.Printf("Special code '<rightShiftOn>' found, replacing with: 36")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightAltOff>") {
|
||||
scancode = []string{"e0b8"}
|
||||
message = message[len("<rightAltOff>"):]
|
||||
log.Printf("Special code '<rightAltOff>' found, replacing with: e0b8")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightCtrlOff>") {
|
||||
scancode = []string{"e09d"}
|
||||
message = message[len("<rightCtrlOff>"):]
|
||||
log.Printf("Special code '<rightCtrlOff>' found, replacing with: e09d")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<rightShiftOff>") {
|
||||
scancode = []string{"b6"}
|
||||
message = message[len("<rightShiftOff>"):]
|
||||
log.Printf("Special code '<rightShiftOff>' found, replacing with: b6")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<wait>") {
|
||||
log.Printf("Special code <wait> found, will sleep 1 second at this point.")
|
||||
scancode = []string{"wait"}
|
||||
|
|
|
@ -26,9 +26,9 @@ type Config struct {
|
|||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
vboxcommon.ExportConfig `mapstructure:",squash"`
|
||||
vboxcommon.ExportOpts `mapstructure:",squash"`
|
||||
vboxcommon.FloppyConfig `mapstructure:",squash"`
|
||||
vboxcommon.OutputConfig `mapstructure:",squash"`
|
||||
vboxcommon.RunConfig `mapstructure:",squash"`
|
||||
vboxcommon.ShutdownConfig `mapstructure:",squash"`
|
||||
|
@ -275,17 +275,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
state.Put("ui", ui)
|
||||
|
||||
// Run
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/builder/virtualbox/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
|
@ -86,6 +89,55 @@ func TestBuilderPrepare_DiskSize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "floppy_files")
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if len(b.config.FloppyFiles) != 0 {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
config["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
b = Builder{}
|
||||
_, errs := b.Prepare(config)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_GuestAdditionsMode(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
|
|
@ -135,16 +135,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run the steps.
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
|
|
|
@ -16,9 +16,9 @@ import (
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
vboxcommon.ExportConfig `mapstructure:",squash"`
|
||||
vboxcommon.ExportOpts `mapstructure:",squash"`
|
||||
vboxcommon.FloppyConfig `mapstructure:",squash"`
|
||||
vboxcommon.OutputConfig `mapstructure:",squash"`
|
||||
vboxcommon.RunConfig `mapstructure:",squash"`
|
||||
vboxcommon.SSHConfig `mapstructure:",squash"`
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
package ovf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"ssh_username": "foo",
|
||||
"shutdown_command": "foo",
|
||||
"source_path": "config_test.go",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +48,29 @@ func testConfigOk(t *testing.T, warns []string, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewConfig_FloppyFiles(t *testing.T) {
|
||||
c := testConfig(t)
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
c["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
_, _, err := NewConfig(c)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfig_InvalidFloppies(t *testing.T) {
|
||||
c := testConfig(t)
|
||||
c["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
_, _, errs := NewConfig(c)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfig_sourcePath(t *testing.T) {
|
||||
// Bad
|
||||
c := testConfig(t)
|
||||
|
|
|
@ -168,6 +168,12 @@ func vncSendString(c *vnc.ClientConn, original string) {
|
|||
special["<end>"] = 0xFF57
|
||||
special["<pageUp>"] = 0xFF55
|
||||
special["<pageDown>"] = 0xFF56
|
||||
special["<leftAlt>"] = 0xFFE9
|
||||
special["<leftCtrl>"] = 0xFFE3
|
||||
special["<leftShift>"] = 0xFFE1
|
||||
special["<rightAlt>"] = 0xFFEA
|
||||
special["<rightCtrl>"] = 0xFFE4
|
||||
special["<rightShift>"] = 0xFFE2
|
||||
|
||||
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
|
||||
|
||||
|
@ -176,6 +182,138 @@ func vncSendString(c *vnc.ClientConn, original string) {
|
|||
var keyCode uint32
|
||||
keyShift := false
|
||||
|
||||
if strings.HasPrefix(original, "<leftAltOn>") {
|
||||
keyCode = special["<leftAlt>"]
|
||||
original = original[len("<leftAltOn>"):]
|
||||
log.Printf("Special code '<leftAltOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftCtrlOn>") {
|
||||
keyCode = special["<leftCtrlOn>"]
|
||||
original = original[len("<leftCtrlOn>"):]
|
||||
log.Printf("Special code '<leftCtrlOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftShiftOn>") {
|
||||
keyCode = special["<leftShiftOn>"]
|
||||
original = original[len("<leftShiftOn>"):]
|
||||
log.Printf("Special code '<leftShiftOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftAltOff>") {
|
||||
keyCode = special["<leftAltOff>"]
|
||||
original = original[len("<leftAltOff>"):]
|
||||
log.Printf("Special code '<leftAltOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftCtrlOff>") {
|
||||
keyCode = special["<leftCtrlOff>"]
|
||||
original = original[len("<leftCtrlOff>"):]
|
||||
log.Printf("Special code '<leftCtrlOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<leftShiftOff>") {
|
||||
keyCode = special["<leftShiftOff>"]
|
||||
original = original[len("<leftShiftOff>"):]
|
||||
log.Printf("Special code '<leftShiftOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightAltOn>") {
|
||||
keyCode = special["<rightAltOn>"]
|
||||
original = original[len("<rightAltOn>"):]
|
||||
log.Printf("Special code '<rightAltOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightCtrlOn>") {
|
||||
keyCode = special["<rightCtrlOn>"]
|
||||
original = original[len("<rightCtrlOn>"):]
|
||||
log.Printf("Special code '<rightCtrlOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightShiftOn>") {
|
||||
keyCode = special["<rightShiftOn>"]
|
||||
original = original[len("<rightShiftOn>"):]
|
||||
log.Printf("Special code '<rightShiftOn>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, true)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightAltOff>") {
|
||||
keyCode = special["<rightAltOff>"]
|
||||
original = original[len("<rightAltOff>"):]
|
||||
log.Printf("Special code '<rightAltOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightCtrlOff>") {
|
||||
keyCode = special["<rightCtrlOff>"]
|
||||
original = original[len("<rightCtrlOff>"):]
|
||||
log.Printf("Special code '<rightCtrlOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<rightShiftOff>") {
|
||||
keyCode = special["<rightShiftOff>"]
|
||||
original = original[len("<rightShiftOff>"):]
|
||||
log.Printf("Special code '<rightShiftOff>' found, replacing with: %d", keyCode)
|
||||
|
||||
c.KeyEvent(keyCode, false)
|
||||
time.Sleep(time.Second / 10)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<wait>") {
|
||||
log.Printf("Special code '<wait>' found, sleeping one second")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
|
|
@ -46,13 +46,13 @@ func EncodeVMX(contents map[string]string) string {
|
|||
// a list of VMX key fragments that the value must not be quoted
|
||||
// fragments are used to cover multliples (i.e. multiple disks)
|
||||
// keys are still lowercase at this point, use lower fragments
|
||||
noQuotes := []string {
|
||||
noQuotes := []string{
|
||||
".virtualssd",
|
||||
}
|
||||
|
||||
// a list of VMX key fragments that are case sensitive
|
||||
// fragments are used to cover multliples (i.e. multiple disks)
|
||||
caseSensitive := []string {
|
||||
caseSensitive := []string{
|
||||
".virtualSSD",
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ func EncodeVMX(contents map[string]string) string {
|
|||
for _, q := range noQuotes {
|
||||
if strings.Contains(k, q) {
|
||||
pat = "%s = %s\n"
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
key := k
|
||||
|
|
|
@ -29,8 +29,8 @@ scsi0:0.virtualSSD = 1
|
|||
|
||||
func TestEncodeVMX(t *testing.T) {
|
||||
contents := map[string]string{
|
||||
".encoding": "UTF-8",
|
||||
"config.version": "8",
|
||||
".encoding": "UTF-8",
|
||||
"config.version": "8",
|
||||
"scsi0:0.virtualssd": "1",
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ type Config struct {
|
|||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
vmwcommon.DriverConfig `mapstructure:",squash"`
|
||||
vmwcommon.OutputConfig `mapstructure:",squash"`
|
||||
vmwcommon.RunConfig `mapstructure:",squash"`
|
||||
|
@ -40,7 +41,6 @@ type Config struct {
|
|||
DiskName string `mapstructure:"vmdk_name"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
DiskTypeId string `mapstructure:"disk_type_id"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
Format string `mapstructure:"format"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
Version string `mapstructure:"version"`
|
||||
|
@ -97,6 +97,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.ToolsConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.VMXConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
if b.config.DiskName == "" {
|
||||
b.config.DiskName = "disk"
|
||||
|
@ -115,10 +116,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if b.config.FloppyFiles == nil {
|
||||
b.config.FloppyFiles = make([]string, 0)
|
||||
}
|
||||
|
||||
if b.config.GuestOSType == "" {
|
||||
b.config.GuestOSType = "other"
|
||||
}
|
||||
|
@ -308,17 +305,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
|
@ -106,7 +107,8 @@ func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
|
||||
config["floppy_files"] = []string{"foo", "bar"}
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
|
@ -116,12 +118,27 @@ func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar"}
|
||||
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
config["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
b = Builder{}
|
||||
_, errs := b.Prepare(config)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Format(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
|
|
@ -122,16 +122,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
// Run the steps.
|
||||
if b.config.PackerDebug {
|
||||
pauseFn := common.MultistepDebugFn(ui)
|
||||
state.Put("pauseFn", pauseFn)
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: pauseFn,
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package vmx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||
|
@ -33,7 +36,8 @@ func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
|
||||
config["floppy_files"] = []string{"foo", "bar"}
|
||||
floppies_path := "../../../common/test-fixtures/floppies"
|
||||
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
|
@ -43,8 +47,23 @@ func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar"}
|
||||
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
|
||||
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig(t)
|
||||
config["floppy_files"] = []string{"nonexistant.bat", "nonexistant.ps1"}
|
||||
b = Builder{}
|
||||
_, errs := b.Prepare(config)
|
||||
if errs == nil {
|
||||
t.Fatalf("Non existant floppies should trigger multierror")
|
||||
}
|
||||
|
||||
if len(errs.(*packer.MultiError).Errors) != 2 {
|
||||
t.Fatalf("Multierror should work and report 2 errors")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
vmwcommon.DriverConfig `mapstructure:",squash"`
|
||||
vmwcommon.OutputConfig `mapstructure:",squash"`
|
||||
vmwcommon.RunConfig `mapstructure:",squash"`
|
||||
|
@ -24,7 +25,6 @@ type Config struct {
|
|||
vmwcommon.VMXConfig `mapstructure:",squash"`
|
||||
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
RemoteType string `mapstructure:"remote_type"`
|
||||
SkipCompaction bool `mapstructure:"skip_compaction"`
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
|
@ -64,6 +64,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
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)...)
|
||||
|
||||
if c.SourcePath == "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is blank, but is required"))
|
||||
|
|
|
@ -10,6 +10,7 @@ func testConfig(t *testing.T) map[string]interface{} {
|
|||
return map[string]interface{}{
|
||||
"ssh_username": "foo",
|
||||
"shutdown_command": "foo",
|
||||
"source_path": "config_test.go",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mitchellh/packer/helper/enumflag"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template"
|
||||
)
|
||||
|
@ -20,11 +21,14 @@ type BuildCommand struct {
|
|||
|
||||
func (c BuildCommand) Run(args []string) int {
|
||||
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
|
||||
var cfgOnError string
|
||||
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
|
||||
flags.Usage = func() { c.Ui.Say(c.Help()) }
|
||||
flags.BoolVar(&cfgColor, "color", true, "")
|
||||
flags.BoolVar(&cfgDebug, "debug", false, "")
|
||||
flags.BoolVar(&cfgForce, "force", false, "")
|
||||
flagOnError := enumflag.New(&cfgOnError, "cleanup", "abort", "ask")
|
||||
flags.Var(flagOnError, "on-error", "")
|
||||
flags.BoolVar(&cfgParallel, "parallel", true, "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
|
@ -99,12 +103,14 @@ func (c BuildCommand) Run(args []string) int {
|
|||
|
||||
log.Printf("Build debug mode: %v", cfgDebug)
|
||||
log.Printf("Force build: %v", cfgForce)
|
||||
log.Printf("On error: %v", cfgOnError)
|
||||
|
||||
// Set the debug and force mode and prepare all the builds
|
||||
for _, b := range builds {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
b.SetDebug(cfgDebug)
|
||||
b.SetForce(cfgForce)
|
||||
b.SetOnError(cfgOnError)
|
||||
|
||||
warnings, err := b.Prepare()
|
||||
if err != nil {
|
||||
|
@ -284,7 +290,7 @@ Options:
|
|||
-except=foo,bar,baz Build all builds other than these
|
||||
-force Force a build to continue if artifacts exist, deletes existing artifacts
|
||||
-machine-readable Machine-readable output
|
||||
-only=foo,bar,baz Only build the given builds by name
|
||||
-on-error=[cleanup|abort|ask] If the build fails do: clean up (default), abort, or ask
|
||||
-parallel=false Disable parallelization (on by default)
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
|
|
|
@ -15,50 +15,49 @@ import (
|
|||
|
||||
amazonchrootbuilder "github.com/mitchellh/packer/builder/amazon/chroot"
|
||||
amazonebsbuilder "github.com/mitchellh/packer/builder/amazon/ebs"
|
||||
amazonimportpostprocessor "github.com/mitchellh/packer/post-processor/amazon-import"
|
||||
amazoninstancebuilder "github.com/mitchellh/packer/builder/amazon/instance"
|
||||
ansiblelocalprovisioner "github.com/mitchellh/packer/provisioner/ansible-local"
|
||||
ansibleprovisioner "github.com/mitchellh/packer/provisioner/ansible"
|
||||
artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice"
|
||||
atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas"
|
||||
azurearmbuilder "github.com/mitchellh/packer/builder/azure/arm"
|
||||
checksumpostprocessor "github.com/mitchellh/packer/post-processor/checksum"
|
||||
chefclientprovisioner "github.com/mitchellh/packer/provisioner/chef-client"
|
||||
chefsoloprovisioner "github.com/mitchellh/packer/provisioner/chef-solo"
|
||||
compresspostprocessor "github.com/mitchellh/packer/post-processor/compress"
|
||||
digitaloceanbuilder "github.com/mitchellh/packer/builder/digitalocean"
|
||||
dockerbuilder "github.com/mitchellh/packer/builder/docker"
|
||||
dockerimportpostprocessor "github.com/mitchellh/packer/post-processor/docker-import"
|
||||
dockerpushpostprocessor "github.com/mitchellh/packer/post-processor/docker-push"
|
||||
dockersavepostprocessor "github.com/mitchellh/packer/post-processor/docker-save"
|
||||
dockertagpostprocessor "github.com/mitchellh/packer/post-processor/docker-tag"
|
||||
filebuilder "github.com/mitchellh/packer/builder/file"
|
||||
fileprovisioner "github.com/mitchellh/packer/provisioner/file"
|
||||
googlecomputebuilder "github.com/mitchellh/packer/builder/googlecompute"
|
||||
googlecomputeexportpostprocessor "github.com/mitchellh/packer/post-processor/googlecompute-export"
|
||||
manifestpostprocessor "github.com/mitchellh/packer/post-processor/manifest"
|
||||
nullbuilder "github.com/mitchellh/packer/builder/null"
|
||||
openstackbuilder "github.com/mitchellh/packer/builder/openstack"
|
||||
parallelsisobuilder "github.com/mitchellh/packer/builder/parallels/iso"
|
||||
parallelspvmbuilder "github.com/mitchellh/packer/builder/parallels/pvm"
|
||||
powershellprovisioner "github.com/mitchellh/packer/provisioner/powershell"
|
||||
puppetmasterlessprovisioner "github.com/mitchellh/packer/provisioner/puppet-masterless"
|
||||
puppetserverprovisioner "github.com/mitchellh/packer/provisioner/puppet-server"
|
||||
qemubuilder "github.com/mitchellh/packer/builder/qemu"
|
||||
saltmasterlessprovisioner "github.com/mitchellh/packer/provisioner/salt-masterless"
|
||||
shelllocalpostprocessor "github.com/mitchellh/packer/post-processor/shell-local"
|
||||
shelllocalprovisioner "github.com/mitchellh/packer/provisioner/shell-local"
|
||||
shellprovisioner "github.com/mitchellh/packer/provisioner/shell"
|
||||
vagrantcloudpostprocessor "github.com/mitchellh/packer/post-processor/vagrant-cloud"
|
||||
vagrantpostprocessor "github.com/mitchellh/packer/post-processor/vagrant"
|
||||
virtualboxisobuilder "github.com/mitchellh/packer/builder/virtualbox/iso"
|
||||
virtualboxovfbuilder "github.com/mitchellh/packer/builder/virtualbox/ovf"
|
||||
vmwareisobuilder "github.com/mitchellh/packer/builder/vmware/iso"
|
||||
vmwarevmxbuilder "github.com/mitchellh/packer/builder/vmware/vmx"
|
||||
amazonimportpostprocessor "github.com/mitchellh/packer/post-processor/amazon-import"
|
||||
artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice"
|
||||
atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas"
|
||||
checksumpostprocessor "github.com/mitchellh/packer/post-processor/checksum"
|
||||
compresspostprocessor "github.com/mitchellh/packer/post-processor/compress"
|
||||
dockerimportpostprocessor "github.com/mitchellh/packer/post-processor/docker-import"
|
||||
dockerpushpostprocessor "github.com/mitchellh/packer/post-processor/docker-push"
|
||||
dockersavepostprocessor "github.com/mitchellh/packer/post-processor/docker-save"
|
||||
dockertagpostprocessor "github.com/mitchellh/packer/post-processor/docker-tag"
|
||||
googlecomputeexportpostprocessor "github.com/mitchellh/packer/post-processor/googlecompute-export"
|
||||
manifestpostprocessor "github.com/mitchellh/packer/post-processor/manifest"
|
||||
shelllocalpostprocessor "github.com/mitchellh/packer/post-processor/shell-local"
|
||||
vagrantpostprocessor "github.com/mitchellh/packer/post-processor/vagrant"
|
||||
vagrantcloudpostprocessor "github.com/mitchellh/packer/post-processor/vagrant-cloud"
|
||||
vspherepostprocessor "github.com/mitchellh/packer/post-processor/vsphere"
|
||||
ansibleprovisioner "github.com/mitchellh/packer/provisioner/ansible"
|
||||
ansiblelocalprovisioner "github.com/mitchellh/packer/provisioner/ansible-local"
|
||||
chefclientprovisioner "github.com/mitchellh/packer/provisioner/chef-client"
|
||||
chefsoloprovisioner "github.com/mitchellh/packer/provisioner/chef-solo"
|
||||
fileprovisioner "github.com/mitchellh/packer/provisioner/file"
|
||||
powershellprovisioner "github.com/mitchellh/packer/provisioner/powershell"
|
||||
puppetmasterlessprovisioner "github.com/mitchellh/packer/provisioner/puppet-masterless"
|
||||
puppetserverprovisioner "github.com/mitchellh/packer/provisioner/puppet-server"
|
||||
saltmasterlessprovisioner "github.com/mitchellh/packer/provisioner/salt-masterless"
|
||||
shellprovisioner "github.com/mitchellh/packer/provisioner/shell"
|
||||
shelllocalprovisioner "github.com/mitchellh/packer/provisioner/shell-local"
|
||||
windowsrestartprovisioner "github.com/mitchellh/packer/provisioner/windows-restart"
|
||||
windowsshellprovisioner "github.com/mitchellh/packer/provisioner/windows-shell"
|
||||
|
||||
)
|
||||
|
||||
type PluginCommand struct {
|
||||
|
@ -67,61 +66,58 @@ type PluginCommand struct {
|
|||
|
||||
var Builders = map[string]packer.Builder{
|
||||
"amazon-chroot": new(amazonchrootbuilder.Builder),
|
||||
"amazon-ebs": new(amazonebsbuilder.Builder),
|
||||
"amazon-instance": new(amazoninstancebuilder.Builder),
|
||||
"azure-arm": new(azurearmbuilder.Builder),
|
||||
"digitalocean": new(digitaloceanbuilder.Builder),
|
||||
"docker": new(dockerbuilder.Builder),
|
||||
"file": new(filebuilder.Builder),
|
||||
"amazon-ebs": new(amazonebsbuilder.Builder),
|
||||
"amazon-instance": new(amazoninstancebuilder.Builder),
|
||||
"azure-arm": new(azurearmbuilder.Builder),
|
||||
"digitalocean": new(digitaloceanbuilder.Builder),
|
||||
"docker": new(dockerbuilder.Builder),
|
||||
"file": new(filebuilder.Builder),
|
||||
"googlecompute": new(googlecomputebuilder.Builder),
|
||||
"null": new(nullbuilder.Builder),
|
||||
"openstack": new(openstackbuilder.Builder),
|
||||
"null": new(nullbuilder.Builder),
|
||||
"openstack": new(openstackbuilder.Builder),
|
||||
"parallels-iso": new(parallelsisobuilder.Builder),
|
||||
"parallels-pvm": new(parallelspvmbuilder.Builder),
|
||||
"qemu": new(qemubuilder.Builder),
|
||||
"virtualbox-iso": new(virtualboxisobuilder.Builder),
|
||||
"virtualbox-ovf": new(virtualboxovfbuilder.Builder),
|
||||
"vmware-iso": new(vmwareisobuilder.Builder),
|
||||
"vmware-vmx": new(vmwarevmxbuilder.Builder),
|
||||
"qemu": new(qemubuilder.Builder),
|
||||
"virtualbox-iso": new(virtualboxisobuilder.Builder),
|
||||
"virtualbox-ovf": new(virtualboxovfbuilder.Builder),
|
||||
"vmware-iso": new(vmwareisobuilder.Builder),
|
||||
"vmware-vmx": new(vmwarevmxbuilder.Builder),
|
||||
}
|
||||
|
||||
|
||||
var Provisioners = map[string]packer.Provisioner{
|
||||
"ansible": new(ansibleprovisioner.Provisioner),
|
||||
"ansible-local": new(ansiblelocalprovisioner.Provisioner),
|
||||
"chef-client": new(chefclientprovisioner.Provisioner),
|
||||
"chef-solo": new(chefsoloprovisioner.Provisioner),
|
||||
"file": new(fileprovisioner.Provisioner),
|
||||
"powershell": new(powershellprovisioner.Provisioner),
|
||||
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
|
||||
"puppet-server": new(puppetserverprovisioner.Provisioner),
|
||||
"ansible": new(ansibleprovisioner.Provisioner),
|
||||
"ansible-local": new(ansiblelocalprovisioner.Provisioner),
|
||||
"chef-client": new(chefclientprovisioner.Provisioner),
|
||||
"chef-solo": new(chefsoloprovisioner.Provisioner),
|
||||
"file": new(fileprovisioner.Provisioner),
|
||||
"powershell": new(powershellprovisioner.Provisioner),
|
||||
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
|
||||
"puppet-server": new(puppetserverprovisioner.Provisioner),
|
||||
"salt-masterless": new(saltmasterlessprovisioner.Provisioner),
|
||||
"shell": new(shellprovisioner.Provisioner),
|
||||
"shell-local": new(shelllocalprovisioner.Provisioner),
|
||||
"shell": new(shellprovisioner.Provisioner),
|
||||
"shell-local": new(shelllocalprovisioner.Provisioner),
|
||||
"windows-restart": new(windowsrestartprovisioner.Provisioner),
|
||||
"windows-shell": new(windowsshellprovisioner.Provisioner),
|
||||
"windows-shell": new(windowsshellprovisioner.Provisioner),
|
||||
}
|
||||
|
||||
|
||||
var PostProcessors = map[string]packer.PostProcessor{
|
||||
"amazon-import": new(amazonimportpostprocessor.PostProcessor),
|
||||
"artifice": new(artificepostprocessor.PostProcessor),
|
||||
"atlas": new(atlaspostprocessor.PostProcessor),
|
||||
"checksum": new(checksumpostprocessor.PostProcessor),
|
||||
"compress": new(compresspostprocessor.PostProcessor),
|
||||
"docker-import": new(dockerimportpostprocessor.PostProcessor),
|
||||
"docker-push": new(dockerpushpostprocessor.PostProcessor),
|
||||
"docker-save": new(dockersavepostprocessor.PostProcessor),
|
||||
"docker-tag": new(dockertagpostprocessor.PostProcessor),
|
||||
"googlecompute-export": new(googlecomputeexportpostprocessor.PostProcessor),
|
||||
"manifest": new(manifestpostprocessor.PostProcessor),
|
||||
"shell-local": new(shelllocalpostprocessor.PostProcessor),
|
||||
"vagrant": new(vagrantpostprocessor.PostProcessor),
|
||||
"vagrant-cloud": new(vagrantcloudpostprocessor.PostProcessor),
|
||||
"vsphere": new(vspherepostprocessor.PostProcessor),
|
||||
"amazon-import": new(amazonimportpostprocessor.PostProcessor),
|
||||
"artifice": new(artificepostprocessor.PostProcessor),
|
||||
"atlas": new(atlaspostprocessor.PostProcessor),
|
||||
"checksum": new(checksumpostprocessor.PostProcessor),
|
||||
"compress": new(compresspostprocessor.PostProcessor),
|
||||
"docker-import": new(dockerimportpostprocessor.PostProcessor),
|
||||
"docker-push": new(dockerpushpostprocessor.PostProcessor),
|
||||
"docker-save": new(dockersavepostprocessor.PostProcessor),
|
||||
"docker-tag": new(dockertagpostprocessor.PostProcessor),
|
||||
"googlecompute-export": new(googlecomputeexportpostprocessor.PostProcessor),
|
||||
"manifest": new(manifestpostprocessor.PostProcessor),
|
||||
"shell-local": new(shelllocalpostprocessor.PostProcessor),
|
||||
"vagrant": new(vagrantpostprocessor.PostProcessor),
|
||||
"vagrant-cloud": new(vagrantcloudpostprocessor.PostProcessor),
|
||||
"vsphere": new(vspherepostprocessor.PostProcessor),
|
||||
}
|
||||
|
||||
|
||||
var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner)-(.+)")
|
||||
|
||||
func (c *PluginCommand) Run(args []string) int {
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
const archiveTemplateEntry = ".packer-template"
|
||||
|
||||
var (
|
||||
reName = regexp.MustCompile("^[a-zA-Z0-9-_/]+$")
|
||||
reName = regexp.MustCompile("^[a-zA-Z0-9-_./]+$")
|
||||
errInvalidName = fmt.Errorf("Your build name can only contain these characters: %s", reName.String())
|
||||
)
|
||||
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// FloppyConfig is configuration related to created floppy disks and attaching
|
||||
// them to a VirtualBox machine.
|
||||
type FloppyConfig struct {
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
}
|
||||
|
||||
func (c *FloppyConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
|
||||
if c.FloppyFiles == nil {
|
||||
c.FloppyFiles = make([]string, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
for _, path := range c.FloppyFiles {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
errs = append(errs, fmt.Errorf("Bad Floppy disk file '%s': %s", path, err))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNilFloppies(t *testing.T) {
|
||||
c := FloppyConfig{}
|
||||
errs := c.Prepare(nil)
|
||||
if len(errs) != 0 {
|
||||
t.Fatal("nil floppies array should not fail")
|
||||
}
|
||||
|
||||
if len(c.FloppyFiles) > 0 {
|
||||
t.Fatal("struct should not have floppy files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyArrayFloppies(t *testing.T) {
|
||||
c := FloppyConfig{
|
||||
FloppyFiles: make([]string, 0),
|
||||
}
|
||||
|
||||
errs := c.Prepare(nil)
|
||||
if len(errs) != 0 {
|
||||
t.Fatal("empty floppies array should never fail")
|
||||
}
|
||||
|
||||
if len(c.FloppyFiles) > 0 {
|
||||
t.Fatal("struct should not have floppy files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingFloppyFile(t *testing.T) {
|
||||
c := FloppyConfig{
|
||||
FloppyFiles: []string{"floppy_config.go"},
|
||||
}
|
||||
|
||||
errs := c.Prepare(nil)
|
||||
if len(errs) != 0 {
|
||||
t.Fatal("array with existing floppies should not fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonExistingFloppyFile(t *testing.T) {
|
||||
c := FloppyConfig{
|
||||
FloppyFiles: []string{"floppy_config.foo"},
|
||||
}
|
||||
|
||||
errs := c.Prepare(nil)
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("array with non existing floppies should return errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiErrorFloppyFiles(t *testing.T) {
|
||||
c := FloppyConfig{
|
||||
FloppyFiles: []string{"floppy_config.foo", "floppy_config.go", "floppy_config.bar", "floppy_config_test.go", "floppy_config.baz"},
|
||||
}
|
||||
|
||||
errs := c.Prepare(nil)
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("array with non existing floppies should return errors")
|
||||
}
|
||||
|
||||
expectedErrors := 3
|
||||
if count := len(errs); count != expectedErrors {
|
||||
t.Fatalf("array with %v non existing floppy should return %v errors but it is returning %v", expectedErrors, expectedErrors, count)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func newRunner(steps []multistep.Step, config PackerConfig, ui packer.Ui) (multistep.Runner, multistep.DebugPauseFn) {
|
||||
switch config.PackerOnError {
|
||||
case "", "cleanup":
|
||||
case "abort":
|
||||
for i, step := range steps {
|
||||
steps[i] = abortStep{step, ui}
|
||||
}
|
||||
case "ask":
|
||||
for i, step := range steps {
|
||||
steps[i] = askStep{step, ui}
|
||||
}
|
||||
}
|
||||
|
||||
if config.PackerDebug {
|
||||
pauseFn := MultistepDebugFn(ui)
|
||||
return &multistep.DebugRunner{Steps: steps, PauseFn: pauseFn}, pauseFn
|
||||
} else {
|
||||
return &multistep.BasicRunner{Steps: steps}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRunner returns a multistep.Runner that runs steps augmented with support
|
||||
// for -debug and -on-error command line arguments.
|
||||
func NewRunner(steps []multistep.Step, config PackerConfig, ui packer.Ui) multistep.Runner {
|
||||
runner, _ := newRunner(steps, config, ui)
|
||||
return runner
|
||||
}
|
||||
|
||||
// NewRunnerWithPauseFn returns a multistep.Runner that runs steps augmented
|
||||
// with support for -debug and -on-error command line arguments. With -debug it
|
||||
// puts the multistep.DebugPauseFn that will pause execution between steps into
|
||||
// the state under the key "pauseFn".
|
||||
func NewRunnerWithPauseFn(steps []multistep.Step, config PackerConfig, ui packer.Ui, state multistep.StateBag) multistep.Runner {
|
||||
runner, pauseFn := newRunner(steps, config, ui)
|
||||
if pauseFn != nil {
|
||||
state.Put("pauseFn", pauseFn)
|
||||
}
|
||||
return runner
|
||||
}
|
||||
|
||||
func typeName(i interface{}) string {
|
||||
return reflect.Indirect(reflect.ValueOf(i)).Type().Name()
|
||||
}
|
||||
|
||||
type abortStep struct {
|
||||
step multistep.Step
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
func (s abortStep) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return s.step.Run(state)
|
||||
}
|
||||
|
||||
func (s abortStep) Cleanup(state multistep.StateBag) {
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
s.ui.Error("Interrupted, aborting...")
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, ok := state.GetOk(multistep.StateHalted); ok {
|
||||
s.ui.Error(fmt.Sprintf("Step %q failed, aborting...", typeName(s.step)))
|
||||
os.Exit(1)
|
||||
}
|
||||
s.step.Cleanup(state)
|
||||
}
|
||||
|
||||
type askStep struct {
|
||||
step multistep.Step
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
func (s askStep) Run(state multistep.StateBag) (action multistep.StepAction) {
|
||||
for {
|
||||
action = s.step.Run(state)
|
||||
|
||||
if action != multistep.ActionHalt {
|
||||
return
|
||||
}
|
||||
|
||||
switch ask(s.ui, typeName(s.step), state) {
|
||||
case askCleanup:
|
||||
return
|
||||
case askAbort:
|
||||
os.Exit(1)
|
||||
case askRetry:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s askStep) Cleanup(state multistep.StateBag) {
|
||||
s.step.Cleanup(state)
|
||||
}
|
||||
|
||||
type askResponse int
|
||||
|
||||
const (
|
||||
askCleanup askResponse = iota
|
||||
askAbort
|
||||
askRetry
|
||||
)
|
||||
|
||||
func ask(ui packer.Ui, name string, state multistep.StateBag) askResponse {
|
||||
ui.Say(fmt.Sprintf("Step %q failed", name))
|
||||
|
||||
result := make(chan askResponse)
|
||||
go func() {
|
||||
result <- askPrompt(ui)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case response := <-result:
|
||||
return response
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
return askCleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func askPrompt(ui packer.Ui) askResponse {
|
||||
for {
|
||||
line, err := ui.Ask("[c] Clean up and exit, [a] abort without cleanup, or [r] retry step (build may fail even if retry succeeds)?")
|
||||
if err != nil {
|
||||
log.Printf("Error asking for input: %s", err)
|
||||
}
|
||||
|
||||
input := strings.ToLower(line) + "c"
|
||||
switch input[0] {
|
||||
case 'c':
|
||||
return askCleanup
|
||||
case 'a':
|
||||
return askAbort
|
||||
case 'r':
|
||||
return askRetry
|
||||
}
|
||||
ui.Say(fmt.Sprintf("Incorrect input: %#v", line))
|
||||
}
|
||||
}
|
|
@ -8,5 +8,6 @@ type PackerConfig struct {
|
|||
PackerBuilderType string `mapstructure:"packer_builder_type"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerForce bool `mapstructure:"packer_force"`
|
||||
PackerOnError string `mapstructure:"packer_on_error"`
|
||||
PackerUserVars map[string]string `mapstructure:"packer_user_variables"`
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package googlecompute
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -25,20 +25,20 @@ func Retry(initialInterval float64, maxInterval float64, numTries uint, function
|
|||
done := false
|
||||
interval := initialInterval
|
||||
for i := uint(0); !done && (numTries == 0 || i < numTries); i++ {
|
||||
done, err = function()
|
||||
done, err = function()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
if !done {
|
||||
// Retry after delay. Calculate next delay.
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
interval = math.Min(interval * 2, maxInterval)
|
||||
interval = math.Min(interval*2, maxInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
return RetryExhaustedError
|
||||
return RetryExhaustedError
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package googlecompute
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -18,7 +18,7 @@ func TestRetry(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Passing function should not have returned a retry error. Error: %s", err)
|
||||
}
|
||||
|
||||
|
||||
// Test that a failing function gets retried (once in this example).
|
||||
numTries = 0
|
||||
results := []bool{false, true}
|
||||
|
@ -33,7 +33,7 @@ func TestRetry(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Successful retried function should not have returned a retry error. Error: %s", err)
|
||||
}
|
||||
|
||||
|
||||
// Test that a function error gets returned, and the function does not get called again.
|
||||
numTries = 0
|
||||
funcErr := fmt.Errorf("This function had an error!")
|
||||
|
@ -47,7 +47,7 @@ func TestRetry(t *testing.T) {
|
|||
if err != funcErr {
|
||||
t.Fatalf("Errant function did not return the right error %s. Error: %s", funcErr, err)
|
||||
}
|
||||
|
||||
|
||||
// Test when a function exhausts its retries.
|
||||
numTries = 0
|
||||
expectedTries := uint(3)
|
||||
|
@ -61,4 +61,4 @@ func TestRetry(t *testing.T) {
|
|||
if err != RetryExhaustedError {
|
||||
t.Fatalf("Unsuccessful retry function should have returned a retry exhausted error. Actual error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Echo I am a floppy with a batch file
|
|
@ -0,0 +1 @@
|
|||
Write-Host "I am a floppy with some Powershell"
|
|
@ -13,4 +13,3 @@ func TestCommIsCommunicator(t *testing.T) {
|
|||
t.Fatalf("comm must be a communicator")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -160,6 +160,7 @@ func (c *comm) UploadDir(dst string, src string, excl []string) error {
|
|||
func (c *comm) DownloadDir(src string, dst string, excl []string) error {
|
||||
log.Printf("Download dir '%s' to '%s'", src, dst)
|
||||
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
||||
dirStack := []string{dst}
|
||||
for {
|
||||
fmt.Fprint(w, "\x00")
|
||||
|
||||
|
@ -178,6 +179,13 @@ func (c *comm) DownloadDir(src string, dst string, excl []string) error {
|
|||
return fmt.Errorf("%s", fi[1:len(fi)])
|
||||
case 'C', 'D':
|
||||
break
|
||||
case 'E':
|
||||
dirStack = dirStack[:len(dirStack)-1]
|
||||
if len(dirStack) == 1 {
|
||||
fmt.Fprint(w, "\x00")
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("unexpected server response (%x)", fi[0])
|
||||
}
|
||||
|
@ -195,14 +203,16 @@ func (c *comm) DownloadDir(src string, dst string, excl []string) error {
|
|||
}
|
||||
|
||||
log.Printf("Download dir mode:%s size:%d name:%s", mode, size, name)
|
||||
|
||||
dst = filepath.Join(dirStack...)
|
||||
switch fi[0] {
|
||||
case 'D':
|
||||
err = os.MkdirAll(filepath.Join(dst, name), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(w, "\x00")
|
||||
return nil
|
||||
dirStack = append(dirStack, name)
|
||||
continue
|
||||
case 'C':
|
||||
fmt.Fprint(w, "\x00")
|
||||
err = scpDownloadFile(filepath.Join(dst, name), stdoutR, size, os.FileMode(0644))
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"variables": {
|
||||
"client_id": "{{env `ARM_CLIENT_ID`}}",
|
||||
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
|
||||
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
|
||||
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
|
||||
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
|
||||
},
|
||||
"builders": [
|
||||
{
|
||||
"type": "azure-arm",
|
||||
|
||||
"client_id": "{{user `client_id`}}",
|
||||
"client_secret": "{{user `client_secret`}}",
|
||||
"resource_group_name": "{{user `resource_group`}}",
|
||||
"storage_account": "{{user `storage_account`}}",
|
||||
"subscription_id": "{{user `subscription_id`}}",
|
||||
|
||||
"capture_container_name": "images",
|
||||
"capture_name_prefix": "packer",
|
||||
|
||||
"os_type": "Linux",
|
||||
"image_url": "https://my-storage-account.blob.core.windows.net/path/to/your/custom/image.vhd",
|
||||
|
||||
"azure_tags": {
|
||||
"dept": "engineering",
|
||||
"task": "image deployment"
|
||||
},
|
||||
|
||||
"location": "West US",
|
||||
"vm_size": "Standard_A2"
|
||||
}
|
||||
],
|
||||
"provisioners": [{
|
||||
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
|
||||
"inline": [
|
||||
"apt-get update",
|
||||
"apt-get upgrade -y",
|
||||
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
|
||||
],
|
||||
"inline_shebang": "/bin/sh -x",
|
||||
"type": "shell"
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"variables": {
|
||||
"client_id": "{{env `ARM_CLIENT_ID`}}",
|
||||
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
|
||||
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
|
||||
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
|
||||
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
|
||||
},
|
||||
"builders": [
|
||||
{
|
||||
"type": "azure-arm",
|
||||
|
||||
"client_id": "{{user `client_id`}}",
|
||||
"client_secret": "{{user `client_secret`}}",
|
||||
"resource_group_name": "{{user `resource_group`}}",
|
||||
"storage_account": "{{user `storage_account`}}",
|
||||
"subscription_id": "{{user `subscription_id`}}",
|
||||
|
||||
"capture_container_name": "images",
|
||||
"capture_name_prefix": "packer",
|
||||
|
||||
"os_type": "Windows",
|
||||
"image_url": "https://my-storage-account.blob.core.windows.net/path/to/your/custom/image.vhd",
|
||||
|
||||
"azure_tags": {
|
||||
"dept": "engineering",
|
||||
"task": "image deployment"
|
||||
},
|
||||
|
||||
"location": "West US",
|
||||
"vm_size": "Standard_A2"
|
||||
}
|
||||
],
|
||||
"provisioners": [{
|
||||
"type": "powershell",
|
||||
"inline": [
|
||||
"dir c:\\"
|
||||
]
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package enumflag
|
||||
|
||||
import "fmt"
|
||||
|
||||
type enumFlag struct {
|
||||
target *string
|
||||
options []string
|
||||
}
|
||||
|
||||
// New returns a flag.Value implementation for parsing flags with a one-of-a-set value
|
||||
func New(target *string, options ...string) *enumFlag {
|
||||
return &enumFlag{target: target, options: options}
|
||||
}
|
||||
|
||||
func (f *enumFlag) String() string {
|
||||
return *f.target
|
||||
}
|
||||
|
||||
func (f *enumFlag) Set(value string) error {
|
||||
for _, v := range f.options {
|
||||
if v == value {
|
||||
*f.target = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected one of %q", f.options)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue