Compare commits

...

205 Commits

Author SHA1 Message Date
Mitchell Hashimoto 801a13dd38 v0.2.1 2013-07-26 09:54:05 -07:00
Mitchell Hashimoto e5b14ac450 builder/amazon/common: wait for AMI to be available [GH-207] 2013-07-26 09:38:16 -07:00
Mitchell Hashimoto e6557c53c2 ctrl-c closes stdin for plugins so that they are unblocked 2013-07-25 23:27:13 -07:00
Mitchell Hashimoto b455d70346 Remove TODO, it was from pre-0.1 2013-07-25 23:14:56 -07:00
Mitchell Hashimoto 48b08fe71a update comments in signal.go 2013-07-25 21:36:46 -05:00
Mitchell Hashimoto 74ef543d5c packer/plugin: connect stdin to parent stdin 2013-07-25 21:24:49 -05:00
Mitchell Hashimoto 9f38f4cb47 fmt 2013-07-25 20:49:39 -05:00
Mitchell Hashimoto 7a00b240c1 builder/amazon/*: sigint while waiting for state change works 2013-07-25 20:49:15 -05:00
Mitchell Hashimoto f5f4266371 Update CHANGELOG 2013-07-25 15:20:20 -05:00
Mitchell Hashimoto 1ec2eacb92 Merge pull request #218 from jstamerj/master
builder/virtualbox, communicator/ssh: fixes for running on windows
2013-07-25 13:19:08 -07:00
Jeff Stamerjohn 88dcb43a3f Merge pull request #1 from jstamerj/jstamerj/windows
Jstamerj/windows
2013-07-25 09:32:13 -07:00
Jeff Stamerjohn c5aad085f3 Merge branch 'master' into jstamerj/windows 2013-07-25 09:31:02 -07:00
Jeff Stamerjohn 41df9fa2e8 Windows fixes
Strip carriage returns from end of lines in virtualbox/driver.go, fixing wait for virtualbox host shutdown.
Fix target upload directory path in communicator.go to use forward slashes.  (When running on windows path/filepath returns backslashes...which does not work when the target host is unix).
2013-07-25 09:28:04 -07:00
Mitchell Hashimoto 029dabaa49 Update CHANGELOG 2013-07-25 10:55:25 -05:00
Mitchell Hashimoto 4857faa363 Merge branch 'builder-amazon-instance'
This adds the "amazon-instance" builder to Packer which can create
instance-storage backed AMIs. The instance-storage backed AMIs are built
by launching another instance-store backed instance, provisioning it,
and then using EC2 AMI tools to bundle and register a new AMI.
2013-07-25 10:52:16 -05:00
Mitchell Hashimoto 0130b1310c builder/amazon/instance: default x509_upload_path 2013-07-25 10:51:38 -05:00
Mitchell Hashimoto 30a4bf7687 website: document the amazon-instance builder 2013-07-25 10:51:21 -05:00
Mitchell Hashimoto 8c3281405d builder/amazon/*: wait for AMI to be ready in common, use it instance 2013-07-25 00:56:37 -05:00
Mitchell Hashimoto 072d7a647a builder/amazon/instance: register AMI using API 2013-07-25 00:45:55 -05:00
Mitchell Hashimoto 77b92cd048 builder/amazon/instance: register the AMI 2013-07-25 00:19:04 -05:00
Mitchell Hashimoto 17f6914fc1 builder/amazon/instance: prefix has CreateTime support 2013-07-24 23:51:46 -05:00
Mitchell Hashimoto 5e6695264a builder/amazon/instance: upload bundle 2013-07-24 23:29:21 -05:00
Mitchell Hashimoto ad1015f35c builder/amazon/instance: bundle volume and keep track of dir 2013-07-24 23:22:16 -05:00
Mitchell Hashimoto 801541714e builder/amazon/instance: run ec2-bundle-vol in batch mode 2013-07-24 22:51:59 -05:00
Mitchell Hashimoto 3f543dddaa builder/amazon/instance: sudo the ec2-bundle-vol by default 2013-07-24 16:07:33 -05:00
Mitchell Hashimoto b5552c00f3 builder/amazon/instance: detect error bundling 2013-07-24 15:44:58 -05:00
Mitchell Hashimoto c6358b0475 builder/amazon/instance: ec2-bundle-vol is called 2013-07-24 15:43:35 -05:00
Mitchell Hashimoto 544b4b1429 builder/amazon/instance: check for the ami tools 2013-07-23 23:19:44 -05:00
Mitchell Hashimoto 3efdb009d7 builder/amazon/common: SSH into private IP if in VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b9b5de5684 fmt 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto a5546fa0f3 builder/amazon/*: Fix failing tests from rebase of VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b11b60746b builder/amazon/instance: boilerplate for bundle volume 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto c39299c21f builder/amazon/instance: after upload x509, should continue 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto d11cabf893 builder/amazon/instance: upload x509 cert 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto caaa6667ed plugin/builder-amazon-instance: add the plugin 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto b91fb5e1a0 fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto a780d0e0dd builder/amazon/common: extract Artifact 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5a4f5c3f6f builder/amazon: provision on instance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5c7c0a6ee2 builder/amazon: extract SSH connect funcs 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 51f7db35f4 builder/amazon: extract StepRunSourceInstance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 579eaaabc5 builder/amazon/common: panic if SSHPot is 0 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto f8319f8881 builder/amazon: extract StepSecurityGroup 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto e67e4cfa16 builder/amazon: extract StepKeyPair for both 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5921492c8e fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto d489f99aad builder/amazon/*: clean up tests 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto ae864b3efa builder/amazon/common: tests for runconfig 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 9fb778c012 builder/common: common config struct for Packer configs 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 45d50279f3 builder/amazon/ebs: validate access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 6fdf237c15 builder/amazon/common: RunConfig for launch info 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 8f649af165 builder/amazon/common: AccessConfig for standard access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 9348f481dd builder/amazon/instance: boilerplate 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 17acee653b packer: In the case of an error, close stdout/stderr writers 2013-07-23 22:46:51 -05:00
Mitchell Hashimoto 1c5e9848b5 provisioner/shell: use StartWithUi 2013-07-23 22:44:32 -05:00
Mitchell Hashimoto 479855a4f1 builder/virtualbox: shutdown messages go to Ui 2013-07-23 22:42:39 -05:00
Mitchell Hashimoto 3641c1a708 packer: Preserve original stdout/stderr on StartWithUi
/cc @sit
2013-07-23 22:38:03 -05:00
Mitchell Hashimoto 2e82358d2a fmt 2013-07-23 22:36:09 -05:00
Mitchell Hashimoto 32c84c17b1 packer: RemoteCmd.StartWithUi thanks to @sit
I ended up working on this on my own outside your PR, sorry, but it is
basically the same as yours.
2013-07-23 22:35:05 -05:00
Mitchell Hashimoto bd1fe21718 packer/rpc: remove unused test file 2013-07-23 13:45:35 -07:00
Mitchell Hashimoto 6422fabd19 command/build: if any builds fail, exit with 1 exit status 2013-07-23 13:36:07 -07:00
Mitchell Hashimoto 54fb31b7a3 Update CHANGELOG 2013-07-21 22:40:02 -07:00
Mitchell Hashimoto 51b63efe50 Merge pull request #210 from rgarcia/vpc-launch
builder/amazon: support launching in VPC
2013-07-21 22:39:01 -07:00
Rafael Garcia 282554b2b0 builder/amazon/ebs: support launching in VPC
adds `vpc_id` and `subnet_id` to builder config

depends on https://github.com/mitchellh/goamz/pull/4
2013-07-21 00:44:14 -07:00
Mitchell Hashimoto 38ae1a0ba9 packer: Properly handle ? in URLs in cache keys
/cc @sit - Found another edge case
2013-07-20 19:20:29 -07:00
Mitchell Hashimoto e3478c38ef builder/vmware: more flexible ISO removing
/cc @timsutton - This is a bit more flexible.
2013-07-20 19:08:20 -07:00
Mitchell Hashimoto 7cab4c4058 Update CHANGELOG 2013-07-20 17:00:36 -07:00
Mitchell Hashimoto 9be941ec3e Merge pull request #203 from timsutton/vmware-eject-isos
builder/vmware: Remove ISO from ide1 in stepCleanVMX
2013-07-20 16:59:55 -07:00
Mitchell Hashimoto e51e9cd56e Merge pull request #204 from sit/builder-output-directory-error
builder/v*: Include path to output dir in error
2013-07-20 16:58:39 -07:00
Mitchell Hashimoto 9c393742df Update CHANGELOG 2013-07-20 16:55:22 -07:00
Mitchell Hashimoto f64fd019e5 packer: Extension works properly with URLs too
/cc @sit
2013-07-20 16:53:55 -07:00
Mitchell Hashimoto cf9bc2c819 packer: Use strings operations, faster than regexp and easy
/cc @sit
2013-07-20 16:50:18 -07:00
Mitchell Hashimoto 9349c81631 Merge pull request #205 from sit/preserve-cache-suffix
Preserve suffix of files put in the cache.
2013-07-20 16:40:31 -07:00
Mitchell Hashimoto f97148bc22 Update changelog 2013-07-20 16:37:48 -07:00
Mitchell Hashimoto cff38c6aea Merge pull request #206 from psi/add-more-special-keys
builder/vmware, builder/virtualbox: add special keys for backspace, delete and F1-F12
2013-07-20 16:36:30 -07:00
JD Harrington 6028a3c3de Add special keys for backspace, delete and F1-F12 2013-07-20 15:37:01 -04:00
Mitchell Hashimoto d179d1ad00 website: sleep in getting started 2013-07-20 12:35:51 -07:00
Emil Sit 20d3946eae builder/v*: Include path to output dir in error 2013-07-20 00:36:59 -04:00
Emil Sit bc4b2d079d Revert "builder/virtualbox: Copy ISO because VirtualBox can't recognize"
This reverts commit 688be43811007d33b9c70dec93b43782a4f71c06; instead
of copying the ISO (which can be huge), we have adjusted the cache path
to include the extension of the source file.

Conflicts:

	builder/virtualbox/step_download_iso.go
2013-07-20 00:26:46 -04:00
Emil Sit 27da543a41 packer/cache: Preserve any extension found on keys
This allows us to hand cache paths to any programs that may want to
interpret file extensions in order to behave differently. For
example, VirtualBox may want ISO images to end with .iso.
2013-07-20 00:19:05 -04:00
Emil Sit fd1ec31a1d packer/cache: Extract cache path calculation to method 2013-07-20 00:15:14 -04:00
Timothy Sutton 7e6e699593 Remove ISO from ide1 in stepCleanVMX 2013-07-19 20:46:26 -04:00
Mitchell Hashimoto 7d1f799ced builder/*: switch to MultiErrorAppend 2013-07-19 19:08:25 -04:00
Mitchell Hashimoto 40ec63d349 packer: MultiErrorAppend helper method. 2013-07-19 18:14:50 -04:00
Mitchell Hashimoto 5dbae2efde builder/common: CheckUnusedConfig 2013-07-19 15:09:13 -04:00
Mitchell Hashimoto d1ecd89635 builder/*: extract to common the config decoding 2013-07-19 15:00:32 -04:00
Mitchell Hashimoto 6f6a27b008 builder/common: extract out decode config 2013-07-19 14:59:04 -04:00
Mitchell Hashimoto 5b8c372fdc communicator/ssh: style
/cc @markpeek - The main thing is that the style for variables/methods
in Go is to use CamelCase (with the first letter generally downcased
unless an exported thing).
2013-07-19 14:16:39 -04:00
Mitchell Hashimoto c80db71361 packer: fix failing tests 2013-07-19 14:13:02 -04:00
Mitchell Hashimoto abf4cfecb2 communicator/ssh: remove complete TODO 2013-07-19 14:09:20 -04:00
Mitchell Hashimoto 27fc4da5f9 Update CHANGELOG 2013-07-19 14:08:59 -04:00
Mitchell Hashimoto 0784f7961b Merge pull request #195 from markpeek/markpeek-file-error
communicator/ssh: check scp codes and report errors
2013-07-19 11:07:03 -07:00
Mitchell Hashimoto 71c0b2c1d5 Update CHANGELOG 2013-07-19 14:02:54 -04:00
Mitchell Hashimoto 9f93ca7d3b website: powered by bintray 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto bf3e137a5a website: remove the ?direct in downloads to use CDN 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto f226bd13af Merge pull request #199 from qur/builder-name
packer: builder name should be removed from rawConfig
2013-07-19 11:02:05 -07:00
Julian Phillips dca4fabd1b packer/template: Remove name from builder rawConfig
This prevents the builder from rejecting the name (which it doesn't
know about) when validating the config.
2013-07-19 15:36:13 +01:00
Julian Phillips a31fd29b8b packer/template: Test that builder.rawConfig excludes name
The name isn't actually part of the builder config, so it should be
removed during parsing.
2013-07-19 15:35:05 +01:00
Radu Voicilas 0f1bc9ee6f Add a clean target to the Makefile 2013-07-18 10:40:23 -04:00
Mark Peek ed75091bd4 Log an upload error closer to when it occurs
When there is an error returned from the Upload it is propagated back
to the main loop and may have additional log messages before the actual
error is displayed. This displays a log message close to where the error
actually occurred.
2013-07-17 18:17:46 -07:00
Mark Peek a8a53fb0b4 Return errors from remote scp command
Currently file provisioners will silently fail when there is an error
with the remote side. This commit checks the scp error code and will
return the error message from the remote.
2013-07-17 18:15:42 -07:00
Mitchell Hashimoto 104178080c Merge pull request #192 from dougm/fusion-leases-path
builder/vmware: fix Fusion DhcpLeasesPath
2013-07-17 16:23:28 -07:00
Doug MacEachern 923a541d8c fix Fusion DhcpLeasesPath 2013-07-17 13:03:05 -07:00
Mitchell Hashimoto bca05ddd5b builder/vmware: look for license for WS9 2013-07-16 14:58:16 -07:00
Mitchell Hashimoto 04b8ff89a6 builder/vmware: workstation 9 show stderr if command fails 2013-07-16 14:46:34 -07:00
Mitchell Hashimoto b9f0b81821 scripts: Use XC_ARCH/XC_OS to limit what arch/os combo is built 2013-07-16 14:36:54 -07:00
Mitchell Hashimoto 56402f9432 Update CHANGELOG 2013-07-16 14:24:56 -07:00
Mitchell Hashimoto cbddd413f1 builder/vmware: wording 2013-07-16 14:23:52 -07:00
Mitchell Hashimoto 229011def4 builder/vmware: split drivers out into individual files 2013-07-16 14:23:01 -07:00
Julian Phillips d7e5a3bb1d builder/vmware: Add support for Workstation 9 (Linux).
Now the VMware builder should automatically pick between Fusion 5 and
Workstation 9, based on which one is installed.
2013-07-16 14:11:59 -07:00
Mitchell Hashimoto 8cb035cc65 Up version for dev 2013-07-16 11:44:15 -07:00
Mitchell Hashimoto e6dcb739d7 website: fix download parsing 2013-07-16 11:42:11 -07:00
Mitchell Hashimoto bdc44a53ab v0.2.0 2013-07-16 11:26:02 -07:00
Mitchell Hashimoto c668ef8f70 builder/common: only count handshake error if auth tried 2013-07-16 11:15:47 -07:00
Mitchell Hashimoto 55882a65c7 builder/vmware: error if guest IP is blank [GH-189] 2013-07-16 11:05:47 -07:00
Mitchell Hashimoto 28a22a5fbe builder/*: convert to common StepProvision 2013-07-16 15:44:41 +09:00
Mitchell Hashimoto 55d3f87f3f builder/common: add StepProvision 2013-07-16 15:43:01 +09:00
Mitchell Hashimoto 2cea79c54a builder/amazon/ebs: rename to amazon/ebs in order to support others 2013-07-15 15:02:18 +09:00
Mitchell Hashimoto 3d44633712 Merge branch 'common-ssh'
This unifies all the builders so that they all wait for SSH using a common
step available in builder/common. This common step has the best practices of
every previously unique step, such as retrying SSH handshakes, limiting
the number of SSH handshakes, timeouts, and so on.
2013-07-15 14:23:08 +09:00
Mitchell Hashimoto 61f02c5d02 CHANGELOG 2013-07-15 14:22:55 +09:00
Mitchell Hashimoto d49480ebff builder/vmware: convert to cmmon ssh step 2013-07-15 14:22:13 +09:00
Mitchell Hashimoto 60b8e1f2dc builder/virtualbox: use common SSH connect step 2013-07-15 14:17:09 +09:00
Mitchell Hashimoto d3ea6c96c5 builder/digitalocean: use common connect ssh 2013-07-15 14:14:23 +09:00
Mitchell Hashimoto 2ee81f1491 builder/common: if cancel during SSH, cancel the attempts 2013-07-15 14:10:30 +09:00
Mitchell Hashimoto 51e998a81f builder/amazonebs: switch to common SSH connect step 2013-07-15 14:06:41 +09:00
Mitchell Hashimoto 2f7097bcfe builder/common: add common StepConnectSSH for builders 2013-07-15 11:41:48 +09:00
Mitchell Hashimoto 5aa8448576 website: document new shell env vars 2013-07-15 10:14:36 +09:00
Mitchell Hashimoto d8a0128d1f provisioner/shell: make build name and builder type env vars [GH-154] 2013-07-15 10:07:49 +09:00
Mitchell Hashimoto b1c7d93ee8 packer: Make builder type available in configs [GH-154] 2013-07-15 09:58:32 +09:00
Mitchell Hashimoto a47ad137a0 packer: move constants into a single const() 2013-07-15 09:55:41 +09:00
Mitchell Hashimoto 85f92e35f6 Merge pull request #181 from qur/overrides
packer: Delete "override" key for config passed to provisioners
2013-07-14 17:22:01 -07:00
Mitchell Hashimoto 2895523e45 packer: Add failing test for [GH-181] to fix 2013-07-15 09:21:19 +09:00
Julian Phillips 37f78fe796 Don't expose overrides directly to provisioners
The overrides are processed, so that the provisioner only sees the
appropriate sub-settings, so the provisioner does not need the raw
overrides.

If we leave the top-level overrides object in the provisioner
configuration, then it will be rejected as an unknown configuration
key.
2013-07-14 22:10:20 +01:00
Mitchell Hashimoto 2ba0876d34 builder/vmware: unexport calculted config fields 2013-07-14 21:23:46 +09:00
Mitchell Hashimoto 033c90cfa3 builder/virtualbox: unexport calculated fields 2013-07-14 21:22:11 +09:00
Mitchell Hashimoto 1a6fca7563 builder/digitalocean: unexport calcaulted settings 2013-07-14 21:20:29 +09:00
Mitchell Hashimoto 039a5019e4 builder/amazonebs: Don't export sshTimeout so it can't be set 2013-07-14 21:18:18 +09:00
Mitchell Hashimoto b353c4946e communicator/ssh: improved logging 2013-07-14 21:07:32 +09:00
Mitchell Hashimoto 544a8de3e8 Update CHANGELOG 2013-07-14 21:06:19 +09:00
Mitchell Hashimoto 559e0a7d6f communicator/ssh: tweakable timeout on retry connection 2013-07-14 21:05:22 +09:00
Mitchell Hashimoto 21113fd550 communicator/ssh: retry connection in ConnectFunc forawhile 2013-07-14 21:02:47 +09:00
Mitchell Hashimoto 343614a172 fmt 2013-07-14 20:55:27 +09:00
Mitchell Hashimoto f65711c716 communicator/ssh: re-establish ssh connection if possible [GH-152] 2013-07-14 20:55:02 +09:00
Mitchell Hashimoto 076a4befab communicator/ssh: ConnectFunc times out after 15 sceonds 2013-07-14 20:31:51 +09:00
Mitchell Hashimoto 4d297315c4 communicator/ssh: have a Connection func so we can re-establish
[GH-152]
2013-07-14 20:22:57 +09:00
Mitchell Hashimoto 89e12245af website: document the fix command 2013-07-14 17:38:31 +09:00
Mitchell Hashimoto 06186c97b6 website: document the checksum features 2013-07-14 17:28:53 +09:00
Mitchell Hashimoto 46f37ab8e3 Update CHANGELOG for packer fix 2013-07-14 17:13:19 +09:00
Mitchell Hashimoto e5773e1b66 command/fix: properly replace the escaped </> 2013-07-14 17:10:50 +09:00
Mitchell Hashimoto 6f86fd9849 command/fix: run the iso-md5 fixer 2013-07-14 17:05:26 +09:00
Mitchell Hashimoto f91d4f2244 command/fix: fixer for iso_md5 change 2013-07-14 16:51:20 +09:00
Mitchell Hashimoto 8ee8612997 command/fix: boilerplate for the fix command 2013-07-14 16:22:18 +09:00
Mitchell Hashimoto 82915de7f9 builder/common: support sha1/sha256 hashes 2013-07-14 16:13:07 +09:00
Mitchell Hashimoto eeffebc3e2 Update CHANGELOG 2013-07-14 16:07:08 +09:00
Mitchell Hashimoto 5b4fc718aa builder/vmware: generic checksum types [GH-175] 2013-07-14 15:58:56 +09:00
Mitchell Hashimoto 5a529ee41a builder/virtualbox: support generic hash types [GH-175] 2013-07-14 15:50:34 +09:00
Mitchell Hashimoto 4f4af1621f Update CHANGELOG 2013-07-14 12:30:09 +09:00
Mark Peek 839045264e Move AWS auth decisions to goamz [GH-160]
Currently the passed in AWS auth or AWS environment variables are
interpreted by packer. This change moves that logic into goamz in
order to support both the existing and instance based IAM role
authentication. This requires a corresponding change to goamz.
2013-07-14 12:29:43 +09:00
Mitchell Hashimoto da45a790e1 website: add PACKER_LOG_PATH docs 2013-07-14 11:20:31 +09:00
Mitchell Hashimoto 8e79bb44a7 Clean up some of the log path detection code
/cc @markpeek I think this is much cleaner. Also, I made it so it
doesn't append by default... I like the idea of appending but it scares
me that for a bug report someone might put like a multi-megabyte gist of
logs at me. HAHA. We'll see.
2013-07-14 11:00:44 +09:00
Mitchell Hashimoto 097e44da48 Merge pull request #168 from markpeek/markpeek-filelogging
Add ability to send log output to a file using PACKER_LOG_PATH
2013-07-13 18:55:12 -07:00
Mitchell Hashimoto b358a0ea6e Error if unknown root level key in template [GH-180] 2013-07-14 10:29:14 +09:00
Mitchell Hashimoto a354f5fc08 Unused keys are invalid in templates [GH-104] 2013-07-14 09:28:56 +09:00
Mark Peek bad2bfc4e5 Add PACKER_LOG_PATH for logging to a file
When the environment variables PACKER_LOG and PACKER_LOG_PATH are both
set the log output will be appended to the PACKER_LOG_PATH file.
2013-07-13 15:51:27 +00:00
Mitchell Hashimoto 39b3fe794b buidler/virtualbox,vmware: only stat if not force
/cc @jasonberanek - Swapped this because why even bother doing a stat
if we're forcing anyways.
2013-07-13 14:47:08 +09:00
Mitchell Hashimoto 96861cf216 Merge pull request #179 from jasonberanek/force-build
builder/virtualbox, vmware: Remove output directory in a step when "-force" flag step [GH-178]
2013-07-12 22:45:42 -07:00
Jason A. Beranek 442c5e3e3b Remove output directory in a step when "-force" flag step [GH-178] 2013-07-12 21:23:00 -05:00
Mitchell Hashimoto 39ec791bb2 Merge pull request #176 from smerrill/better-contributor-notes
Add instructions on how to get Go set up to contribute.
2013-07-12 18:04:59 -07:00
Mitchell Hashimoto 7c2f50d4dc fmt 2013-07-13 10:01:27 +09:00
Mitchell Hashimoto e8082e3c72 Update CHANGELOG 2013-07-13 10:01:19 +09:00
Mitchell Hashimoto f131b622be Merge pull request #173 from jasonberanek/force-build
packer, builder/*: Add support for -force flag on builds [GH-119]
2013-07-12 18:00:21 -07:00
Steven Merrill db5b98f79b Add instructions for how to get Go set up to contribute. 2013-07-12 14:40:03 -04:00
Mitchell Hashimoto b33c914fea website: add docs for new security_group_id 2013-07-12 17:27:03 +09:00
Mitchell Hashimoto edb9534773 Merge pull request #162 from brendanhay/persistent-security-group
builder/amazonebs: Ability to specify a pre-existing SecurityGroupId for the AmazonEBS Builder
2013-07-12 01:19:25 -07:00
Mitchell Hashimoto 65a0042a0d Update CHANGELOG 2013-07-12 17:01:38 +09:00
Mitchell Hashimoto 4184926cc8 Merge pull request #164 from mitchellh/do-env-vars
builder/digitalocean: use detected env variables for credentials
2013-07-12 01:00:50 -07:00
Jack Pearkes bc4704f5ca builder/digitalocean: add tests for credentials via env vars 2013-07-12 09:47:45 +02:00
Jason A. Beranek 71a1bd7354 Add support for -force flag on builds [GH-119] 2013-07-11 23:43:23 -05:00
Mitchell Hashimoto 0211c033f4 fmt 2013-07-12 07:01:30 +09:00
Mitchell Hashimoto c88f75b234 Update CHANGELOG 2013-07-12 07:01:23 +09:00
Mitchell Hashimoto 1218c11776 Merge pull request #172 from MattSurabian/feature/ebs-builder-bug
builder/amazonebs: Validate that instance exists and is EBS based before trying to launch it
2013-07-11 14:59:02 -07:00
Matt Surabian f9decb7aca Validate that instance exists and is EBS based before trying to launch it. 2013-07-11 17:41:17 -04:00
Mitchell Hashimoto 8d2e32ea3b Merge pull request #170 from markpeek/markpeek-awsinstancedebugging
builder/amazonebs: Add AWS instance id to normal output
2013-07-11 13:32:49 -07:00
Mark Peek 7f5baf9ca4 Add AWS instance id to normal output
The AWS instance id is only output when logging is occurring. If an
error happens it is useful to know the AWS instance id of the instance
spun up by packer in case further cleanup or forensics are needed.
2013-07-11 20:27:44 +00:00
Mark Peek fb4a86ef35 Add ability to send log output to a file
Using PACKER_LOG=1 causes all the log output to be sent to Stderr. This
change maintains that backward compatility. Anything other than "1" will
be treated as a filename which will have logging appended to that file.
This is useful, for example, to always have debugging available without
cluttering up stdout (and without having to redirect stderr all the time).
2013-07-11 19:42:30 +00:00
Jack Pearkes 3461da5411 builder/digitalocean: use detected env variables for credentials 2013-07-11 11:31:09 +02:00
Brendan Hay fdc126afa7 Adding the ability to specify a security_group_id for the amazonebs builder 2013-07-10 09:32:50 +02:00
Mitchell Hashimoto d9c77c66f4 builder/vmware: error if shutdown command failed 2013-07-09 14:39:02 -07:00
Mitchell Hashimoto 8537d08caa scripts: add -e to build.sh again 2013-07-09 12:42:54 -07:00
Mitchell Hashimoto a5e5e689e0 Update CHANGELOG 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto 86ec808fe0 builder/virtualbox: paused is still running 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto e60945ed7b builder/virtualbox: remove floppy drive before exporting 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto c9dc998b0e website: document the floppy_files feature 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto efc9e9db6e builder/virtualbox: support floppy_files 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto eaa6c9fec1 packer: Only trim whitespace on the right of prefixed UI 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto 65b00775e8 builder/common: continue after creating floppy 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto 24fad7d443 script: build completes 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto cae89a49a1 builder/vmware: support floppy_files for mounting a floppy disk 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto 70f63492f9 builder/common: step to create floppy disks 2013-07-09 12:41:36 -07:00
Mitchell Hashimoto 13ed6c53b2 Merge pull request #157 from rvoicilas/master
Fix typo in packer.go
2013-07-08 23:37:18 -07:00
Radu Voicilas bd937ec8f2 Fix typo 2013-07-09 09:28:07 +03:00
Steven Merrill c80b1bb75d Don't allow a dirty Git tree to fail the make command. 2013-07-09 01:24:19 -04:00
Mitchell Hashimoto 207c15b91b packer: only show git commit in version output for prerelease 2013-07-08 15:38:14 -07:00
Mitchell Hashimoto 3b32ba9f11 scripts: Add git commit to builds 2013-07-08 15:37:01 -07:00
Mitchell Hashimoto d4e8321a80 Up version for dev 2013-07-08 15:17:23 -07:00
Mitchell Hashimoto 00d04d68d9 Merge pull request #150 from dregin/master
website: Just fixing a small error in the text.
2013-07-08 08:47:09 -07:00
Bernard McKeever 845349b5a9 Making sense of the sentence
The sentence was missing a few words.
I've made it read similar to the previous 3 sentences.
2013-07-08 11:09:16 +01:00
137 changed files with 4805 additions and 1978 deletions
+78
View File
@@ -1,3 +1,81 @@
## 0.2.1 (July 26, 2013)
FEATURES:
* New builder: `amazon-instance` can create instance-storage backed
AMIs.
* VMware builder now works with Workstation 9 on Linux.
IMPROVEMENTS:
* builder/amazon/all: Ctrl-C while waiting for state change works
* builder/amazon/ebs: Can now launch instances into a VPC for added protection [GH-210]
* builder/virtualbox,vmware: Add backspace, delete, and F1-F12 keys to the boot
command.
* builder/virtualbox: massive performance improvements with big ISO files because
an expensive copy is avoided. [GH-202]
* builder/vmware: CD is removed prior to exporting final machine. [GH-198]
BUG FIXES:
* builder/amazon/all: Gracefully handle when AMI appears to not exist
while AWS state is propogating. [GH-207]
* builder/virtualbox: Trim carriage returns for Windows to properly
detect VM state on Windows. [GH-218]
* core: build names no longer cause invalid config errors. [GH-197]
* command/build: If any builds fail, exit with non-zero exit status.
* communicator/ssh: SCP exit codes are tested and errors are reported. [GH-195]
* communicator/ssh: Properly change slash direction for Windows hosts. [GH-218]
## 0.2.0 (July 16, 2013)
BACKWARDS INCOMPATIBILITIES:
* "iso_md5" in the virtualbox and vmware builders is replaced with
"iso_checksum" and "iso_checksum_type" (with the latter set to "md5").
See the announce below on `packer fix` to automatically fix your templates.
FEATURES:
* **NEW COMMAND:** `packer fix` will attempt to fix templates from older
versions of Packer that are now broken due to backwards incompatibilities.
This command will fix the backwards incompatibilities introduced in this
version.
* Amazon EBS builder can now optionally use a pre-made security group
instead of randomly generating one.
* DigitalOcean API key and client IDs can now be passed in as
environmental variables. See the documentatin for more details.
* VirtualBox and VMware can now have `floppy_files` specified to attach
floppy disks when booting. This allows for unattended Windows installs.
* `packer build` has a new `-force` flag that forces the removal of
existing artifacts if they exist. [GH-173]
* You can now log to a file (instead of just stderr) by setting the
`PACKER_LOG_FILE` environmental variable. [GH-168]
* Checksums other than MD5 can now be used. SHA1 and SHA256 can also
be used. See the documentation on `iso_checksum_type` for more info. [GH-175]
IMPROVEMENTS:
* core: invalid keys in configuration are now considered validation
errors. [GH-104]
* core: all builders now share a common SSH connection core, improving
SSH reliability over all the builders.
* amazon-ebs: Credentials will come from IAM role if available. [GH-160]
* amazon-ebs: Verify the source AMI is EBS-backed before launching. [GH-169]
* shell provisioner: the build name and builder type are available in
the `PACKER_BUILD_NAME` and `PACKER_BUILDER_TYPE` env vars by default,
respectively. [GH-154]
* vmware: error if shutdown command has non-zero exit status.
BUG FIXES:
* core: UI messages are now properly prefixed with spaces again.
* core: If SSH connection ends, re-connection attempts will take
place. [GH-152]
* virtualbox: "paused" doesn't mean the VM is stopped, improving
shutdown detection.
* vmware: error if guest IP could not be detected. [GH-189]
## 0.1.5 (July 7, 2013)
FEATURES:
+24
View File
@@ -41,3 +41,27 @@ it raises the chances we can quickly merge or address your contributions.
linked.
5. The issue is closed.
## Setting up Go to work on Packer
If you have never worked with Go before, you will have to complete the
following steps in order to be able to compile and test Packer.
1. Install Go. On a Mac, you can `brew install go`.
2. Set and export the `GOPATH` environment variable. For example, you can
add `export GOPATH=$HOME/Documents/golang` to your `.bash_profile`.
3. Download the Packer source (and its dependencies) by running
`go get github.com/mitchellh/packer`. This will download the Packer
source to `$GOPATH/src/github.com/mitchellh/packer`.
4. Make your changes to the Packer source. You can run `make` from the main
source directory to recompile all the binaries. Any compilation errors
will be shown when the binaries are rebuilding.
5. Test your changes by running `make test` and then running
`$GOPATH/src/github.com/mitchellh/packer/bin/packer` to build a machine.
6. If everything works well and the tests pass, run `go fmt` on your code
before submitting a pull request.
+3
View File
@@ -10,6 +10,9 @@ all:
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./scripts/build.sh
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
format:
go fmt ./...
-7
View File
@@ -1,7 +0,0 @@
* builder/amazonebs: Copy AMI to multiple regions
* builder/vmware: VMX templates
* communicator/ssh: Ability to re-establish connection
* communicator/ssh: Download()
* packer: Communicator should have Close() method
* packer/plugin: Better error messages/detection if plugin crashes
* provisioner/shell: Arguments
+21
View File
@@ -0,0 +1,21 @@
package common
import (
"github.com/mitchellh/goamz/aws"
)
// AccessConfig is for common configuration related to AWS access
type AccessConfig struct {
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
}
// Auth returns a valid aws.Auth object for access to AWS services, or
// an error if the authentication couldn't be resolved.
func (c *AccessConfig) Auth() (aws.Auth, error) {
return aws.GetAuth(c.AccessKey, c.SecretKey)
}
func (c *AccessConfig) Prepare() []error {
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
// WaitForAMI waits for the given AMI ID to become ready.
func WaitForAMI(c *ec2.EC2, imageId string) error {
for {
imageResp, err := c.Images([]string{imageId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
log.Println("AMI not found, probably state issues on AWS side. Trying again.")
continue
}
return err
}
if imageResp.Images[0].State == "available" {
return nil
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
}
}
@@ -1,4 +1,4 @@
package amazonebs
package common
import (
"fmt"
@@ -8,35 +8,39 @@ import (
"strings"
)
type artifact struct {
// Artifact is an artifact implementation that contains built AMIs.
type Artifact struct {
// A map of regions to AMI IDs.
amis map[string]string
Amis map[string]string
// BuilderId is the unique ID for the builder that created this AMI
BuilderIdValue string
// EC2 connection for performing API stuff.
conn *ec2.EC2
Conn *ec2.EC2
}
func (*artifact) BuilderId() string {
return BuilderId
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*artifact) Files() []string {
func (*Artifact) Files() []string {
// We have no files
return nil
}
func (a *artifact) Id() string {
parts := make([]string, 0, len(a.amis))
for region, amiId := range a.amis {
func (a *Artifact) Id() string {
parts := make([]string, 0, len(a.Amis))
for region, amiId := range a.Amis {
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
}
return strings.Join(parts, ",")
}
func (a *artifact) String() string {
amiStrings := make([]string, 0, len(a.amis))
for region, id := range a.amis {
func (a *Artifact) String() string {
amiStrings := make([]string, 0, len(a.Amis))
for region, id := range a.Amis {
single := fmt.Sprintf("%s: %s", region, id)
amiStrings = append(amiStrings, single)
}
@@ -44,12 +48,12 @@ func (a *artifact) String() string {
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *artifact) Destroy() error {
func (a *Artifact) Destroy() error {
errors := make([]error, 0)
for _, imageId := range a.amis {
for _, imageId := range a.Amis {
log.Printf("Deregistering image ID: %s", imageId)
if _, err := a.conn.DeregisterImage(imageId); err != nil {
if _, err := a.Conn.DeregisterImage(imageId); err != nil {
errors = append(errors, err)
}
@@ -1,4 +1,4 @@
package amazonebs
package common
import (
"cgl.tideland.biz/asserts"
@@ -10,7 +10,7 @@ func TestArtifact_Impl(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
var actual packer.Artifact
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
assert.Implementor(&Artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
@@ -22,7 +22,10 @@ func TestArtifactId(t *testing.T) {
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{
Amis: amis,
}
result := a.Id()
assert.Equal(result, expected, "should match output")
}
@@ -39,7 +42,7 @@ west: bar`
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{Amis: amis}
result := a.String()
assert.Equal(result, expected, "should match output")
}
+55
View File
@@ -0,0 +1,55 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"log"
"time"
)
type StateChangeConf struct {
Conn *ec2.EC2
Instance *ec2.Instance
Pending []string
StepState map[string]interface{}
Target string
}
func WaitForState(conf *StateChangeConf) (i *ec2.Instance, err error) {
log.Printf("Waiting for instance state to become: %s", conf.Target)
i = conf.Instance
for i.State.Name != conf.Target {
if conf.StepState != nil {
if _, ok := conf.StepState[multistep.StateCancelled]; ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if i.State.Name == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, conf.Target)
return
}
var resp *ec2.InstancesResp
resp, err = conf.Conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return
}
i = &resp.Reservations[0].Instances[0]
time.Sleep(2 * time.Second)
}
return
}
+68
View File
@@ -0,0 +1,68 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"time"
)
// RunConfig contains configuration for running an instance from a source
// AMI and details on how to access that launched image.
type RunConfig struct {
Region string
SourceAmi string `mapstructure:"source_ami"`
InstanceType string `mapstructure:"instance_type"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
SubnetId string `mapstructure:"subnet_id"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare() []error {
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "1m"
}
// Validation
var err error
errs := make([]error, 0)
if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if c.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if c.Region == "" {
errs = append(errs, errors.New("A region must be specified"))
} else if _, ok := aws.Regions[c.Region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", c.Region))
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
+108
View File
@@ -0,0 +1,108 @@
package common
import (
"os"
"testing"
)
func init() {
// Clear out the AWS access key env vars so they don't
// affect our tests.
os.Setenv("AWS_ACCESS_KEY_ID", "")
os.Setenv("AWS_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_KEY", "")
}
func testConfig() *RunConfig {
return &RunConfig{
Region: "us-east-1",
SourceAmi: "abcd",
InstanceType: "m1.small",
SSHUsername: "root",
}
}
func TestRunConfigPrepare(t *testing.T) {
c := testConfig()
err := c.Prepare()
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_Region(t *testing.T) {
c := testConfig()
c.Region = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.Region = "us-east-12"
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.Region = "us-east-1"
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
c.SSHPort = 44
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
+45
View File
@@ -0,0 +1,45 @@
package common
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
var host string
instance := state["instance"].(*ec2.Instance)
if instance.VpcId != "" {
host = instance.PrivateIpAddress
} else {
host = instance.DNSName
}
return fmt.Sprintf("%s:%d", host, port), nil
}
}
// SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the instance created over SSH using the generated
// private key.
func SSHConfig(username string) func(map[string]interface{}) (*gossh.ClientConfig, error) {
return func(state map[string]interface{}) (*gossh.ClientConfig, error) {
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: username,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
}
@@ -1,4 +1,4 @@
package amazonebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,11 +10,11 @@ import (
"log"
)
type stepKeyPair struct {
type StepKeyPair struct {
keyName string
}
func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
@@ -23,9 +23,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := ec2conn.CreateKeyPair(keyName)
if err != nil {
err := fmt.Errorf("Error creating temporary keypair: %s", err)
state["error"] = err
ui.Error(err.Error())
state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err)
return multistep.ActionHalt
}
@@ -39,7 +37,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionContinue
}
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
func (s *StepKeyPair) Cleanup(state map[string]interface{}) {
// If no key name is set, then we never created it, so just return
if s.keyName == "" {
return
@@ -0,0 +1,105 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type StepRunSourceInstance struct {
ExpectedRootDevice string
InstanceType string
SourceAMI string
SubnetId string
instance *ec2.Instance
}
func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
keyName := state["keyPair"].(string)
securityGroupId := state["securityGroupId"].(string)
ui := state["ui"].(packer.Ui)
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
SubnetId: s.SubnetId,
}
ui.Say("Launching a source AWS instance...")
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
if err != nil {
state["error"] = fmt.Errorf("There was a problem with the source AMI: %s", err)
return multistep.ActionHalt
}
if s.ExpectedRootDevice != "" && imageResp.Images[0].RootDeviceType != s.ExpectedRootDevice {
state["error"] = fmt.Errorf(
"The provided source AMI has an invalid root device type.\n"+
"Expected '%s', got '%s'.",
s.ExpectedRootDevice, imageResp.Images[0].RootDeviceType)
return multistep.ActionHalt
}
runResp, err := ec2conn.RunInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source instance: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = &runResp.Instances[0]
log.Printf("instance id: %s", s.instance.InstanceId)
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
stateChange := StateChangeConf{
Conn: ec2conn,
Instance: s.instance,
Pending: []string{"pending"},
Target: "running",
StepState: state,
}
s.instance, err = WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["instance"] = s.instance
return multistep.ActionContinue
}
func (s *StepRunSourceInstance) Cleanup(state map[string]interface{}) {
if s.instance == nil {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
ui.Say("Terminating the source AWS instance...")
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Conn: ec2conn,
Instance: s.instance,
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "running",
}
WaitForState(&stateChange)
}
@@ -1,4 +1,4 @@
package amazonebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,34 +10,52 @@ import (
"log"
)
type stepSecurityGroup struct {
groupId string
type StepSecurityGroup struct {
SecurityGroupId string
SSHPort int
VpcId string
createdGroupId string
}
func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
func (s *StepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
if s.SecurityGroupId != "" {
log.Printf("Using specified security group: %s", s.SecurityGroupId)
state["securityGroupId"] = s.SecurityGroupId
return multistep.ActionContinue
}
if s.SSHPort == 0 {
panic("SSHPort must be set to a non-zero value.")
}
// Create the group
ui.Say("Creating temporary security group for this instance...")
groupName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("Temporary group name: %s", groupName)
groupResp, err := ec2conn.CreateSecurityGroup(groupName, "Temporary group for Packer")
group := ec2.SecurityGroup{
Name: groupName,
Description: "Temporary group for Packer",
VpcId: s.VpcId,
}
groupResp, err := ec2conn.CreateSecurityGroup(group)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the group ID so we can delete it later
s.groupId = groupResp.Id
s.createdGroupId = groupResp.Id
// Authorize the SSH access
perms := []ec2.IPPerm{
ec2.IPPerm{
Protocol: "tcp",
FromPort: config.SSHPort,
ToPort: config.SSHPort,
FromPort: s.SSHPort,
ToPort: s.SSHPort,
SourceIPs: []string{"0.0.0.0/0"},
},
}
@@ -51,13 +69,13 @@ func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepActi
}
// Set some state data for use in future steps
state["securityGroupId"] = s.groupId
state["securityGroupId"] = s.createdGroupId
return multistep.ActionContinue
}
func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.groupId == "" {
func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.createdGroupId == "" {
return
}
@@ -65,10 +83,10 @@ func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary security group...")
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
if err != nil {
log.Printf("Error deleting security group: %s", err)
ui.Error(fmt.Sprintf(
"Error cleaning up security group. Please delete the group manually: %s", s.groupId))
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
}
}
+150
View File
@@ -0,0 +1,150 @@
// The amazonebs package contains a packer.Builder implementation that
// builds AMIs for Amazon EC2.
//
// In general, there are two types of AMIs that can be created: ebs-backed or
// instance-store. This builder _only_ builds ebs-backed images.
package ebs
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazonebs"
type config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
}
type Builder struct {
config config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
// Accumulate any errors
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, ok := aws.Regions[b.config.Region]
if !ok {
panic("region not found")
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "ebs",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state["amis"].(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
+78
View File
@@ -0,0 +1,78 @@
package ebs
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
"secret_key": "bar",
"source_ami": "foo",
"instance_type": "foo",
"region": "us-east-1",
"ssh_username": "root",
"ami_name": "foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"access_key": []string{},
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
@@ -1,4 +1,4 @@
package amazonebs
package ebs
// This hook is fired prior to launching the EC2 instance.
const HookPreLaunch = "amazonebs_pre_launch"
@@ -1,12 +1,12 @@
package amazonebs
package ebs
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"log"
"strconv"
"text/template"
"time"
@@ -57,23 +57,11 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
for {
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error querying images: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
if imageResp.Images[0].State == "available" {
break
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
if err := awscommon.WaitForAMI(ec2conn, createResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
@@ -1,9 +1,10 @@
package amazonebs
package ebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
@@ -26,7 +27,14 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio
// Wait for the instance to actual stop
ui.Say("Waiting for the instance to stop...")
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
stateChange := awscommon.StateChangeConf{
Conn: ec2conn,
Instance: instance,
Pending: []string{"running", "stopping"},
Target: "stopped",
StepState: state,
}
instance, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
state["error"] = err
+219
View File
@@ -0,0 +1,219 @@
// The instance package contains a packer.Builder implementation that builds
// AMIs for Amazon EC2 backed by instance storage, as opposed to EBS storage.
package instance
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
"text/template"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazon.instance"
// 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.RunConfig `mapstructure:",squash"`
AccountId string `mapstructure:"account_id"`
AMIName string `mapstructure:"ami_name"`
BundleDestination string `mapstructure:"bundle_destination"`
BundlePrefix string `mapstructure:"bundle_prefix"`
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
if b.config.BundleDestination == "" {
b.config.BundleDestination = "/tmp"
}
if b.config.BundlePrefix == "" {
b.config.BundlePrefix = "image-{{.CreateTime}}"
}
if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--retry"
}
if b.config.BundleVolCommand == "" {
b.config.BundleVolCommand = "sudo -n ec2-bundle-vol " +
"-k {{.KeyPath}} " +
"-u {{.AccountId}} " +
"-c {{.CertPath}} " +
"-r {{.Architecture}} " +
"-e {{.PrivatePath}} " +
"-d {{.Destination}} " +
"-p {{.Prefix}} " +
"--batch"
}
if b.config.X509UploadPath == "" {
b.config.X509UploadPath = "/tmp"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
if b.config.AccountId == "" {
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
} else {
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
}
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if b.config.S3Bucket == "" {
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
}
if b.config.X509CertPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_cert_path is required"))
} else if _, err := os.Stat(b.config.X509CertPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_cert_path points to bad file: %s", err))
}
if b.config.X509KeyPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_key_path is required"))
} else if _, err := os.Stat(b.config.X509KeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_key_path points to bad file: %s", err))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, ok := aws.Regions[b.config.Region]
if !ok {
panic("region not found")
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = &b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&StepUploadX509Cert{},
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state["amis"].(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
+219
View File
@@ -0,0 +1,219 @@
package instance
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
func testConfig() map[string]interface{} {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
panic(err)
}
return map[string]interface{}{
"account_id": "foo",
"ami_name": "foo",
"instance_type": "m1.small",
"region": "us-east-1",
"s3_bucket": "foo",
"source_ami": "foo",
"ssh_username": "bob",
"x509_cert_path": tf.Name(),
"x509_key_path": tf.Name(),
"x509_upload_path": "/foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilderPrepare_AccountId(t *testing.T) {
b := &Builder{}
config := testConfig()
config["account_id"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["account_id"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
config["account_id"] = "0123-0456-7890"
err = b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.AccountId != "012304567890" {
t.Errorf("should strip hyphens: %s", b.config.AccountId)
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_BundleDestination(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_destination"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundleDestination != "/tmp" {
t.Fatalf("bad: %s", b.config.BundleDestination)
}
}
func TestBuilderPrepare_BundlePrefix(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_prefix"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
t.Fatalf("bad: %s", b.config.BundlePrefix)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_S3Bucket(t *testing.T) {
b := &Builder{}
config := testConfig()
config["s3_bucket"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["s3_bucket"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_X509CertPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_cert_path"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_cert_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_cert_path"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509KeyPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_key_path"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_key_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_key_path"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509UploadPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_upload_path"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
@@ -0,0 +1,104 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type bundleCmdData struct {
AccountId string
Architecture string
CertPath string
Destination string
KeyPath string
Prefix string
PrivatePath string
}
type bundlePrefixData struct {
CreateTime string
}
type StepBundleVolume struct{}
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
x509RemoteCertPath := state["x509RemoteCertPath"].(string)
x509RemoteKeyPath := state["x509RemoteKeyPath"].(string)
// Verify the AMI tools are available
ui.Say("Checking for EC2 AMI tools...")
cmd := &packer.RemoteCmd{Command: "ec2-ami-tools-version"}
if err := comm.Start(cmd); err != nil {
state["error"] = fmt.Errorf("Error checking for AMI tools: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
cmd.Wait()
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"The EC2 AMI tools could not be detected. These must be manually\n" +
"via a provisioner or some other means and are required for Packer\n" +
"to create an instance-store AMI.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Bundle the volume
var bundlePrefix bytes.Buffer
prefixTData := bundlePrefixData{
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
t.Execute(&bundlePrefix, prefixTData)
var bundleCmd bytes.Buffer
tData := bundleCmdData{
AccountId: config.AccountId,
Architecture: instance.Architecture,
CertPath: x509RemoteCertPath,
Destination: config.BundleDestination,
KeyPath: x509RemoteKeyPath,
Prefix: bundlePrefix.String(),
PrivatePath: config.X509UploadPath,
}
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
t.Execute(&bundleCmd, tData)
ui.Say("Bundling the volume...")
cmd = new(packer.RemoteCmd)
cmd.Command = bundleCmd.String()
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Volume bundling failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Store the manifest path
manifestName := bundlePrefix.String() + ".manifest.xml"
state["manifest_name"] = manifestName
state["manifest_path"] = fmt.Sprintf(
"%s/%s", config.BundleDestination, manifestName)
return multistep.ActionContinue
}
func (s *StepBundleVolume) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,68 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ec2conn := state["ec2"].(*ec2.EC2)
manifestPath := state["remote_manifest_path"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: amiName,
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state["error"] = fmt.Errorf("Error registering AMI: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Set the AMI ID in the state
ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId))
amis := make(map[string]string)
amis[config.Region] = registerResp.ImageId
state["amis"] = amis
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
if err := awscommon.WaitForAMI(ec2conn, registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,61 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"text/template"
)
type uploadCmdData struct {
AccessKey string
BucketName string
BundleDirectory string
ManifestPath string
SecretKey string
}
type StepUploadBundle struct{}
func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
manifestName := state["manifest_name"].(string)
manifestPath := state["manifest_path"].(string)
ui := state["ui"].(packer.Ui)
var uploadCmd bytes.Buffer
tData := uploadCmdData{
AccessKey: config.AccessKey,
BucketName: config.S3Bucket,
BundleDirectory: config.BundleDestination,
ManifestPath: manifestPath,
SecretKey: config.SecretKey,
}
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
t.Execute(&uploadCmd, tData)
ui.Say("Uploading the bundle...")
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Bundle upload failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["remote_manifest_path"] = fmt.Sprintf(
"%s/%s", config.S3Bucket, manifestName)
return multistep.ActionContinue
}
func (s *StepUploadBundle) Cleanup(state map[string]interface{}) {}
@@ -0,0 +1,49 @@
package instance
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
type StepUploadX509Cert struct{}
func (s *StepUploadX509Cert) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
ui := state["ui"].(packer.Ui)
x509RemoteCertPath := config.X509UploadPath + "/cert.pem"
x509RemoteKeyPath := config.X509UploadPath + "/key.pem"
ui.Say("Uploading X509 Certificate...")
if err := s.uploadSingle(comm, x509RemoteCertPath, config.X509CertPath); err != nil {
state["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if err := s.uploadSingle(comm, x509RemoteKeyPath, config.X509KeyPath); err != nil {
state["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["x509RemoteCertPath"] = x509RemoteCertPath
state["x509RemoteKeyPath"] = x509RemoteKeyPath
return multistep.ActionContinue
}
func (s *StepUploadX509Cert) Cleanup(map[string]interface{}) {}
func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
return comm.Upload(dst, f)
}
-199
View File
@@ -1,199 +0,0 @@
// The amazonebs package contains a packer.Builder implementation that
// builds AMIs for Amazon EC2.
//
// In general, there are two types of AMIs that can be created: ebs-backed or
// instance-store. This builder _only_ builds ebs-backed images.
package amazonebs
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"text/template"
"time"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazonebs"
type config struct {
// Access information
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
// Information for the source instance
Region string
SourceAmi string `mapstructure:"source_ami"`
InstanceType string `mapstructure:"instance_type"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SSHTimeout time.Duration
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
}
type Builder struct {
config config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
var err error
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
}
if b.config.AccessKey == "" {
b.config.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
}
if b.config.AccessKey == "" {
b.config.AccessKey = os.Getenv("AWS_ACCESS_KEY")
}
if b.config.SecretKey == "" {
b.config.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
if b.config.SecretKey == "" {
b.config.SecretKey = os.Getenv("AWS_SECRET_KEY")
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
b.config.RawSSHTimeout = "1m"
}
// Accumulate any errors
errs := make([]error, 0)
if b.config.AccessKey == "" {
errs = append(errs, errors.New("An access_key must be specified"))
}
if b.config.SecretKey == "" {
errs = append(errs, errors.New("A secret_key must be specified"))
}
if b.config.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if b.config.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if b.config.Region == "" {
errs = append(errs, errors.New("A region must be specified"))
} else if _, ok := aws.Regions[b.config.Region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", b.config.Region))
}
if b.config.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
if b.config.AMIName == "" {
errs = append(errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, ok := aws.Regions[b.config.Region]
if !ok {
panic("region not found")
}
auth := aws.Auth{b.config.AccessKey, b.config.SecretKey}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&stepKeyPair{},
&stepSecurityGroup{},
&stepRunSourceInstance{},
&stepConnectSSH{},
&stepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &artifact{
amis: state["amis"].(map[string]string),
conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
-324
View File
@@ -1,324 +0,0 @@
package amazonebs
import (
"github.com/mitchellh/packer/packer"
"os"
"testing"
)
func init() {
// Clear out the AWS access key env vars so they don't
// affect our tests.
os.Setenv("AWS_ACCESS_KEY_ID", "")
os.Setenv("AWS_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_KEY", "")
}
func testConfig() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
"secret_key": "bar",
"source_ami": "foo",
"instance_type": "foo",
"region": "us-east-1",
"ssh_username": "root",
"ami_name": "foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"access_key": []string{},
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_AccessKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["access_key"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.AccessKey != "foo" {
t.Errorf("access key invalid: %s", b.config.AccessKey)
}
// Test bad
delete(config, "access_key")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test env
delete(config, "access_key")
os.Setenv("AWS_ACCESS_KEY_ID", "foo")
defer os.Setenv("AWS_ACCESS_KEY_ID", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.AccessKey != "foo" {
t.Errorf("access key invalid: %s", b.config.AccessKey)
}
delete(config, "access_key")
os.Setenv("AWS_ACCESS_KEY", "foo")
defer os.Setenv("AWS_ACCESS_KEY", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.AccessKey != "foo" {
t.Errorf("access key invalid: %s", b.config.AccessKey)
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InstanceType(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["instance_type"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.InstanceType != "foo" {
t.Errorf("invalid: %s", b.config.InstanceType)
}
// Test bad
delete(config, "instance_type")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["region"] = "us-east-1"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != "us-east-1" {
t.Errorf("invalid: %s", b.config.Region)
}
// Test bad
delete(config, "region")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test invalid
config["region"] = "i-am-not-real"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SecretKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["secret_key"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SecretKey != "foo" {
t.Errorf("secret key invalid: %s", b.config.SecretKey)
}
// Test bad
delete(config, "secret_key")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test env
delete(config, "secret_key")
os.Setenv("AWS_SECRET_ACCESS_KEY", "foo")
defer os.Setenv("AWS_SECRET_ACCESS_KEY", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SecretKey != "foo" {
t.Errorf("access key invalid: %s", b.config.SecretKey)
}
delete(config, "secret_key")
os.Setenv("AWS_SECRET_KEY", "foo")
defer os.Setenv("AWS_SECRET_KEY", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SecretKey != "foo" {
t.Errorf("access key invalid: %s", b.config.SecretKey)
}
}
func TestBuilderPrepare_SourceAmi(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["source_ami"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SourceAmi != "foo" {
t.Errorf("invalid: %s", b.config.SourceAmi)
}
// Test bad
delete(config, "source_ami")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SSHPort(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 22 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
// Test set
config["ssh_port"] = 35
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 35 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
}
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
config["ssh_timeout"] = "this is not good"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["ssh_timeout"] = "5s"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHUsername(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ssh_username"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername)
}
// Test bad
delete(config, "ssh_username")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
-39
View File
@@ -1,39 +0,0 @@
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, pending []string, target string) (i *ec2.Instance, err error) {
log.Printf("Waiting for instance state to become: %s", target)
i = originalInstance
for i.State.Name != target {
found := false
for _, allowed := range pending {
if i.State.Name == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, target)
return
}
var resp *ec2.InstancesResp
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return
}
i = &resp.Reservations[0].Instances[0]
time.Sleep(2 * time.Second)
}
return
}
-151
View File
@@ -1,151 +0,0 @@
package amazonebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"net"
"time"
)
type stepConnectSSH struct {
cancel bool
conn net.Conn
}
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", config.SSHTimeout.String())
timeout := time.After(config.SSHTimeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
return multistep.ActionHalt
}
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
}
// This blocks until SSH becomes available, and sends the communicator
// on the given channel.
func (s *stepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(config)
instance := state["instance"].(*ec2.Instance)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
// Build the keyring for authentication. This stores the private key
// we'll use to authenticate.
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
var nc net.Conn
for {
if nc != nil {
nc.Close()
}
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// Attempt to connect to SSH port
log.Printf(
"Opening TCP conn for SSH to %s:%d",
instance.DNSName, config.SSHPort)
nc, err := net.Dial("tcp",
fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort))
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
}
// Build the actual SSH client configuration
sshConfig := &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}
sshConnectSuccess := make(chan bool, 1)
go func() {
comm, err = ssh.New(nc, sshConfig)
if err != nil {
log.Printf("SSH connection fail: %s", err)
sshConnectSuccess <- false
return
}
sshConnectSuccess <- true
}()
select {
case success := <-sshConnectSuccess:
if !success {
continue
}
case <-time.After(5 * time.Second):
log.Printf("SSH handshake timeout. Trying again.")
continue
}
ui.Say("Connected via SSH!")
break
}
// Store the connection so we can close it later
s.conn = nc
return comm, nil
}
-25
View File
@@ -1,25 +0,0 @@
package amazonebs
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepProvision struct{}
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*stepProvision) Cleanup(map[string]interface{}) {}
@@ -1,73 +0,0 @@
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepRunSourceInstance struct {
instance *ec2.Instance
}
func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ec2conn := state["ec2"].(*ec2.EC2)
keyName := state["keyPair"].(string)
securityGroupId := state["securityGroupId"].(string)
ui := state["ui"].(packer.Ui)
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: config.SourceAmi,
InstanceType: config.InstanceType,
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
}
ui.Say("Launching a source AWS instance...")
runResp, err := ec2conn.RunInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source instance: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = &runResp.Instances[0]
log.Printf("instance id: %s", s.instance.InstanceId)
ui.Say("Waiting for instance to become ready...")
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
if err != nil {
err := fmt.Errorf("Error waiting for instance to become ready: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["instance"] = s.instance
return multistep.ActionContinue
}
func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
if s.instance == nil {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
ui.Say("Terminating the source AWS instance...")
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
pending := []string{"pending", "running", "shutting-down", "stopped", "stopping"}
waitForState(ec2conn, s.instance, pending, "terminated")
}
+57
View File
@@ -0,0 +1,57 @@
package common
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/packer"
"sort"
"strings"
)
// CheckUnusedConfig is a helper that makes sure that the there are no
// unused configuration keys, properly ignoring keys that don't matter.
func CheckUnusedConfig(md *mapstructure.Metadata) *packer.MultiError {
errs := make([]error, 0)
if md.Unused != nil && len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
return nil
}
// DecodeConfig is a helper that handles decoding raw configuration using
// mapstructure. It returns the metadata and any errors that may happen.
// If you need extra configuration for mapstructure, you should configure
// it manually and not use this helper function.
func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metadata, error) {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: target,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
if err != nil {
return nil, err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return nil, err
}
}
return &md, nil
}
+63
View File
@@ -0,0 +1,63 @@
package common
import (
"github.com/mitchellh/mapstructure"
"reflect"
"testing"
)
func TestCheckUnusedConfig(t *testing.T) {
md := &mapstructure.Metadata{
Unused: make([]string, 0),
}
err := CheckUnusedConfig(md)
if err != nil {
t.Fatalf("err: %s", err)
}
md.Unused = []string{"foo", "bar"}
err = CheckUnusedConfig(md)
if err == nil {
t.Fatal("should have error")
}
}
func TestDecodeConfig(t *testing.T) {
type Local struct {
Foo string
Bar string
}
raws := []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"bar": "baz",
"baz": "what",
},
}
var result Local
md, err := DecodeConfig(&result, raws...)
if err != nil {
t.Fatalf("err: %s", err)
}
if result.Foo != "bar" {
t.Fatalf("invalid: %#v", result.Foo)
}
if result.Bar != "baz" {
t.Fatalf("invalid: %#v", result.Bar)
}
if md == nil {
t.Fatal("metadata should not be nil")
}
if !reflect.DeepEqual(md.Unused, []string{"baz"}) {
t.Fatalf("unused: %#v", md.Unused)
}
}
+18
View File
@@ -2,6 +2,9 @@ package common
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
@@ -46,6 +49,21 @@ type DownloadClient struct {
downloader Downloader
}
// HashForType returns the Hash implementation for the given string
// type, or nil if the type is not supported.
func HashForType(t string) hash.Hash {
switch t {
case "md5":
return md5.New()
case "sha1":
return sha1.New()
case "sha256":
return sha256.New()
default:
return nil
}
}
// NewDownloadClient returns a new DownloadClient for the given
// configuration.
func NewDownloadClient(c *DownloadConfig) *DownloadClient {
+45
View File
@@ -40,3 +40,48 @@ func TestDownloadClient_VerifyChecksum(t *testing.T) {
t.Fatal("didn't verify")
}
}
func TestHashForType(t *testing.T) {
if h := HashForType("md5"); h == nil {
t.Fatalf("md5 hash is nil")
} else {
h.Write([]byte("foo"))
result := h.Sum(nil)
expected := "acbd18db4cc2f85cedef654fccc4a4d8"
actual := hex.EncodeToString(result)
if actual != expected {
t.Fatalf("bad hash: %s", actual)
}
}
if h := HashForType("sha1"); h == nil {
t.Fatalf("sha1 hash is nil")
} else {
h.Write([]byte("foo"))
result := h.Sum(nil)
expected := "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
actual := hex.EncodeToString(result)
if actual != expected {
t.Fatalf("bad hash: %s", actual)
}
}
if h := HashForType("sha256"); h == nil {
t.Fatalf("sha256 hash is nil")
} else {
h.Write([]byte("foo"))
result := h.Sum(nil)
expected := "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
actual := hex.EncodeToString(result)
if actual != expected {
t.Fatalf("bad hash: %s", actual)
}
}
if HashForType("fake") != nil {
t.Fatalf("fake hash is not nil")
}
}
+10
View File
@@ -0,0 +1,10 @@
package common
// PackerConfig is a struct that contains the configuration keys that
// are sent by packer, properly tagged already so mapstructure can load
// them. Embed this structure into your configuration class to get it.
type PackerConfig struct {
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
}
+157
View File
@@ -0,0 +1,157 @@
package common
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"strings"
"time"
)
// StepConnectSSH is a multistep Step implementation that waits for SSH
// to become available. It gets the connection information from a single
// configuration when creating the step.
//
// Uses:
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
type StepConnectSSH struct {
// SSHAddress is a function that returns the TCP address to connect to
// for SSH. This is a function so that you can query information
// if necessary for this address.
SSHAddress func(map[string]interface{}) (string, error)
// SSHConfig is a function that returns the proper client configuration
// for SSH access.
SSHConfig func(map[string]interface{}) (*gossh.ClientConfig, error)
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
cancel bool
comm packer.Communicator
}
func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for SSH to become available...")
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout)
timeout := time.After(s.SSHWaitTimeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
return multistep.ActionHalt
}
ui.Say("Connected to SSH!")
s.comm = comm
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
// The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process.
s.cancel = true
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepConnectSSH) Cleanup(map[string]interface{}) {
}
func (s *StepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
handshakeAttempts := 0
var comm packer.Communicator
for {
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// First we request the TCP connection information
address, err := s.SSHAddress(state)
if err != nil {
log.Printf("Error getting SSH address: %s", err)
continue
}
// Retrieve the SSH configuration
sshConfig, err := s.SSHConfig(state)
if err != nil {
log.Printf("Error getting SSH config: %s", err)
continue
}
// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address, 5*time.Minute)
nc, err := connFunc()
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
}
nc.Close()
// Then we attempt to connect via SSH
config := &ssh.Config{
Connection: connFunc,
SSHConfig: sshConfig,
}
comm, err = ssh.New(config)
if err != nil {
log.Printf("SSH handshake err: %s", err)
// Only count this as an attempt if we were able to attempt
// to authenticate. Note this is very brittle since it depends
// on the string of the error... but I don't see any other way.
if strings.Contains(err.Error(), "authenticate") {
log.Printf("Detected authentication error. Increasing handshake attempts.")
handshakeAttempts += 1
}
if handshakeAttempts < 10 {
// Try to connect via SSH a handful of times
continue
}
return nil, err
}
break
}
return comm, nil
}
+14
View File
@@ -0,0 +1,14 @@
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepConnectSSH_Impl(t *testing.T) {
var raw interface{}
raw = new(StepConnectSSH)
if _, ok := raw.(multistep.Step); !ok {
t.Fatalf("connect ssh should be a step")
}
}
+135
View File
@@ -0,0 +1,135 @@
package common
import (
"fmt"
"github.com/mitchellh/go-fs"
"github.com/mitchellh/go-fs/fat"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
// StepCreateFloppy will create a floppy disk with the given files.
// The floppy disk doesn't support sub-directories. Only files at the
// root level are supported.
type StepCreateFloppy struct {
Files []string
floppyPath string
}
func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepAction {
if len(s.Files) == 0 {
log.Println("No floppy files specified. Floppy disk will not be made.")
return multistep.ActionContinue
}
ui := state["ui"].(packer.Ui)
ui.Say("Creating floppy disk...")
// Create a temporary file to be our floppy drive
floppyF, err := ioutil.TempFile("", "packer")
if err != nil {
state["error"] = fmt.Errorf("Error creating temporary file for floppy: %s", err)
return multistep.ActionHalt
}
defer floppyF.Close()
// Set the path so we can remove it later
s.floppyPath = floppyF.Name()
log.Printf("Floppy path: %s", floppyF.Name())
// Set the size of the file to be a floppy sized
if err := floppyF.Truncate(1440 * 1024); err != nil {
state["error"] = fmt.Errorf("Error creating floppy: %s", err)
return multistep.ActionHalt
}
// BlockDevice backed by the file for our filesystem
log.Println("Initializing block device backed by temporary file")
device, err := fs.NewFileDisk(floppyF)
if err != nil {
state["error"] = fmt.Errorf("Error creating floppy: %s", err)
return multistep.ActionHalt
}
// Format the block device so it contains a valid FAT filesystem
log.Println("Formatting the block device with a FAT filesystem...")
formatConfig := &fat.SuperFloppyConfig{
FATType: fat.FAT12,
Label: "packer",
OEMName: "packer",
}
if fat.FormatSuperFloppy(device, formatConfig); err != nil {
state["error"] = fmt.Errorf("Error creating floppy: %s", err)
return multistep.ActionHalt
}
// The actual FAT filesystem
log.Println("Initializing FAT filesystem on block device")
fatFs, err := fat.New(device)
if err != nil {
state["error"] = fmt.Errorf("Error creating floppy: %s", err)
return multistep.ActionHalt
}
// Get the root directory to the filesystem
log.Println("Reading the root directory from the filesystem")
rootDir, err := fatFs.RootDir()
if err != nil {
state["error"] = fmt.Errorf("Error creating floppy: %s", err)
return multistep.ActionHalt
}
// Go over each file and copy it.
for _, filename := range s.Files {
ui.Message(fmt.Sprintf("Copying: %s", filepath.Base(filename)))
if s.addSingleFile(rootDir, filename); err != nil {
state["error"] = fmt.Errorf("Error adding file to floppy: %s", err)
return multistep.ActionHalt
}
}
// Set the path to the floppy so it can be used later
state["floppy_path"] = s.floppyPath
return multistep.ActionContinue
}
func (s *StepCreateFloppy) Cleanup(map[string]interface{}) {
if s.floppyPath != "" {
log.Printf("Deleting floppy disk: %s", s.floppyPath)
os.Remove(s.floppyPath)
}
}
func (s *StepCreateFloppy) addSingleFile(dir fs.Directory, src string) error {
log.Printf("Adding file to floppy: %s", src)
inputF, err := os.Open(src)
if err != nil {
return err
}
defer inputF.Close()
entry, err := dir.AddFile(filepath.Base(src))
if err != nil {
return err
}
fatFile, err := entry.File()
if err != nil {
return err
}
if _, err := io.Copy(fatFile, inputF); err != nil {
return err
}
return nil
}
@@ -1,4 +1,4 @@
package digitalocean
package common
import (
"github.com/mitchellh/multistep"
@@ -6,9 +6,18 @@ import (
"log"
)
type stepProvision struct{}
// StepProvision runs the provisioners.
//
// Uses:
// communicator packer.Communicator
// hook packer.Hook
// ui packer.Ui
//
// Produces:
// <nothing>
type StepProvision struct{}
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
func (*StepProvision) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui)
@@ -22,4 +31,4 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionContinue
}
func (*stepProvision) Cleanup(map[string]interface{}) {}
func (*StepProvision) Cleanup(map[string]interface{}) {}
+14
View File
@@ -0,0 +1,14 @@
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepProvision_Impl(t *testing.T) {
var raw interface{}
raw = new(StepProvision)
if _, ok := raw.(multistep.Step); !ok {
t.Fatalf("provision should be a step")
}
}
+48 -29
View File
@@ -7,11 +7,11 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strconv"
"text/template"
"time"
@@ -28,6 +28,8 @@ type snapshotNameData struct {
// to use while communicating with DO and describes the image
// you are creating
type config struct {
common.PackerConfig `mapstructure:",squash"`
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
RegionID uint `mapstructure:"region_id"`
@@ -37,16 +39,17 @@ type config struct {
SnapshotName string
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
SSHTimeout time.Duration
EventDelay time.Duration
StateTimeout time.Duration
PackerDebug bool `mapstructure:"packer_debug"`
RawSnapshotName string `mapstructure:"snapshot_name"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawEventDelay string `mapstructure:"event_delay"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
eventDelay time.Duration
stateTimeout time.Duration
}
type Builder struct {
@@ -55,15 +58,25 @@ type Builder struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
// Optional configuration with defaults
//
if b.config.APIKey == "" {
// Default to environment variable for api_key, if it exists
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
}
if b.config.ClientID == "" {
// Default to environment variable for client_id, if it exists
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
}
if b.config.RegionID == 0 {
// Default to Region "New York"
b.config.RegionID = 1
@@ -112,36 +125,37 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.RawStateTimeout = "6m"
}
// A list of errors on the configuration
errs := make([]error, 0)
// Required configurations that will display errors if not set
//
if b.config.ClientID == "" {
errs = append(errs, errors.New("a client_id must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id must be specified"))
}
if b.config.APIKey == "" {
errs = append(errs, errors.New("an api_key must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.SSHTimeout = sshTimeout
b.config.sshTimeout = sshTimeout
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing event_delay: %s", err))
}
b.config.EventDelay = eventDelay
b.config.eventDelay = eventDelay
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.StateTimeout = stateTimeout
b.config.stateTimeout = stateTimeout
// Parse the name of the snapshot
snapNameBuf := new(bytes.Buffer)
@@ -150,14 +164,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
} else {
t.Execute(snapNameBuf, tData)
b.config.SnapshotName = snapNameBuf.String()
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
@@ -180,8 +195,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepCreateSSHKey),
new(stepCreateDroplet),
new(stepDropletInfo),
new(stepConnectSSH),
new(stepProvision),
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
},
new(common.StepProvision),
new(stepPowerOff),
new(stepSnapshot),
}
+37
View File
@@ -2,10 +2,17 @@ package digitalocean
import (
"github.com/mitchellh/packer/packer"
"os"
"strconv"
"testing"
)
func init() {
// Clear out the credential env vars
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
func testConfig() map[string]interface{} {
return map[string]interface{}{
"client_id": "foo",
@@ -55,6 +62,15 @@ func TestBuilderPrepare_APIKey(t *testing.T) {
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "api_key")
os.Setenv("DIGITALOCEAN_API_KEY", "foo")
defer os.Setenv("DIGITALOCEAN_API_KEY", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ClientID(t *testing.T) {
@@ -79,6 +95,27 @@ func TestBuilderPrepare_ClientID(t *testing.T) {
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "client_id")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo")
defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_RegionID(t *testing.T) {
+30
View File
@@ -0,0 +1,30 @@
package digitalocean
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(config)
ipAddress := state["droplet_ip"].(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
-137
View File
@@ -1,137 +0,0 @@
package digitalocean
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"net"
"time"
)
type stepConnectSSH struct {
conn net.Conn
}
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
ipAddress := state["droplet_ip"]
// Build the keyring for authentication. This stores the private key
// we'll use to authenticate.
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
err := fmt.Errorf("Error setting up SSH config: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Build the actual SSH client configuration
sshConfig := &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}
// Start trying to connect to SSH
connected := make(chan error, 1)
connectQuit := make(chan bool, 1)
defer func() {
connectQuit <- true
}()
var comm packer.Communicator
go func() {
var err error
ui.Say("Connecting to the droplet via SSH...")
attempts := 0
handshakeAttempts := 0
for {
select {
case <-connectQuit:
return
default:
}
attempts += 1
log.Printf(
"Opening TCP conn for SSH to %s:%d (attempt %d)",
ipAddress, config.SSHPort, attempts)
s.conn, err = net.DialTimeout(
"tcp",
fmt.Sprintf("%s:%d", ipAddress, config.SSHPort),
10*time.Second)
if err == nil {
log.Println("TCP connection made. Attempting SSH handshake.")
comm, err = ssh.New(s.conn, sshConfig)
if err == nil {
log.Println("Connected to SSH!")
break
}
handshakeAttempts += 1
log.Printf("SSH handshake error: %s", err)
if handshakeAttempts > 5 {
connected <- err
return
}
}
// A brief sleep so we're not being overly zealous attempting
// to connect to the instance.
time.Sleep(500 * time.Millisecond)
}
connected <- nil
}()
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
timeout := time.After(config.SSHTimeout)
ConnectWaitLoop:
for {
select {
case err := <-connected:
if err != nil {
err := fmt.Errorf("Error connecting to SSH: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// We connected. Just break the loop.
break ConnectWaitLoop
case <-timeout:
err := errors.New("Timeout waiting for SSH to become available.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
// Set the communicator on the state bag so it can be used later
state["communicator"] = comm
return multistep.ActionContinue
}
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
if s.conn != nil {
s.conn.Close()
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) {
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.EventDelay)
time.Sleep(c.eventDelay)
err := client.DestroyDroplet(s.dropletId)
+1 -1
View File
@@ -20,7 +20,7 @@ func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.EventDelay)
time.Sleep(c.eventDelay)
// Poweroff the droplet so it can be snapshot
err := client.PowerOffDroplet(dropletId)
+1 -3
View File
@@ -17,9 +17,7 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId)
if err != nil {
log.Println(err)
break
@@ -37,7 +35,7 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
}()
log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState)
timeout := time.After(c.StateTimeout)
timeout := time.After(c.stateTimeout)
ActiveWaitLoop:
for {
+106 -65
View File
@@ -3,7 +3,6 @@ package virtualbox
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
@@ -25,54 +24,59 @@ type Builder struct {
}
type config struct {
BootCommand []string `mapstructure:"boot_command"`
BootWait time.Duration ``
DiskSize uint `mapstructure:"disk_size"`
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
GuestOSType string `mapstructure:"guest_os_type"`
Headless bool `mapstructure:"headless"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"`
ShutdownCommand string `mapstructure:"shutdown_command"`
ShutdownTimeout time.Duration ``
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHWaitTimeout time.Duration ``
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
VBoxManage [][]string `mapstructure:"vboxmanage"`
VMName string `mapstructure:"vm_name"`
common.PackerConfig `mapstructure:",squash"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
BootCommand []string `mapstructure:"boot_command"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
GuestOSType string `mapstructure:"guest_os_type"`
Headless bool `mapstructure:"headless"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"`
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
VBoxManage [][]string `mapstructure:"vboxmanage"`
VMName string `mapstructure:"vm_name"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
}
func (b *Builder) Prepare(raws ...interface{}) error {
var err error
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
}
if b.config.FloppyFiles == nil {
b.config.FloppyFiles = make([]string, 0)
}
if b.config.GuestAdditionsPath == "" {
b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
@@ -121,24 +125,38 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
errs := make([]error, 0)
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOMD5 == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_md5 is required"))
if b.config.ISOChecksum == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOMD5 = strings.ToLower(b.config.ISOMD5)
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -146,7 +164,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -161,12 +180,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
}
@@ -179,7 +199,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if b.config.GuestAdditionsURL != "" {
url, err := url.Parse(b.config.GuestAdditionsURL)
if err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -187,7 +208,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -202,24 +224,30 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.GuestAdditionsURL = url.String()
}
}
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
b.config.BootWait, err = time.ParseDuration(b.config.RawBootWait)
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
if b.config.RawShutdownTimeout == "" {
@@ -230,31 +258,36 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.RawSSHWaitTimeout = "20m"
}
b.config.ShutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
b.config.SSHWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
b.driver, err = b.newDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -265,19 +298,27 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepDownloadGuestAdditions),
new(stepDownloadISO),
new(stepPrepareOutputDir),
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
},
new(stepHTTPServer),
new(stepSuppressMessages),
new(stepCreateVM),
new(stepCreateDisk),
new(stepAttachISO),
new(stepAttachFloppy),
new(stepForwardSSH),
new(stepVBoxManage),
new(stepRun),
new(stepTypeBootCommand),
new(stepWaitForSSH),
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
},
new(stepUploadVersion),
new(stepUploadGuestAdditions),
new(stepProvision),
new(common.StepProvision),
new(stepShutdown),
new(stepExport),
}
+78 -8
View File
@@ -10,9 +10,10 @@ import (
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_md5": "foo",
"iso_url": "http://www.google.com/",
"ssh_username": "foo",
"iso_checksum": "foo",
"iso_checksum_type": "md5",
"iso_url": "http://www.google.com/",
"ssh_username": "foo",
packer.BuildNameConfigKey: "foo",
}
@@ -115,6 +116,33 @@ func TestBuilderPrepare_DiskSize(t *testing.T) {
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "floppy_files")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if len(b.config.FloppyFiles) != 0 {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
config["floppy_files"] = []string{"foo", "bar"}
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := []string{"foo", "bar"}
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
}
func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) {
var b Builder
config := testConfig()
@@ -244,26 +272,68 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
}
}
func TestBuilderPrepare_ISOMD5(t *testing.T) {
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_ISOChecksum(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_md5"] = ""
config["iso_checksum"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_md5"] = "FOo"
config["iso_checksum"] = "FOo"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOMD5 != "foo" {
t.Fatalf("should've lowercased: %s", b.config.ISOMD5)
if b.config.ISOChecksum != "foo" {
t.Fatalf("should've lowercased: %s", b.config.ISOChecksum)
}
}
func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_checksum_type"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_checksum_type"] = "mD5"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOChecksumType != "md5" {
t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType)
}
// Test unknown
config["iso_checksum_type"] = "fake"
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
+9
View File
@@ -50,6 +50,9 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
}
for _, line := range strings.Split(stdout.String(), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
if line == `VMState="running"` {
return true, nil
}
@@ -59,6 +62,12 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
if line == `VMState="stopping"` {
return true, nil
}
// We consider "paused" to still be running. We wait for it to
// be completely stopped or some other state.
if line == `VMState="paused"` {
return true, nil
}
}
return false, nil
+25
View File
@@ -0,0 +1,25 @@
package virtualbox
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
sshHostPort := state["sshHostPort"].(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}, nil
}
+128
View File
@@ -0,0 +1,128 @@
package virtualbox
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
// This step attaches the ISO to the virtual machine.
//
// Uses:
//
// Produces:
type stepAttachFloppy struct {
floppyPath string
}
func (s *stepAttachFloppy) Run(state map[string]interface{}) multistep.StepAction {
// Determine if we even have a floppy disk to attach
var floppyPath string
if floppyPathRaw, ok := state["floppy_path"]; ok {
floppyPath = floppyPathRaw.(string)
} else {
log.Println("No floppy disk, not attaching.")
return multistep.ActionContinue
}
// VirtualBox is really dumb and can't figure out the format of the file
// without an extension, so we need to add the "vfd" extension to the
// floppy.
floppyPath, err := s.copyFloppy(floppyPath)
if err != nil {
state["error"] = fmt.Errorf("Error preparing floppy: %s", err)
return multistep.ActionHalt
}
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
vmName := state["vmName"].(string)
ui.Say("Attaching floppy disk...")
// Create the floppy disk controller
command := []string{
"storagectl", vmName,
"--name", "Floppy Controller",
"--add", "floppy",
}
if err := driver.VBoxManage(command...); err != nil {
state["error"] = fmt.Errorf("Error creating floppy controller: %s", err)
return multistep.ActionHalt
}
// Attach the floppy to the controller
command = []string{
"storageattach", vmName,
"--storagectl", "Floppy Controller",
"--port", "0",
"--device", "0",
"--type", "fdd",
"--medium", floppyPath,
}
if err := driver.VBoxManage(command...); err != nil {
state["error"] = fmt.Errorf("Error attaching floppy: %s", err)
return multistep.ActionHalt
}
// Track the path so that we can unregister it from VirtualBox later
s.floppyPath = floppyPath
return multistep.ActionContinue
}
func (s *stepAttachFloppy) Cleanup(state map[string]interface{}) {
if s.floppyPath == "" {
return
}
// Delete the floppy disk
defer os.Remove(s.floppyPath)
driver := state["driver"].(Driver)
vmName := state["vmName"].(string)
command := []string{
"storageattach", vmName,
"--storagectl", "Floppy Controller",
"--port", "0",
"--device", "0",
"--medium", "none",
}
if err := driver.VBoxManage(command...); err != nil {
log.Printf("Error unregistering floppy: %s", err)
}
}
func (s *stepAttachFloppy) copyFloppy(path string) (string, error) {
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
return "", err
}
floppyPath := filepath.Join(tempdir, "floppy.vfd")
f, err := os.Create(floppyPath)
if err != nil {
return "", err
}
defer f.Close()
sourceF, err := os.Open(path)
if err != nil {
return "", err
}
defer sourceF.Close()
log.Printf("Copying floppy to temp location: %s", floppyPath)
if _, err := io.Copy(f, sourceF); err != nil {
return "", err
}
return floppyPath, nil
}
+6 -47
View File
@@ -1,17 +1,12 @@
package virtualbox
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
@@ -24,16 +19,14 @@ import (
//
// Produces:
// iso_path string
type stepDownloadISO struct {
isoCopyDir string
}
type stepDownloadISO struct{}
func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
checksum, err := hex.DecodeString(config.ISOMD5)
checksum, err := hex.DecodeString(config.ISOChecksum)
if err != nil {
state["error"] = fmt.Errorf("Error parsing checksum: %s", err)
return multistep.ActionHalt
@@ -47,7 +40,7 @@ func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction
Url: config.ISOUrl,
TargetPath: cachePath,
CopyFile: false,
Hash: md5.New(),
Hash: common.HashForType(config.ISOChecksumType),
Checksum: checksum,
}
@@ -85,44 +78,10 @@ DownloadWaitLoop:
}
}
// VirtualBox is really dumb and can't figure out that the file is an
// ISO unless it has a ".iso" extension. We can't modify the cache
// filenames so we just do a copy.
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
s.isoCopyDir = tempdir
f, err := os.Create(filepath.Join(tempdir, "image.iso"))
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer f.Close()
sourceF, err := os.Open(cachePath)
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer sourceF.Close()
log.Printf("Copying ISO to temp location: %s", tempdir)
if _, err := io.Copy(f, sourceF); err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = f.Name()
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (s *stepDownloadISO) Cleanup(map[string]interface{}) {
if s.isoCopyDir != "" {
os.RemoveAll(s.isoCopyDir)
}
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
+19 -1
View File
@@ -22,7 +22,8 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
vmName := state["vmName"].(string)
// Clear out the Packer-created forwarding rule
ui.Say(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"]))
ui.Say("Preparing to export machine...")
ui.Message(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"]))
command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error deleting port forwarding rule: %s", err)
@@ -31,6 +32,23 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionHalt
}
// Remove the attached floppy disk, if it exists
if _, ok := state["floppy_path"]; ok {
ui.Message("Removing floppy drive...")
command := []string{
"storageattach", vmName,
"--storagectl", "Floppy Controller",
"--port", "0",
"--device", "0",
"--medium", "none",
}
if err := driver.VBoxManage(command...); err != nil {
state["error"] = fmt.Errorf("Error removing floppy: %s", err)
return multistep.ActionHalt
}
}
// Export the VM to an OVF
outputPath := filepath.Join(config.OutputDir, "packer.ovf")
@@ -10,6 +10,12 @@ type stepPrepareOutputDir struct{}
func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
ui.Say("Deleting previous output directory...")
os.RemoveAll(config.OutputDir)
}
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state["error"] = err
-25
View File
@@ -1,25 +0,0 @@
package virtualbox
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepProvision struct{}
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*stepProvision) Cleanup(map[string]interface{}) {}
+3 -3
View File
@@ -40,9 +40,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
s.vmName = vmName
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait))
time.Sleep(config.BootWait)
if int64(config.bootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait))
time.Sleep(config.bootWait)
}
return multistep.ActionContinue
+3 -6
View File
@@ -34,19 +34,16 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
if err := cmd.StartWithUi(comm, ui); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the command to run
cmd.Wait()
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.ShutdownTimeout)
shutdownTimer := time.After(config.ShutdownTimeout)
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
shutdownTimer := time.After(config.shutdownTimeout)
for {
running, _ := driver.IsRunning(vmName)
if !running {
@@ -91,8 +91,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func scancodes(message string) []string {
special := make(map[string][]string)
special["<bs>"] = []string{"ff", "08"}
special["<del>"] = []string{"ff", "ff"}
special["<enter>"] = []string{"1c", "9c"}
special["<esc>"] = []string{"01", "81"}
special["<f1>"] = []string{"ff", "be"}
special["<f2>"] = []string{"ff", "bf"}
special["<f3>"] = []string{"ff", "c0"}
special["<f4>"] = []string{"ff", "c1"}
special["<f5>"] = []string{"ff", "c2"}
special["<f6>"] = []string{"ff", "c3"}
special["<f7>"] = []string{"ff", "c4"}
special["<f8>"] = []string{"ff", "c5"}
special["<f9>"] = []string{"ff", "c6"}
special["<f10>"] = []string{"ff", "c7"}
special["<f11>"] = []string{"ff", "c8"}
special["<f12>"] = []string{"ff", "c9"}
special["<return>"] = []string{"1c", "9c"}
special["<tab>"] = []string{"0f", "8f"}
-149
View File
@@ -1,149 +0,0 @@
package virtualbox
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"net"
"time"
)
// This step waits for SSH to become available and establishes an SSH
// connection.
//
// Uses:
// config *config
// sshHostPort uint
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
type stepWaitForSSH struct {
cancel bool
conn net.Conn
}
func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", config.SSHWaitTimeout.String())
timeout := time.After(config.SSHWaitTimeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
return multistep.ActionHalt
}
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepWaitForSSH) Cleanup(map[string]interface{}) {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
}
// This blocks until SSH becomes available, and sends the communicator
// on the given channel.
func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
sshHostPort := state["sshHostPort"].(uint)
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
var nc net.Conn
for {
if nc != nil {
nc.Close()
}
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// Attempt to connect to SSH port
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
}
// Then we attempt to connect via SSH
sshConfig := &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}
sshConnectSuccess := make(chan bool, 1)
go func() {
comm, err = ssh.New(nc, sshConfig)
if err != nil {
log.Printf("SSH connection fail: %s", err)
sshConnectSuccess <- false
return
}
sshConnectSuccess <- true
}()
select {
case success := <-sshConnectSuccess:
if !success {
continue
}
case <-time.After(5 * time.Second):
log.Printf("SSH handshake timeout. Trying again.")
continue
}
ui.Say("Connected via SSH!")
break
}
// Store the connection so we can close it later
s.conn = nc
return comm, nil
}
+82 -53
View File
@@ -3,7 +3,6 @@ package vmware
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
@@ -26,10 +25,14 @@ type Builder struct {
}
type config struct {
common.PackerConfig `mapstructure:",squash"`
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
GuestOSType string `mapstructure:"guest_os_type"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrl string `mapstructure:"iso_url"`
VMName string `mapstructure:"vm_name"`
OutputDir string `mapstructure:"output_directory"`
@@ -38,36 +41,35 @@ type config struct {
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
BootCommand []string `mapstructure:"boot_command"`
BootWait time.Duration ``
SkipCompaction bool `mapstructure:"skip_compaction"`
ShutdownCommand string `mapstructure:"shutdown_command"`
ShutdownTimeout time.Duration ``
SSHUser string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHWaitTimeout time.Duration ``
ToolsUploadFlavor string `mapstructure:"tools_upload_flavor"`
ToolsUploadPath string `mapstructure:"tools_upload_path"`
VMXData map[string]string `mapstructure:"vmx_data"`
VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
}
func (b *Builder) Prepare(raws ...interface{}) error {
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
if b.config.DiskName == "" {
b.config.DiskName = "disk"
}
@@ -76,6 +78,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.DiskSize = 40000
}
if b.config.FloppyFiles == nil {
b.config.FloppyFiles = make([]string, 0)
}
if b.config.GuestOSType == "" {
b.config.GuestOSType = "other"
}
@@ -116,26 +122,38 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.ToolsUploadPath = "{{ .Flavor }}.iso"
}
// Accumulate any errors
var err error
errs := make([]error, 0)
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOMD5 == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_md5 is required"))
if b.config.ISOChecksum == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOMD5 = strings.ToLower(b.config.ISOMD5)
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -143,7 +161,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -158,29 +177,36 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
}
}
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
if b.config.RawBootWait != "" {
b.config.BootWait, err = time.ParseDuration(b.config.RawBootWait)
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
}
@@ -188,35 +214,40 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.RawShutdownTimeout = "5m"
}
b.config.ShutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.RawSSHWaitTimeout == "" {
b.config.RawSSHWaitTimeout = "20m"
}
b.config.SSHWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
if _, err := template.New("path").Parse(b.config.ToolsUploadPath); err != nil {
errs = append(errs, fmt.Errorf("tools_upload_path invalid: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("tools_upload_path invalid: %s", err))
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
b.driver, err = b.newDriver()
b.driver, err = NewDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VMware driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VMware driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -230,17 +261,25 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepPrepareTools{},
&stepDownloadISO{},
&stepPrepareOutputDir{},
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
},
&stepCreateDisk{},
&stepCreateVMX{},
&stepHTTPServer{},
&stepConfigureVNC{},
&stepRun{},
&stepTypeBootCommand{},
&stepWaitForSSH{},
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
},
&stepUploadTools{},
&stepProvision{},
&common.StepProvision{},
&stepShutdown{},
&stepCleanFiles{},
&stepCleanVMX{},
&stepCompactDisk{},
}
@@ -305,13 +344,3 @@ func (b *Builder) Cancel() {
b.runner.Cancel()
}
}
func (b *Builder) newDriver() (Driver, error) {
fusionAppPath := "/Applications/VMware Fusion.app"
driver := &Fusion5Driver{fusionAppPath}
if err := driver.Verify(); err != nil {
return nil, err
}
return driver, nil
}
+89 -19
View File
@@ -4,15 +4,17 @@ import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"reflect"
"testing"
"time"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_md5": "foo",
"iso_url": "http://www.packer.io",
"ssh_username": "foo",
"iso_checksum": "foo",
"iso_checksum_type": "md5",
"iso_url": "http://www.packer.io",
"ssh_username": "foo",
packer.BuildNameConfigKey: "foo",
}
@@ -56,6 +58,58 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
}
}
func TestBuilderPrepare_ISOChecksum(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_checksum"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_checksum"] = "FOo"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOChecksum != "foo" {
t.Fatalf("should've lowercased: %s", b.config.ISOChecksum)
}
}
func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_checksum_type"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_checksum_type"] = "mD5"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOChecksumType != "md5" {
t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType)
}
// Test unknown
config["iso_checksum_type"] = "fake"
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Defaults(t *testing.T) {
var b Builder
config := testConfig()
@@ -72,8 +126,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad output dir: %s", b.config.OutputDir)
}
if b.config.SSHWaitTimeout != (20 * time.Minute) {
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
if b.config.sshWaitTimeout != (20 * time.Minute) {
t.Errorf("bad wait timeout: %s", b.config.sshWaitTimeout)
}
if b.config.VMName != "packer-foo" {
@@ -107,6 +161,33 @@ func TestBuilderPrepare_DiskSize(t *testing.T) {
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "floppy_files")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if len(b.config.FloppyFiles) != 0 {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
config["floppy_files"] = []string{"foo", "bar"}
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := []string{"foo", "bar"}
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
var b Builder
config := testConfig()
@@ -135,27 +216,16 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
}
}
func TestBuilderPrepare_ISOMD5(t *testing.T) {
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_md5"] = ""
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_md5"] = "FOo"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOMD5 != "foo" {
t.Fatalf("should've lowercased: %s", b.config.ISOMD5)
}
}
func TestBuilderPrepare_ISOUrl(t *testing.T) {
+19 -125
View File
@@ -1,13 +1,8 @@
package vmware
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"runtime"
)
// A driver is able to talk to VMware, control virtual machines, etc.
@@ -30,6 +25,9 @@ type Driver interface {
// Get the path to the VMware ISO for the given flavor.
ToolsIsoPath(string) string
// Get the path to the DHCP leases file for the given device.
DhcpLeasesPath(string) string
// Verify checks to make sure that this driver should function
// properly. This should check that all the files it will use
// appear to exist and so on. If everything is okay, this doesn't
@@ -37,129 +35,25 @@ type Driver interface {
Verify() error
}
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
// NewDriver returns a new driver implementation for this operating
// system, or an error if the driver couldn't be initialized.
func NewDriver() (Driver, error) {
var driver Driver
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
switch runtime.GOOS {
case "darwin":
driver = &Fusion5Driver{
AppPath: "/Applications/VMware Fusion.app",
}
case "linux":
driver = &Workstation9LinuxDriver{}
default:
return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS)
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
if err := driver.Verify(); err != nil {
return nil, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
}
return err
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
}
return err
}
return nil
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
return stdout.String(), stderr.String(), err
return driver, nil
}
+142
View File
@@ -0,0 +1,142 @@
package vmware
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
}
return err
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
}
return err
}
return nil
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) DhcpLeasesPath(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
return stdout.String(), stderr.String(), err
}
+168
View File
@@ -0,0 +1,168 @@
package vmware
import (
"bytes"
"errors"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
)
// Workstation9LinuxDriver is a driver that can run VMware Workstation 9
// on Linux.
type Workstation9LinuxDriver struct {
AppPath string
VdiskManagerPath string
VmrunPath string
}
func (d *Workstation9LinuxDriver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.VdiskManagerPath, "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.VdiskManagerPath, "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Workstation9LinuxDriver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless {
guiArgument = "nogui"
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) Stop(vmxPath string) error {
cmd := exec.Command(d.VmrunPath, "-T", "ws", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) Verify() error {
if err := d.findApp(); err != nil {
return fmt.Errorf("VMware Workstation application ('vmware') not found in path.")
}
if err := d.findVmrun(); err != nil {
return fmt.Errorf("Required application 'vmrun' not found in path.")
}
if err := d.findVdiskManager(); err != nil {
return fmt.Errorf("Required application 'vmware-vdiskmanager' not found in path.")
}
// Check to see if it APPEARS to be licensed.
matches, err := filepath.Glob("/etc/vmware/license-*")
if err != nil {
return fmt.Errorf("Error looking for VMware license: %s", err)
}
if len(matches) == 0 {
return errors.New("Workstation does not appear to be licensed. Please license it.")
}
return nil
}
func (d *Workstation9LinuxDriver) findApp() error {
path, err := exec.LookPath("vmware")
if err != nil {
return err
}
d.AppPath = path
return nil
}
func (d *Workstation9LinuxDriver) findVdiskManager() error {
path, err := exec.LookPath("vmware-vdiskmanager")
if err != nil {
return err
}
d.VdiskManagerPath = path
return nil
}
func (d *Workstation9LinuxDriver) findVmrun() error {
path, err := exec.LookPath("vmrun")
if err != nil {
return err
}
d.VmrunPath = path
return nil
}
func (d *Workstation9LinuxDriver) ToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func (d *Workstation9LinuxDriver) DhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
}
func (d *Workstation9LinuxDriver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("VMware error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return stdout.String(), stderr.String(), err
}
+9 -2
View File
@@ -1,7 +1,7 @@
package vmware
import (
"fmt"
"errors"
"io/ioutil"
"os"
"regexp"
@@ -17,6 +17,9 @@ type GuestIPFinder interface {
// DHCPLeaseGuestLookup looks up the IP address of a guest using DHCP
// lease information from the VMware network devices.
type DHCPLeaseGuestLookup struct {
// Driver that is being used (to find leases path)
Driver Driver
// Device that the guest is connected to.
Device string
@@ -25,7 +28,7 @@ type DHCPLeaseGuestLookup struct {
}
func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
fh, err := os.Open(fmt.Sprintf("/var/db/vmware/vmnet-dhcpd-%s.leases", f.Device))
fh, err := os.Open(f.Driver.DhcpLeasesPath(f.Device))
if err != nil {
return "", err
}
@@ -68,5 +71,9 @@ func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
}
}
if curIp == "" {
return "", errors.New("IP not found for MAC in DHCP leases")
}
return curIp, nil
}
+1 -1
View File
@@ -33,7 +33,7 @@ func (f *IfconfigIPFinder) HostIP() (string, error) {
return "", err
}
re := regexp.MustCompile(`inet\s*(.+?)\s`)
re := regexp.MustCompile(`inet\s*(?:addr:)?(.+?)\s`)
matches := re.FindStringSubmatch(stdout.String())
if matches == nil {
return "", errors.New("IP not found in ifconfig output...")
+72
View File
@@ -0,0 +1,72 @@
package vmware
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"log"
"os"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(*config)
driver := state["driver"].(Driver)
vmxPath := state["vmx_path"].(string)
log.Println("Lookup up IP information...")
f, err := os.Open(vmxPath)
if err != nil {
return "", err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
vmxData := ParseVMX(string(vmxBytes))
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedAddress"]; !ok || macAddress == "" {
return "", errors.New("couldn't find MAC address in VMX")
}
}
ipLookup := &DHCPLeaseGuestLookup{
Driver: driver,
Device: "vmnet8",
MACAddress: macAddress,
}
ipAddress, err := ipLookup.GuestIP()
if err != nil {
log.Printf("IP lookup failed: %s", err)
return "", fmt.Errorf("IP lookup failed: %s", err)
}
if ipAddress == "" {
log.Println("IP is blank, no IP yet.")
return "", errors.New("IP is blank")
}
log.Printf("Detected IP: %s", ipAddress)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}, nil
}
+92
View File
@@ -0,0 +1,92 @@
package vmware
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
)
// This step cleans up the VMX by removing or changing this prior to
// being ready for use.
//
// Uses:
// ui packer.Ui
// vmx_path string
//
// Produces:
// <nothing>
type stepCleanVMX struct{}
func (s stepCleanVMX) Run(state map[string]interface{}) multistep.StepAction {
isoPath := state["iso_path"].(string)
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
ui.Say("Cleaning VMX prior to finishing up...")
vmxData, err := s.readVMX(vmxPath)
if err != nil {
state["error"] = fmt.Errorf("Error reading VMX: %s", err)
return multistep.ActionHalt
}
if _, ok := state["floppy_path"]; ok {
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Message("Unmounting floppy from VMX...")
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
}
}
vmxData["floppy0.present"] = "FALSE"
}
ui.Message("Detatching ISO from CD-ROM device...")
devRe := regexp.MustCompile(`^ide\d:\d\.`)
for k, _ := range vmxData {
match := devRe.FindString(k)
if match == "" {
continue
}
filenameKey := match + ".filename"
if filename, ok := vmxData[filenameKey]; ok {
if filename == isoPath {
// Change the CD-ROM device back to auto-detect to eject
vmxData[filenameKey] = "auto detect"
vmxData[match+".deviceType"] = "cdrom-raw"
}
}
}
// Rewrite the VMX
if err := WriteVMX(vmxPath, vmxData); err != nil {
state["error"] = fmt.Errorf("Error writing VMX: %s", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepCleanVMX) Cleanup(map[string]interface{}) {}
func (stepCleanVMX) readVMX(vmxPath string) (map[string]string, error) {
vmxF, err := os.Open(vmxPath)
if err != nil {
return nil, err
}
defer vmxF.Close()
vmxBytes, err := ioutil.ReadAll(vmxF)
if err != nil {
return nil, err
}
return ParseVMX(string(vmxBytes)), nil
}
+11 -4
View File
@@ -36,10 +36,10 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Building and writing VMX file")
tplData := &vmxTemplateData{
config.VMName,
config.GuestOSType,
config.DiskName,
isoPath,
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
ISOPath: isoPath,
}
var buf bytes.Buffer
@@ -55,6 +55,13 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
}
}
if floppyPathRaw, ok := state["floppy_path"]; ok {
log.Println("Floppy path present, setting in VMX")
vmxData["floppy0.present"] = "TRUE"
vmxData["floppy0.fileType"] = "file"
vmxData["floppy0.fileName"] = floppyPathRaw.(string)
}
vmxPath := filepath.Join(config.OutputDir, config.VMName+".vmx")
if err := WriteVMX(vmxPath, vmxData); err != nil {
err := fmt.Errorf("Error creating VMX file: %s", err)
+2 -3
View File
@@ -1,7 +1,6 @@
package vmware
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
@@ -27,7 +26,7 @@ func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
checksum, err := hex.DecodeString(config.ISOMD5)
checksum, err := hex.DecodeString(config.ISOChecksum)
if err != nil {
err := fmt.Errorf("Error parsing checksum: %s", err)
state["error"] = err
@@ -43,7 +42,7 @@ func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction
Url: config.ISOUrl,
TargetPath: cachePath,
CopyFile: false,
Hash: md5.New(),
Hash: common.HashForType(config.ISOChecksumType),
Checksum: checksum,
}
@@ -10,6 +10,12 @@ type stepPrepareOutputDir struct{}
func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
ui.Say("Deleting previous output directory...")
os.RemoveAll(config.OutputDir)
}
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state["error"] = err
-25
View File
@@ -1,25 +0,0 @@
package vmware
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepProvision struct{}
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*stepProvision) Cleanup(map[string]interface{}) {}
+3 -3
View File
@@ -49,9 +49,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
}
// Wait the wait amount
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait.String()))
time.Sleep(config.BootWait)
if int64(config.bootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait.String()))
time.Sleep(config.bootWait)
}
return multistep.ActionContinue
+21 -3
View File
@@ -1,6 +1,7 @@
package vmware
import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/multistep"
@@ -35,7 +36,13 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
if config.ShutdownCommand != "" {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
var stdout, stderr bytes.Buffer
cmd := &packer.RemoteCmd{
Command: config.ShutdownCommand,
Stdout: &stdout,
Stderr: &stderr,
}
if err := comm.Start(cmd); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
@@ -46,9 +53,20 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
// Wait for the command to run
cmd.Wait()
// If the command failed to run, notify the user in some way.
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Shutdown command has non-zero exit status.\n\nStdout: %s\n\nStderr: %s",
stdout.String(), stderr.String())
return multistep.ActionHalt
}
log.Printf("Shutdown stdout: %s", stdout.String())
log.Printf("Shutdown stderr: %s", stderr.String())
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.ShutdownTimeout)
shutdownTimer := time.After(config.ShutdownTimeout)
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
shutdownTimer := time.After(config.shutdownTimeout)
for {
running, _ := driver.IsRunning(vmxPath)
if !running {
+14
View File
@@ -95,8 +95,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func vncSendString(c *vnc.ClientConn, original string) {
special := make(map[string]uint32)
special["<bs>"] = 0xFF08
special["<del>"] = 0xFFFF
special["<enter>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<f1>"] = 0xFFBE
special["<f2>"] = 0xFFBF
special["<f3>"] = 0xFFC0
special["<f4>"] = 0xFFC1
special["<f5>"] = 0xFFC2
special["<f6>"] = 0xFFC3
special["<f7>"] = 0xFFC4
special["<f8>"] = 0xFFC5
special["<f9>"] = 0xFFC6
special["<f10>"] = 0xFFC7
special["<f11>"] = 0xFFC8
special["<f12>"] = 0xFFC9
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
-185
View File
@@ -1,185 +0,0 @@
package vmware
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"net"
"os"
"time"
)
// This step waits for SSH to become available and establishes an SSH
// connection.
//
// Uses:
// config *config
// ui packer.Ui
// vmx_path string
//
// Produces:
// communicator packer.Communicator
type stepWaitForSSH struct {
cancel bool
conn net.Conn
}
func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", config.SSHWaitTimeout.String())
timeout := time.After(config.SSHWaitTimeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
return multistep.ActionHalt
}
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepWaitForSSH) Cleanup(map[string]interface{}) {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
}
// Reads the network information for lookup via DHCP.
func (s *stepWaitForSSH) dhcpLeaseLookup(vmxPath string) (GuestIPFinder, error) {
f, err := os.Open(vmxPath)
if err != nil {
return nil, err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
vmxData := ParseVMX(string(vmxBytes))
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedAddress"]; !ok || macAddress == "" {
return nil, errors.New("couldn't find MAC address in VMX")
}
}
return &DHCPLeaseGuestLookup{"vmnet8", macAddress}, nil
}
// This blocks until SSH becomes available, and sends the communicator
// on the given channel.
func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
handshakeAttempts := 0
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
var nc net.Conn
for {
if nc != nil {
nc.Close()
}
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// First we wait for the IP to become available...
log.Println("Lookup up IP information...")
ipLookup, err := s.dhcpLeaseLookup(vmxPath)
if err != nil {
log.Printf("Can't lookup via DHCP lease: %s", err)
}
ip, err := ipLookup.GuestIP()
if err != nil {
log.Printf("IP lookup failed: %s", err)
continue
}
log.Printf("Detected IP: %s", ip)
// Attempt to connect to SSH port
nc, err = net.Dial("tcp", fmt.Sprintf("%s:%d", ip, config.SSHPort))
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
}
// Then we attempt to connect via SSH
sshConfig := &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}
comm, err = ssh.New(nc, sshConfig)
if err != nil {
log.Printf("SSH handshake err: %s", err)
handshakeAttempts += 1
if handshakeAttempts < 10 {
// Try to connect via SSH a handful of times
continue
}
return nil, err
}
ui.Say("Connected via SSH!")
break
}
// Store the connection so we can close it later
s.conn = nc
return comm, nil
}
+10 -1
View File
@@ -21,12 +21,14 @@ func (Command) Help() string {
func (c Command) Run(env packer.Environment, args []string) int {
var cfgDebug bool
var cfgForce bool
var cfgExcept []string
var cfgOnly []string
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
cmdFlags.BoolVar(&cfgDebug, "debug", false, "debug mode for builds")
cmdFlags.BoolVar(&cfgForce, "force", false, "force a build if artifacts exist")
cmdFlags.Var((*stringSliceValue)(&cfgExcept), "except", "build all builds except these")
cmdFlags.Var((*stringSliceValue)(&cfgOnly), "only", "only build the given builds by name")
if err := cmdFlags.Parse(args); err != nil {
@@ -141,11 +143,13 @@ func (c Command) Run(env packer.Environment, args []string) int {
env.Ui().Say("")
log.Printf("Build debug mode: %v", cfgDebug)
log.Printf("Force build: %v", cfgForce)
// Set the debug mode and prepare all the builds
// 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)
err := b.Prepare()
if err != nil {
env.Ui().Error(err.Error())
@@ -246,6 +250,11 @@ func (c Command) Run(env packer.Environment, args []string) int {
env.Ui().Say("\n==> Builds finished but no artifacts were created.")
}
if len(errors) > 0 {
// If any errors occurred, exit with a non-zero exit status
return 1
}
return 0
}
+1
View File
@@ -9,6 +9,7 @@ Usage: packer build [options] TEMPLATE
Options:
-debug Debug mode enabled for builds
-force Force a build to continue if artifacts exist, deletes existing artifacts
-except=foo,bar,baz Build all builds other than these
-only=foo,bar,baz Only build the given builds by name
`
+85
View File
@@ -0,0 +1,85 @@
package fix
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/mitchellh/packer/packer"
"os"
"strings"
)
type Command byte
func (Command) Help() string {
return strings.TrimSpace(helpString)
}
func (c Command) Run(env packer.Environment, args []string) int {
cmdFlags := flag.NewFlagSet("fix", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
args = cmdFlags.Args()
if len(args) != 1 {
cmdFlags.Usage()
return 1
}
// Read the file for decoding
tplF, err := os.Open(args[0])
if err != nil {
env.Ui().Error(fmt.Sprintf("Error opening template: %s", err))
return 1
}
defer tplF.Close()
// Decode the JSON into a generic map structure
var templateData map[string]interface{}
decoder := json.NewDecoder(tplF)
if err := decoder.Decode(&templateData); err != nil {
env.Ui().Error(fmt.Sprintf("Error parsing template: %s", err))
return 1
}
// Close the file since we're done with that
tplF.Close()
// Run the template through the various fixers
fixers := []Fixer{Fixers["iso-md5"]}
input := templateData
for _, fixer := range fixers {
var err error
input, err = fixer.Fix(input)
if err != nil {
env.Ui().Error(fmt.Sprintf("Error fixing: %s", err))
return 1
}
}
var output bytes.Buffer
encoder := json.NewEncoder(&output)
if err := encoder.Encode(input); err != nil {
env.Ui().Error(fmt.Sprintf("Error encoding: %s", err))
return 1
}
var indented bytes.Buffer
if err := json.Indent(&indented, output.Bytes(), "", " "); err != nil {
env.Ui().Error(fmt.Sprintf("Error encoding: %s", err))
return 1
}
result := indented.String()
result = strings.Replace(result, `\u003c`, "<", -1)
result = strings.Replace(result, `\u003e`, ">", -1)
env.Ui().Say(result)
return 0
}
func (c Command) Synopsis() string {
return "fixes templates from old versions of packer"
}
+14
View File
@@ -0,0 +1,14 @@
package fix
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}
+18
View File
@@ -0,0 +1,18 @@
package fix
// A Fixer is something that can perform a fix operation on a template.
type Fixer interface {
// Fix takes a raw map structure input, potentially transforms it
// in some way, and returns the new, transformed structure. The
// Fix method is allowed to mutate the input.
Fix(input map[string]interface{}) (map[string]interface{}, error)
}
// Fixers is the map of all available fixers, by name.
var Fixers map[string]Fixer
func init() {
Fixers = map[string]Fixer{
"iso-md5": new(FixerISOMD5),
}
}
+43
View File
@@ -0,0 +1,43 @@
package fix
import (
"github.com/mitchellh/mapstructure"
)
// FixerISOMD5 is a Fixer that replaces the "iso_md5" configuration key
// with the newer "iso_checksum" and "iso_checksum_type" within builders.
type FixerISOMD5 struct{}
func (FixerISOMD5) Fix(input map[string]interface{}) (map[string]interface{}, error) {
// Our template type we'll use for this fixer only
type template struct {
Builders []map[string]interface{}
}
// Decode the input into our structure, if we can
var tpl template
if err := mapstructure.Decode(input, &tpl); err != nil {
return nil, err
}
// Go through each builder and replace the iso_md5 if we can
for _, builder := range tpl.Builders {
md5raw, ok := builder["iso_md5"]
if !ok {
continue
}
md5, ok := md5raw.(string)
if !ok {
// TODO: error?
continue
}
delete(builder, "iso_md5")
builder["iso_checksum"] = md5
builder["iso_checksum_type"] = "md5"
}
input["builders"] = tpl.Builders
return input, nil
}
+46
View File
@@ -0,0 +1,46 @@
package fix
import (
"reflect"
"testing"
)
func TestFixerISOMD5_Impl(t *testing.T) {
var raw interface{}
raw = new(FixerISOMD5)
if _, ok := raw.(Fixer); !ok {
t.Fatalf("must be a Fixer")
}
}
func TestFixerISOMD5_Fix(t *testing.T) {
var f FixerISOMD5
input := map[string]interface{}{
"builders": []interface{}{
map[string]string{
"type": "foo",
"iso_md5": "bar",
},
},
}
expected := map[string]interface{}{
"builders": []map[string]interface{}{
map[string]interface{}{
"type": "foo",
"iso_checksum": "bar",
"iso_checksum_type": "md5",
},
},
}
output, err := f.Fix(input)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(output, expected) {
t.Fatalf("unexpected: %#v\nexpected: %#v\n", output, expected)
}
}
+16
View File
@@ -0,0 +1,16 @@
package fix
const helpString = `
Usage: packer fix [options] TEMPLATE
Reads the JSON template and attempts to fix known backwards
incompatibilities. The fixed template will be outputted to standard out.
If the template cannot be fixed due to an error, the command will exit
with a non-zero exit status. Error messages will appear on standard error.
Fixes that are run:
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
`
+110 -17
View File
@@ -1,6 +1,7 @@
package ssh
import (
"bufio"
"bytes"
"code.google.com/p/go.crypto/ssh"
"errors"
@@ -14,18 +15,39 @@ import (
type comm struct {
client *ssh.ClientConn
config *Config
conn net.Conn
}
// Config is the structure used to configure the SSH communicator.
type Config struct {
// The configuration of the Go SSH connection
SSHConfig *ssh.ClientConfig
// Connection returns a new connection. The current connection
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
}
// Creates a new packer.Communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func New(c net.Conn, config *ssh.ClientConfig) (result *comm, err error) {
client, err := ssh.Client(c, config)
result = &comm{client}
func New(config *Config) (result *comm, err error) {
// Establish an initial connection and connect
result = &comm{
config: config,
}
if err = result.reconnect(); err != nil {
result = nil
return
}
return
}
func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
session, err := c.client.NewSession()
session, err := c.newSession()
if err != nil {
return
}
@@ -56,7 +78,6 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
// exit boolean and status.
go func() {
defer session.Close()
err := session.Wait()
cmd.ExitStatus = 0
if err != nil {
@@ -73,8 +94,7 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
}
func (c *comm) Upload(path string, input io.Reader) error {
log.Println("Opening new SSH session")
session, err := c.client.NewSession()
session, err := c.newSession()
if err != nil {
return err
}
@@ -87,12 +107,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
// Set stderr/stdout to a bytes buffer
stderr := new(bytes.Buffer)
stdout := new(bytes.Buffer)
session.Stderr = stderr
session.Stdout = stdout
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
@@ -101,10 +115,26 @@ func (c *comm) Upload(path string, input io.Reader) error {
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash seperators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
log.Println("Starting remote scp process in sink mode")
@@ -124,11 +154,17 @@ func (c *comm) Upload(path string, input io.Reader) error {
// Start the protocol
log.Println("Beginning file upload...")
fmt.Fprintln(w, "C0644", input_memory.Len(), target_file)
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
io.Copy(w, input_memory)
fmt.Fprint(w, "\x00")
// TODO(mitchellh): Each step above results in a 0/1/2 being sent by
// the remote side to confirm. We should check for those confirmations.
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
@@ -159,7 +195,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
log.Printf("scp stdout (length %d): %#v", stdout.Len(), stdout.Bytes())
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
@@ -168,3 +203,61 @@ func (c *comm) Upload(path string, input io.Reader) error {
func (c *comm) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *comm) newSession() (*ssh.Session, error) {
log.Println("opening new ssh session")
session, err := c.client.NewSession()
if err != nil {
log.Printf("ssh session open error: '%s', attempting reconnect", err)
if err := c.reconnect(); err != nil {
return nil, err
}
return c.client.NewSession()
}
return session, nil
}
func (c *comm) reconnect() (err error) {
if c.conn != nil {
c.conn.Close()
}
log.Printf("reconnecting to TCP connection for SSH")
c.conn, err = c.config.Connection()
if err != nil {
log.Printf("reconnection error: %s", err)
return
}
log.Printf("handshaking with SSH")
c.client, err = ssh.Client(c.conn, c.config.SSHConfig)
if err != nil {
log.Printf("handshake error: %s", err)
}
return
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("Error reading error message: %s", err)
}
return errors.New(string(message))
}
return nil
}
+24 -8
View File
@@ -115,12 +115,20 @@ func TestNew_Invalid(t *testing.T) {
},
}
conn, err := net.Dial("tcp", newMockLineServer(t))
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", newMockLineServer(t))
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
_, err = New(conn, clientConfig)
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
_, err := New(config)
if err == nil {
t.Fatal("should have had an error connecting")
}
@@ -134,12 +142,20 @@ func TestStart(t *testing.T) {
},
}
conn, err := net.Dial("tcp", newMockLineServer(t))
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", newMockLineServer(t))
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
client, err := New(conn, clientConfig)
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
client, err := New(config)
if err != nil {
t.Fatalf("error connecting to SSH: %s", err)
}
+33
View File
@@ -0,0 +1,33 @@
package ssh
import (
"errors"
"log"
"net"
"time"
)
// ConnectFunc is a convenience method for returning a function
// that just uses net.Dial to communicate with the remote end that
// is suitable for use with the SSH communicator configuration.
func ConnectFunc(network, addr string, timeout time.Duration) func() (net.Conn, error) {
return func() (net.Conn, error) {
timeoutCh := time.After(timeout)
for {
select {
case <-timeoutCh:
return nil, errors.New("timeout connecting to remote machine")
default:
}
log.Printf("Opening conn for SSH to %s %s", network, addr)
nc, err := net.DialTimeout(network, addr, 15*time.Second)
if err == nil {
return nc, nil
}
time.Sleep(500 * time.Millisecond)
}
}
}
+2
View File
@@ -20,6 +20,7 @@ const defaultConfig = `
"builders": {
"amazon-ebs": "packer-builder-amazon-ebs",
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
@@ -27,6 +28,7 @@ const defaultConfig = `
"commands": {
"build": "packer-command-build",
"fix": "packer-command-fix",
"validate": "packer-command-validate"
},
+27 -8
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/packer/plugin"
"io"
"io/ioutil"
"log"
"os"
@@ -14,22 +15,40 @@ import (
)
func main() {
if os.Getenv("PACKER_LOG") == "" {
// If we don't have logging explicitly enabled, then disable it
log.SetOutput(ioutil.Discard)
} else {
// Logging is enabled, make sure it goes to stderr
log.SetOutput(os.Stderr)
// Setup logging if PACKER_LOG is set.
// Log to PACKER_LOG_PATH if it is set, otherwise default to stderr.
var logOutput io.Writer = ioutil.Discard
if os.Getenv("PACKER_LOG") != "" {
logOutput = os.Stderr
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
var err error
logOutput, err = os.Create(logPath)
if err != nil {
fmt.Fprintf(
os.Stderr,
"Couldn't open '%s' for logging: %s",
logPath, err)
os.Exit(1)
}
}
}
log.SetOutput(logOutput)
// If there is no explicit number of Go threads to use, then set it
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
log.Printf("Packer Version: %s %s", packer.Version, packer.VersionPrerelease)
log.Printf(
"Packer Version: %s %s %s",
packer.Version, packer.VersionPrerelease, packer.GitCommit)
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
// Prepare stdin for plugin usage by switching it to a pipe
setupStdin()
config, err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
@@ -100,7 +119,7 @@ func loadConfig() (*config, error) {
mustExist = false
if err != nil {
log.Printf("Error detecing default config file path: %s", err)
log.Printf("Error detecting default config file path: %s", err)
}
}
+36 -8
View File
@@ -6,13 +6,24 @@ import (
"sync"
)
// This is the key in configurations that is set to the name of the
// build.
const BuildNameConfigKey = "packer_build_name"
const (
// This is the key in configurations that is set to the name of the
// build.
BuildNameConfigKey = "packer_build_name"
// This is the key in configurations that is set to "true" when Packer
// debugging is enabled.
const DebugConfigKey = "packer_debug"
// This is the key in the configuration that is set to the type
// of the builder that is run. This is useful for provisioners and
// such who want to make use of this.
BuilderTypeConfigKey = "packer_builder_type"
// This is the key in configurations that is set to "true" when Packer
// debugging is enabled.
DebugConfigKey = "packer_debug"
// This is the key in configurations that is set to "true" when Packer
// force build is enabled.
ForceConfigKey = "packer_force"
)
// A Build represents a single job within Packer that is responsible for
// building some machine image artifact. Builds are meant to be parallelized.
@@ -42,6 +53,12 @@ type Build interface {
// When SetDebug is set to true, parallelism between builds is
// strictly prohibited.
SetDebug(bool)
// SetForce will enable/disable forcing a build when artifacts exist.
//
// When SetForce is set to true, existing artifacts from the build are
// deleted prior to the build.
SetForce(bool)
}
// A build struct represents a single build job, the result of which should
@@ -58,6 +75,7 @@ type coreBuild struct {
provisioners []coreBuildProvisioner
debug bool
force bool
l sync.Mutex
prepareCalled bool
}
@@ -96,8 +114,10 @@ func (b *coreBuild) Prepare() (err error) {
b.prepareCalled = true
packerConfig := map[string]interface{}{
BuildNameConfigKey: b.name,
DebugConfigKey: b.debug,
BuildNameConfigKey: b.name,
BuilderTypeConfigKey: b.builderType,
DebugConfigKey: b.debug,
ForceConfigKey: b.force,
}
// Prepare the builder
@@ -265,6 +285,14 @@ func (b *coreBuild) SetDebug(val bool) {
b.debug = val
}
func (b *coreBuild) SetForce(val bool) {
if b.prepareCalled {
panic("prepare has already been called")
}
b.force = val
}
// Cancels the build if it is running.
func (b *coreBuild) Cancel() {
b.builder.Cancel()
+9 -4
View File
@@ -11,6 +11,7 @@ func testBuild() *coreBuild {
name: "test",
builder: &TestBuilder{artifactId: "b"},
builderConfig: 42,
builderType: "foo",
hooks: map[string][]Hook{
"foo": []Hook{&TestHook{}},
},
@@ -40,8 +41,10 @@ func TestBuild_Prepare(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
packerConfig := map[string]interface{}{
BuildNameConfigKey: "test",
DebugConfigKey: false,
BuildNameConfigKey: "test",
BuilderTypeConfigKey: "foo",
DebugConfigKey: false,
ForceConfigKey: false,
}
build := testBuild()
@@ -86,8 +89,10 @@ func TestBuild_Prepare_Debug(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
packerConfig := map[string]interface{}{
BuildNameConfigKey: "test",
DebugConfigKey: true,
BuildNameConfigKey: "test",
BuilderTypeConfigKey: "foo",
DebugConfigKey: true,
ForceConfigKey: false,
}
build := testBuild()
+21 -2
View File
@@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"sync"
)
@@ -14,6 +15,9 @@ type Cache interface {
// Packer guarantees that no other process will write to this file while
// the lock is held.
//
// If the key has an extension (e.g., file.ext), the resulting path
// will have that extension as well.
//
// The cache will block and wait for the lock.
Lock(string) string
@@ -44,7 +48,7 @@ func (f *FileCache) Lock(key string) string {
rw := f.rwLock(hashKey)
rw.Lock()
return filepath.Join(f.CacheDir, hashKey)
return f.cachePath(key, hashKey)
}
func (f *FileCache) Unlock(key string) {
@@ -58,7 +62,7 @@ func (f *FileCache) RLock(key string) (string, bool) {
rw := f.rwLock(hashKey)
rw.RLock()
return filepath.Join(f.CacheDir, hashKey), true
return f.cachePath(key, hashKey), true
}
func (f *FileCache) RUnlock(key string) {
@@ -67,6 +71,21 @@ func (f *FileCache) RUnlock(key string) {
rw.RUnlock()
}
func (f *FileCache) cachePath(key string, hashKey string) string {
suffix := ""
endIndex := strings.Index(key, "?")
if endIndex == -1 {
endIndex = len(key)
}
dotIndex := strings.LastIndex(key[0:endIndex], ".")
if dotIndex > -1 {
suffix = key[dotIndex:endIndex]
}
return filepath.Join(f.CacheDir, hashKey+suffix)
}
func (f *FileCache) hashKey(key string) string {
sha := sha256.New()
sha.Write([]byte(key))
+15 -4
View File
@@ -3,6 +3,7 @@ package packer
import (
"io/ioutil"
"os"
"strings"
"testing"
)
@@ -36,19 +37,29 @@ func TestFileCache(t *testing.T) {
defer os.RemoveAll(cacheDir)
cache := &FileCache{CacheDir: cacheDir}
path := cache.Lock("foo")
path := cache.Lock("foo.ext?foo=bar.foo")
defer cache.Unlock("foo.ext?foo=bar.foo")
if !strings.HasSuffix(path, ".ext") {
t.Fatalf("bad extension with question mark: %s", path)
}
path = cache.Lock("foo.iso")
if !strings.HasSuffix(path, ".iso") {
t.Fatalf("path doesn't end with suffix '%s': '%s'", ".iso", path)
}
err = ioutil.WriteFile(path, []byte("data"), 0666)
if err != nil {
t.Fatalf("error writing: %s", err)
}
cache.Unlock("foo")
cache.Unlock("foo.iso")
path, ok := cache.RLock("foo")
path, ok := cache.RLock("foo.iso")
if !ok {
t.Fatal("cache says key doesn't exist")
}
defer cache.RUnlock("foo")
defer cache.RUnlock("foo.iso")
data, err := ioutil.ReadFile(path)
if err != nil {
+68
View File
@@ -1,7 +1,9 @@
package packer
import (
"github.com/mitchellh/iochan"
"io"
"strings"
"time"
)
@@ -57,6 +59,72 @@ type Communicator interface {
Download(string, io.Writer) error
}
// StartWithUi runs the remote command and streams the output to any
// configured Writers for stdout/stderr, while also writing each line
// as it comes to a Ui.
func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
} else {
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
}
if r.Stderr == nil {
r.Stderr = stderr_w
} else {
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
}
// Start the command
if err := c.Start(r); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan int, 1)
stdoutCh := iochan.DelimReader(stdout_r, '\n')
stderrCh := iochan.DelimReader(stderr_r, '\n')
// Start the goroutine to watch for the exit
go func() {
defer stdout_w.Close()
defer stderr_w.Close()
r.Wait()
exitCh <- r.ExitStatus
}()
// Loop and get all our output
OutputLoop:
for {
select {
case output := <-stderrCh:
ui.Message(strings.TrimSpace(output))
case output := <-stdoutCh:
ui.Message(strings.TrimSpace(output))
case <-exitCh:
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel before finishing these first.
for output := range stdoutCh {
ui.Message(strings.TrimSpace(output))
}
for output := range stderrCh {
ui.Message(strings.TrimSpace(output))
}
return nil
}
// Wait waits for the remote command to complete.
func (r *RemoteCmd) Wait() {
for !r.Exited {
+71
View File
@@ -1,10 +1,81 @@
package packer
import (
"bytes"
"io"
"strings"
"testing"
"time"
)
type TestCommunicator struct {
Stderr io.Reader
Stdout io.Reader
}
func (c *TestCommunicator) Start(rc *RemoteCmd) error {
go func() {
if rc.Stdout != nil && c.Stdout != nil {
io.Copy(rc.Stdout, c.Stdout)
}
if rc.Stderr != nil && c.Stderr != nil {
io.Copy(rc.Stderr, c.Stderr)
}
}()
return nil
}
func (c *TestCommunicator) Upload(string, io.Reader) error {
return nil
}
func (c *TestCommunicator) Download(string, io.Writer) error {
return nil
}
func TestRemoteCmd_StartWithUi(t *testing.T) {
data := "hello\nworld\nthere"
originalOutput := new(bytes.Buffer)
rcOutput := new(bytes.Buffer)
uiOutput := new(bytes.Buffer)
rcOutput.WriteString(data)
testComm := &TestCommunicator{
Stdout: rcOutput,
}
testUi := &ReaderWriterUi{
Reader: new(bytes.Buffer),
Writer: uiOutput,
}
rc := &RemoteCmd{
Command: "test",
Stdout: originalOutput,
}
go func() {
time.Sleep(100 * time.Millisecond)
rc.Exited = true
}()
err := rc.StartWithUi(testComm, testUi)
if err != nil {
t.Fatalf("err: %s", err)
}
if uiOutput.String() != strings.TrimSpace(data)+"\n" {
t.Fatalf("bad output: '%s'", uiOutput.String())
}
if originalOutput.String() != data {
t.Fatalf("original is bad: '%s'", originalOutput.String())
}
}
func TestRemoteCmd_Wait(t *testing.T) {
var cmd RemoteCmd
+30
View File
@@ -22,3 +22,33 @@ func (e *MultiError) Error() string {
"%d error(s) occurred:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
// MultiErrorAppend is a helper function that will append more errors
// onto a MultiError in order to create a larger multi-error. If the
// original error is not a MultiError, it will be turned into one.
func MultiErrorAppend(err error, errs ...error) *MultiError {
if err == nil {
err = new(MultiError)
}
switch err := err.(type) {
case *MultiError:
if err == nil {
err = new(MultiError)
}
if err.Errors == nil {
err.Errors = make([]error, 0, len(errs))
}
err.Errors = append(err.Errors, errs...)
return err
default:
newErrs := make([]error, len(errs)+1)
newErrs[0] = err
copy(newErrs[1:], errs)
return &MultiError{
Errors: newErrs,
}
}
}
+25
View File
@@ -30,3 +30,28 @@ func TestMultiErrorError(t *testing.T) {
multi := &MultiError{errors}
assert.Equal(multi.Error(), expected, "should have proper error")
}
func TestMultiErrorAppend_MultiError(t *testing.T) {
original := &MultiError{
Errors: []error{errors.New("foo")},
}
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
original = &MultiError{}
result = MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 1 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}
func TestMultiErrorAppend_NonMultiError(t *testing.T) {
original := errors.New("foo")
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}

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