Compare commits

..

156 Commits

Author SHA1 Message Date
Mitchell Hashimoto a000e24dc7 v0.3.5 2013-08-28 10:47:19 -07:00
Mitchell Hashimoto 6941f72b21 website: document shuting down network interfaces for reboot 2013-08-28 10:46:45 -07:00
Mitchell Hashimoto 4540285b15 common: support SHA512 as checksum type [Gh-356] 2013-08-28 09:09:43 -07:00
Mitchell Hashimoto a14d64f0ed website: update website for ssh_key_path stuff 2013-08-27 22:59:36 -07:00
Mitchell Hashimoto e7b2751d63 builder/vmware: support key based SSH auth [GH-70] 2013-08-27 22:57:42 -07:00
Mitchell Hashimoto 19bafd8da8 builder/virtualbox: support ssh keys as auth mechanism [GH-70] 2013-08-27 22:55:13 -07:00
Mark Peek f717edd644 builder/openstack: return artifacts 2013-08-27 22:37:06 -07:00
Mark Peek ca1388f7bc builder/openstack: simplify by passing csp instead of accessor and api 2013-08-27 22:37:06 -07:00
Mitchell Hashimoto 62fbf8fef8 communicator/ssh: Reset failure count if heartbeat works 2013-08-27 22:14:31 -07:00
Mitchell Hashimoto c16c467e63 communicator/ssh: heartbeat the SSH connection to detect drops [GH-200] 2013-08-27 22:12:21 -07:00
Mitchell Hashimoto 6805d62b44 packer: template error if override specified for bad builder [GH-336] 2013-08-27 21:34:55 -07:00
Mitchell Hashimoto caa4fce2d3 Merge branch 'fix-minion-config-with-non-tmp-dir' of https://github.com/rach/packer into rach-fix-minion-config-with-non-tmp-dir
Conflicts:
	provisioner/salt-masterless/provisioner.go
2013-08-27 20:21:02 -07:00
Mitchell Hashimoto d08019c1a7 website: Fix typo in docs [GH-340] 2013-08-27 17:55:09 -07:00
Mitchell Hashimoto 6f1675ab6b provisioner/chef-solo: template process the configuration 2013-08-27 17:41:49 -07:00
Mitchell Hashimoto ec8ba97c11 fmt 2013-08-27 17:37:29 -07:00
Mitchell Hashimoto c186d2dc3e Update CHANGELOG 2013-08-27 17:37:07 -07:00
Mitchell Hashimoto 06643423b1 Merge pull request #353 from rach/add-setting-for-pillar-folder
provisioner/salt-masterless: Add setting for pillar folder
2013-08-27 17:36:28 -07:00
Mitchell Hashimoto e50a15c4ee builder/vmware: support vmx_template_path [GH-270] 2013-08-27 17:23:28 -07:00
Rachid Belaid 99a7ae8792 Add doc and change pillar tree in roots to respect salt vocabulary 2013-08-28 01:01:37 +01:00
Mitchell Hashimoto 20541a7eda builder/vmware: ability to not request a PTY for SSH [GH-270] 2013-08-27 16:51:05 -07:00
Rachid Belaid 2c03c1a64e Add setting for pillar folder to respect saltstack structure 2013-08-28 00:38:36 +01:00
Mitchell Hashimoto f626790edd Update CHANGELOG 2013-08-27 16:36:36 -07:00
Mitchell Hashimoto 9865dd0161 Merge pull request #339 from mitchellh/do-soft-shutdown
builder/digitalocean: send a "shutdown" before snapshotting
2013-08-27 16:35:30 -07:00
Rachid Belaid c680fe3909 Fix minion config with non tmp dir 2013-08-27 23:49:04 +01:00
Mitchell Hashimoto 95682f6dfc command/build: build error machine-readable is targetted to the build 2013-08-27 15:03:49 -07:00
Mitchell Hashimoto 869c0b94ff provisioner/chef-solo: use proper exceute command 2013-08-27 14:35:11 -07:00
Mitchell Hashimoto e6960e3e04 plugin/provisioner-chef-solo: compile 2013-08-27 14:31:42 -07:00
Mitchell Hashimoto 2746139591 website: document chef solo provisioner 2013-08-27 14:29:52 -07:00
Mitchell Hashimoto f536e351fa command/build: "end" sentinel in artifact machine-readable output 2013-08-27 13:40:19 -07:00
Mitchell Hashimoto 53890e9de3 command/build: put build errors in machine readable output 2013-08-27 13:37:31 -07:00
Mitchell Hashimoto 56a309941b provisioner/chef-solo: remove unused constants and variables 2013-08-27 13:32:59 -07:00
Mitchell Hashimoto 286c36b3c4 Update CHANGELOG 2013-08-27 13:27:53 -07:00
Mitchell Hashimoto fa2347e711 provisioner/chef-solo: upload cookbooks 2013-08-27 13:27:20 -07:00
Mitchell Hashimoto 5e841befee provisioner/chef-solo: massive refactor and style nitpick 2013-08-27 13:27:20 -07:00
Mitchell Hashimoto e43d08a3fc provisioner/chef-solo: installChef fixed up to use tpl and StartWithUi 2013-08-27 13:27:20 -07:00
Mark Peek 2e760ee57b Add new openstack builder 2013-08-26 22:05:21 -07:00
Mark Peek 9bc3702adc website: add openstack page 2013-08-26 22:02:03 -07:00
Mark Peek e7140cf405 Add base support for openstack [GH-155]
This change adds base support for an openstack builder.
Thank you to Rackspace for providing cloud assets to complete this work
and @sam-falvo for working with us on the perigee/gophercloud changes.
2013-08-26 21:57:23 -07:00
Mitchell Hashimoto 99dd3ccec5 Update CHANGELOG 2013-08-25 20:51:51 -07:00
Mitchell Hashimoto d6fde7dcb1 Merge branch 'jvandyke-provisioner-chef-solo'
Chef-solo provisioner first pass.
2013-08-25 20:50:06 -07:00
Mitchell Hashimoto da1de12b78 Merge branch 'provisioner-chef-solo' of https://github.com/jvandyke/packer into jvandyke-provisioner-chef-solo
Conflicts:
	config.go
2013-08-25 20:49:53 -07:00
Mitchell Hashimoto 5eb97908c2 communicator/ssh: Trailing slash won't create destination dir 2013-08-25 20:47:10 -07:00
Mitchell Hashimoto aa47487960 communicator/ssh: UploadDir works properly 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto 1948350d20 communicator/ssh: refactor to upload directories properly 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto e75d3c1fbb communicator/ssh: refactor scpSession that we'll reuse for UploadDir 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto 7895df8c8f builder/amazon/chroot: implement UploadDir for chroot communicator 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto 8a194a3750 packer/rpc: implement UploadDir 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto dbfe8e21e1 packer/rpc: use public MockCommunicator, tests pass 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto f5c7c6ea90 packer: Add UploadDir to the Communicator interface 2013-08-25 20:30:56 -07:00
Mitchell Hashimoto e589bf2724 Merge pull request #343 from masteinhauser/patch-1
website: Actual md5sum as Ubuntu Hashesh webpage is out of date.
2013-08-24 21:07:50 -07:00
Myles A. K. Steinhauser 8311b3508c Actual md5sum as Ubuntu Hashesh webpage is out of date. 2013-08-24 20:43:49 -04:00
Mitchell Hashimoto 894ca6deca Update CHANGELOG 2013-08-24 17:14:45 -07:00
Mitchell Hashimoto 32f1092608 Merge pull request #338 from mitchellh/signal-confirm
main: confirm cleanup at first signal received
2013-08-24 17:13:25 -07:00
Mitchell Hashimoto c365dd2f9f Merge pull request #342 from masteinhauser/patch-1
website: bump Ubuntu example from 12.04.2 to 12.04.3
2013-08-24 17:05:48 -07:00
Myles A. K. Steinhauser d6d54cc406 Bump Ubuntu 12.04.2 to 12.04.3
Ubuntu has removed the 12.04.2 release ISOs from their mirrors when they released 12.04.3. This impacted the URL and the Checksum contained in the example.json which returned an HTTP 404 from Ubuntu's Mirrors.
2013-08-24 20:04:45 -04:00
Jack Pearkes 43dd2bd2c9 builder/digitalocean: send a "shutdown" before snapshotting
Instead of pulling the plug on a droplet with the "poweroff" command,
we first send a soft shutdown to the API, then we poweroff to allow
the snapshot to properly complete.

Sending just a shutdown and then snapshotting wasn't as reliable as
sending the poweroff manually, for reasons unknown to me.

This fixes #332.
2013-08-24 13:04:51 +02:00
Jack Pearkes 088665840f packer/plugin: confirm cleanup at first signal received 2013-08-24 12:55:25 +02:00
Mitchell Hashimoto 14517e6fa5 provisioner/salt-masterless: fix the build 2013-08-23 15:58:30 -07:00
Mitchell Hashimoto 9527f60884 builder/amazon/instance: remove check for AMI tools [GH-330] 2013-08-23 15:53:54 -07:00
Mitchell Hashimoto 788fe84053 Update CHANGELOG 2013-08-23 15:42:30 -07:00
Mitchell Hashimoto f56601fd64 website: delete salt.html.markdown 2013-08-23 15:41:55 -07:00
Rachid Belaid 4b2d1efb13 Fix #264 to upload local minion config
Conflicts:
	provisioner/salt-masterless/provisioner.go
	website/source/docs/provisioners/salt.html.markdown
2013-08-23 15:41:11 -07:00
Mitchell Hashimoto da70359949 Update CHANGELOG 2013-08-23 15:32:05 -07:00
Mitchell Hashimoto 3a19127f53 post-processor/vagrant: don't error if unused 2013-08-23 15:31:21 -07:00
Mitchell Hashimoto 303a003d2f packer: no need to check if nil since we're appending to slice 2013-08-23 15:25:02 -07:00
Mitchell Hashimoto 5d7dc6e1c9 Merge pull request #329 from jasonberanek/vagrant-output-fix
post-processor/vagrant: proper output ConfigTemplate validation [GH-324]
2013-08-23 15:15:55 -07:00
Mitchell Hashimoto 012b534873 post-processor/vagrant: nitpick format 2013-08-23 15:14:58 -07:00
Mitchell Hashimoto 701b867a95 Update CHANGELOG 2013-08-23 15:07:13 -07:00
Mitchell Hashimoto e550c4503e Merge pull request #331 from jasonberanek/vbox-vagrant-ova
post-processor/vagrant: support for Virtualbox OVA artifacts
2013-08-23 15:06:44 -07:00
Mitchell Hashimoto d3fff7d145 packer/rpc: don't panic on failed Ui calls 2013-08-23 14:39:59 -07:00
Mitchell Hashimoto 4513a2509e Update CHANGELOG 2013-08-23 14:34:31 -07:00
Mitchell Hashimoto 2b797bb1ed packer/plugin: swallowInterrupts in plugin should swallow all 2013-08-23 14:22:32 -07:00
Mitchell Hashimoto c7e932ee6e packer/rpc: error instead of panic cancelling builder 2013-08-23 13:58:00 -07:00
Mitchell Hashimoto a7d3638146 packer/rpc: less panics 2013-08-23 13:57:26 -07:00
Mitchell Hashimoto 5a1073a980 communicator/ssh: more error handlingin Upload 2013-08-23 11:27:59 -07:00
Mitchell Hashimoto 99a7116939 provisioner/shell: retry the upload if command fails as well
This improves reboot handling robustness
2013-08-23 11:21:20 -07:00
Jason A. Beranek dec7f3f2ef post-processor/vagrant/virtualbox: support ova artifacts [GH-315] 2013-08-22 21:47:50 -05:00
Jason A. Beranek b2157b87b9 post-processor/vagrant: fix output ConfigTemplate validation [GH-324]
-apply same output validation steps found in builder specific Configure
functions
2013-08-22 20:19:03 -05:00
Mitchell Hashimoto f4f433d0d8 builder/amazon: modify attributes before region copy
/cc @jmassara
2013-08-22 15:35:47 -07:00
Mitchell Hashimoto d9e80803c9 builder/amazon/common: modify output for AMI copy 2013-08-22 15:32:24 -07:00
Mitchell Hashimoto 9810766373 website: document ami_regions
/cc @jmassara
2013-08-22 15:20:44 -07:00
Mitchell Hashimoto 385ca4cc97 website: document tags on every amazon builder 2013-08-22 15:17:07 -07:00
Mitchell Hashimoto 0306759840 builder/amazon: support tags in chroot and instance 2013-08-22 15:11:54 -07:00
Mitchell Hashimoto 50e2535bd8 builder/amazon/common: move tags into ami_config 2013-08-22 15:09:21 -07:00
Mitchell Hashimoto f197c2b5f0 builder/amazon: region copy must go before modify attributes
/cc @jmassara - This has to go before the modify attributes so that
the attributes are properly modified on all resulting AMIs.
2013-08-22 15:03:30 -07:00
Mitchell Hashimoto 374f2fb647 builder/amazon/common: remove duplicates from ami_region
/cc @jmassara
2013-08-22 14:58:21 -07:00
Mitchell Hashimoto cd4fb50cb7 Update CHANGELOG 2013-08-22 14:44:48 -07:00
Mitchell Hashimoto 4522db5d7d Merge pull request #322 from jmassara/ami_region_copy
amazon/common: Added AMI CopyImage support
2013-08-22 14:44:03 -07:00
Mitchell Hashimoto 16df9a2c69 Update CHANGELOG 2013-08-22 14:29:33 -07:00
Mitchell Hashimoto 6fd17791e6 Merge pull request #328 from jbronn/vmdk_type
builder/vmware: Enable customization of VMDK type with `disk_type_id`
2013-08-22 14:28:43 -07:00
Mitchell Hashimoto db01abc008 packer/rpc: fix build by missing import 2013-08-22 11:53:54 -07:00
Mitchell Hashimoto 3375c9e4fd communicator/ssh: respect interrupts by not looping on retyr [GH-327] 2013-08-22 11:53:31 -07:00
Justin Bronn 53b13869b3 Document the disk_type_id parameter. 2013-08-22 11:51:53 -07:00
Mitchell Hashimoto 24b12993b7 packer/rpc: get rid of panicable case 2013-08-22 11:45:58 -07:00
Justin Bronn ffe63b8bd8 Make it possible to customize the VMware virtual disk type id. 2013-08-22 11:40:56 -07:00
Mitchell Hashimoto 58a48ed171 common/command: error if only/except points to bad build 2013-08-22 11:40:30 -07:00
Mitchell Hashimoto 2cf112c0ba common/command: more tests for filtering builds 2013-08-22 11:34:51 -07:00
Mitchell Hashimoto cc0f91b41b packer: add mock implementations and more template tests 2013-08-22 11:32:59 -07:00
James Massara e2ce81ce5e Make sure ami_regions are valid 2013-08-21 18:44:14 -07:00
James Massara 1e27feb6c6 amazon/common: Added AMI CopyImage support 2013-08-21 13:53:07 -07:00
Mitchell Hashimoto cfb53005ff Update CHANGELOG 2013-08-21 11:56:02 -07:00
Mitchell Hashimoto ba900fc2e8 up version for dev 2013-08-21 11:55:45 -07:00
Mitchell Hashimoto 148394a264 v0.3.4 2013-08-21 11:44:20 -07:00
Mitchell Hashimoto 45ad452c3b scripts: build go get ./... early to speed up builds 2013-08-21 11:20:55 -07:00
Mitchell Hashimoto 415fb2c935 scripts: set PACKER_RACE to build with race detector 2013-08-21 11:15:09 -07:00
Mitchell Hashimoto aa383885c5 Make travis run data race tests 2013-08-21 11:06:01 -07:00
Mitchell Hashimoto 77dd02c332 communicator/ssh: get data race tests passing 2013-08-21 11:05:21 -07:00
Mitchell Hashimoto b059cce542 packer/plugin: remove race in Exited() 2013-08-21 11:00:07 -07:00
Mitchell Hashimoto 5d7586cc59 packer/plugin: get rid of data race setting exited for Client 2013-08-21 10:56:58 -07:00
Mitchell Hashimoto 0b8bd1d7b7 packer/plugin: fix data race reading stdout in Client 2013-08-21 10:49:57 -07:00
Mitchell Hashimoto 2b5282b3d8 packer/rpc: get rid of data races in tests 2013-08-21 10:21:32 -07:00
Mitchell Hashimoto 4524b13911 packer: fix data race in communicator 2013-08-21 10:16:33 -07:00
Mark Peek 32ab55c79f website: tweak the formatting of the sleep tip 2013-08-20 23:45:02 -07:00
Mark Peek 2b2903c4eb Merge pull request #319 from ahawkins/patch-1
Add sleep tip about ssh
2013-08-20 23:40:57 -07:00
Adam Hawkins de1cabb1f3 Fix syntax error 2013-08-21 01:35:48 +02:00
Adam Hawkins e64aec3e57 Add sleep tip about ssh 2013-08-21 01:34:48 +02:00
Mark Peek fff968eb5a website: add a troubleshooting section for the shell provisioner 2013-08-20 14:07:21 -07:00
Mitchell Hashimoto 21b1d1f00e Update CHANGELOG 2013-08-19 23:40:14 -07:00
Mitchell Hashimoto 96f8b45add packer/plugin: Set killed in the kill clients method 2013-08-19 23:39:14 -07:00
Mitchell Hashimoto 32216f5707 packer/plugin: Killed bool to avoid panics when killing clients 2013-08-19 23:38:22 -07:00
Mitchell Hashimoto 29ede35b28 packer/rpc: when communicator abruptly exits, set exit status 123 2013-08-19 23:21:53 -07:00
Mitchell Hashimoto 6e99c468d4 provisioner/shell: retry uploads if reboot [GH-282] 2013-08-19 23:02:06 -07:00
Mitchell Hashimoto db60498f4f post-processor/vagrant: show file being compressed [GH-314] 2013-08-19 22:38:32 -07:00
Mitchell Hashimoto cd7e0403fd update version for dev 2013-08-19 16:39:00 -07:00
Mitchell Hashimoto fd21277907 v0.3.3 2013-08-19 16:25:58 -07:00
Mitchell Hashimoto 28a8293a22 packer/plugin: set TCP keep-alive on connection 2013-08-19 16:25:00 -07:00
Mitchell Hashimoto 7ad307e95a builder/virtualbox: fmt 2013-08-19 16:24:29 -07:00
Mitchell Hashimoto 844e355ed3 website: document formats feature 2013-08-19 16:10:49 -07:00
Mitchell Hashimoto 489a568741 Update CHANGELOG 2013-08-19 16:09:51 -07:00
Mitchell Hashimoto 0208548082 Merge pull request #309 from jsiebens/virtualbox_ova
builder/virtualbox: export to ovf or ova (default ovf)
2013-08-19 16:08:39 -07:00
Mitchell Hashimoto 5d9a2b63ff packer: remove keep_input_artifact prior to sending to build [GH-310] 2013-08-19 16:00:25 -07:00
Mitchell Hashimoto 629ec33aa8 packer: postProvisioner should be postProcessor 2013-08-19 15:55:30 -07:00
Johan Siebens a73ec1deb7 builder/virtualbox: export to ovf or ova (default ovf) 2013-08-19 20:21:36 +02:00
Mitchell Hashimoto c84d2aeffc post-processor/vagrant: process output path properly 2013-08-18 20:37:04 -06:00
Mitchell Hashimoto 1d0ceec7af Update CHANGELOG 2013-08-18 20:30:49 -06:00
Mitchell Hashimoto 513e4a2a3a builder/digitalocean: use HTTP proxy if in env 2013-08-18 20:29:54 -06:00
Mitchell Hashimoto 9bf7d7b81b common: use HTTP proxy if available from env [GH-252] 2013-08-18 12:34:36 -06:00
Mitchell Hashimoto 58960a8790 up version for dev 2013-08-18 12:27:25 -06:00
Mitchell Hashimoto 86a9d4fa09 website: set some more page titles 2013-08-18 10:42:19 -06:00
Mitchell Hashimoto 29812ae9b7 scripts: update dist.sh to latest go-xc 2013-08-18 10:38:00 -06:00
Mitchell Hashimoto 258e247cf6 v0.3.2 2013-08-18 10:38:00 -06:00
Mitchell Hashimoto 8d8edc998a Merge pull request #305 from mitchellh/website-deep-links
website: generate toc data for linking into headers
2013-08-18 07:44:52 -07:00
Jack Pearkes 9da7b5db30 website: generate toc data for linking into headers
A note: redcarpet just added an awesome feature that makes the
anchor links human readable. i.e `shell-provisioner` instead of `toc_0`.

see: https://github.com/vmg/redcarpet/blob/master/CHANGELOG.md#changelog

It's not yet released, but when it is we shpould upgrade to that, ya!
2013-08-18 13:34:35 +02:00
James Van Dyke 428bc4d745 Remove debugging statement. 2013-07-15 16:38:26 -04:00
James Van Dyke ba5d7a9d72 Remove redundant code and clean up some string concatenation.
Clean up Say statements.
2013-07-12 11:09:52 -04:00
James Van Dyke a6f3bb3bb2 Improve logging.
Correct behavior for undefined recipe list.
Correct package name to upstream repo.
2013-07-11 12:22:15 -04:00
James Van Dyke 1fadd03837 Merge remote-tracking branch 'upstream/master' into provisioner-chef-solo 2013-07-11 10:48:53 -04:00
James Van Dyke a84f26bfdf Add basic test file for provisioner-chef-solo. 2013-07-11 10:47:51 -04:00
James Van Dyke 24c4b5334b Merge remote-tracking branch 'upstream/master' into provisioner-chef-solo
Conflicts:
	config.go

Add the chef-solo provisioner back to config.go.
Fix import path for chef-solo provisioner.
2013-07-10 08:58:00 -04:00
James Van Dyke 69f0049a44 Rename configuration avoid_sudo to prevent_sudo 2013-07-06 00:37:59 -04:00
James Van Dyke 3416f0760c Remove errant die statement. 2013-07-06 00:17:28 -04:00
James Van Dyke e5e97f3cc1 Implement skip_install configuration to skip Chef installation. 2013-07-06 00:15:21 -04:00
James Van Dyke 552298b683 Implement avoid_sudo configuration option. 2013-07-05 23:56:51 -04:00
James Van Dyke bb3ff62623 Rename configuration cookbook_paths to cookbooks_paths to
conform to Vagrant wording.
Remove configuration `cookbook_path` to simplify some things.
2013-07-05 22:33:24 -04:00
James Van Dyke 608d874b45 Make JSON attributes work with real JSON! 2013-07-05 21:57:56 -04:00
James Van Dyke 45a16fceac First pass at Chef Solo provisioner. 2013-07-05 00:26:48 -04:00
110 changed files with 3778 additions and 606 deletions
+3 -1
View File
@@ -5,7 +5,9 @@ go:
- tip
install: make deps
script: make test
script:
- go test ./...
- go test -race ./...
notifications:
flowdock:
+82
View File
@@ -1,3 +1,85 @@
## 0.3.5 (August 28, 2013)
FEATURES:
* **NEW BUILDER:** `openstack`. You can now build on OpenStack. [GH-155]
* **NEW PROVISIONER:** `chef-solo`. You can now provision with Chef
using `chef-solo` from local cookbooks.
* builder/amazon: Copy AMI to multiple regions with `ami_regions`. [GH-322]
* builder/virtualbox,vmware: Can now use SSH keys as an auth mechanism for
SSH using `ssh_key_path`. [GH-70]
* builder/virtualbox,vmware: Support SHA512 as a checksum type. [GH-356]
* builder/vmware: The root hard drive type can now be specified with
"disk_type_id" for advanced users. [GH-328]
* provisioner/salt-masterless: Ability to specfy a minion config. [GH-264]
* provisioner/salt-masterless: Ability to upload pillars. [GH-353]
IMPROVEMENTS:
* core: Output message when Ctrl-C received that we're cleaning up. [GH-338]
* builder/amazon: Tagging now works with all amazon builder types.
* builder/vmware: Option `ssh_skip_request_pty` for not requesting a PTY
for the SSH connection. [GH-270]
* builder/vmware: Specify a `vmx_template_path` in order to customize
the generated VMX. [GH-270]
* command/build: Machine-readable output now contains build errors, if any.
* command/build: An "end" sentinel is outputted in machine-readable output
for artifact listing so it is easier to know when it is over.
BUG FIXES:
* core: Fixed a couple cases where a double ctrl-C could panic.
* core: Template validation fails if an override is specified for a
non-existent builder. [GH-336]
* core: The SSH connection is heartbeated so that drops can be
detected. [GH-200]
* builder/amazon/instance: Remove check for ec2-ami-tools because it
didn't allow absolute paths to work properly. [GH-330]
* builder/digitalocean: Send a soft shutdown request so that files
are properly synced before shutdown. [GH-332]
* command/build,command/validate: If a non-existent build is specified to
'-only' or '-except', it is now an error. [GH-326]
* post-processor/vagrant: Setting OutputPath with a timestamp now
always works properly. [GH-324]
* post-processor/vagrant: VirtualBox OVA formats now turn into
Vagrant boxes properly. [GH-331]
* provisioner/shell: Retry upload if start command fails, making reboot
handling much more robust.
## 0.3.4 (August 21, 2013)
IMPROVEMENTS:
* post-processor/vagrant: the file being compressed will be shown
in the UI [GH-314]
BUG FIXES:
* core: Avoid panics when double-interrupting Packer.
* provisioner/shell: Retry shell script uploads, making reboots more
robust if they happen to fail in this stage. [GH-282]
## 0.3.3 (August 19, 2013)
FEATURES:
* builder/virtualbox: support exporting in OVA format. [GH-309]
IMPROVEMENTS:
* core: All HTTP downloads across Packer now support the standard
proxy environmental variables (`HTTP_PROXY`, `NO_PROXY`, etc.) [GH-252]
* builder/amazon: API requests will use HTTP proxy if specified by
enviromental variables.
* builder/digitalocean: API requests will use HTTP proxy if specified
by environmental variables.
BUG FIXES:
* core: TCP connection between plugin processes will keep-alive. [GH-312]
* core: No more "unused key keep_input_artifact" for post processors [GH-310]
* post-processor/vagrant: `output_path` templates now work again.
## 0.3.2 (August 18, 2013)
FEATURES:
+7
View File
@@ -194,6 +194,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
Tags: b.config.AMITags,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
+32
View File
@@ -70,6 +70,38 @@ func (c *Communicator) Upload(dst string, r io.Reader) error {
return nil
}
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
walkFn := func(fullPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
path, err := filepath.Rel(src, fullPath)
if err != nil {
return err
}
for _, e := range exclude {
if e == path {
log.Printf("Skipping excluded file: %s", path)
return nil
}
}
dstPath := filepath.Join(dst, path)
f, err := os.Open(fullPath)
if err != nil {
return err
}
defer f.Close()
return c.Upload(dstPath, f)
}
log.Printf("Uploading directory '%s' to '%s'", src, dst)
return filepath.Walk(src, walkFn)
}
func (c *Communicator) Download(src string, w io.Writer) error {
src = filepath.Join(c.Chroot, src)
log.Printf("Downloading from chroot dir: %s", src)
+55 -5
View File
@@ -2,16 +2,19 @@ package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/packer/packer"
)
// AMIConfig is for common configuration related to creating AMIs.
type AMIConfig struct {
AMIName string `mapstructure:"ami_name"`
AMIDescription string `mapstructure:"ami_description"`
AMIUsers []string `mapstructure:"ami_users"`
AMIGroups []string `mapstructure:"ami_groups"`
AMIProductCodes []string `mapstructure:"ami_product_codes"`
AMIName string `mapstructure:"ami_name"`
AMIDescription string `mapstructure:"ami_description"`
AMIUsers []string `mapstructure:"ami_users"`
AMIGroups []string `mapstructure:"ami_groups"`
AMIProductCodes []string `mapstructure:"ami_product_codes"`
AMIRegions []string `mapstructure:"ami_regions"`
AMITags map[string]string `mapstructure:"tags"`
}
func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
@@ -42,6 +45,7 @@ func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
"ami_users": c.AMIUsers,
"ami_groups": c.AMIGroups,
"ami_product_codes": c.AMIProductCodes,
"ami_regions": c.AMIRegions,
}
for n, slice := range sliceTemplates {
@@ -59,6 +63,52 @@ func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
errs = append(errs, fmt.Errorf("ami_name must be specified"))
}
if len(c.AMIRegions) > 0 {
regionSet := make(map[string]struct{})
regions := make([]string, 0, len(c.AMIRegions))
for _, region := range c.AMIRegions {
// If we already saw the region, then don't look again
if _, ok := regionSet[region]; ok {
continue
}
// Mark that we saw the region
regionSet[region] = struct{}{}
// Verify the region is real
if _, ok := aws.Regions[region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", region))
continue
}
regions = append(regions, region)
}
c.AMIRegions = regions
}
newTags := make(map[string]string)
for k, v := range c.AMITags {
k, err := t.Process(k, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag key %s: %s", k, err))
continue
}
v, err := t.Process(v, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag value '%s': %s", v, err))
continue
}
newTags[k] = v
}
c.AMITags = newTags
if len(errs) > 0 {
return errs
}
+25 -1
View File
@@ -1,6 +1,7 @@
package common
import (
"reflect"
"testing"
)
@@ -10,7 +11,7 @@ func testAMIConfig() *AMIConfig {
}
}
func TestAMIConfigPrepare_Region(t *testing.T) {
func TestAMIConfigPrepare_name(t *testing.T) {
c := testAMIConfig()
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
@@ -21,3 +22,26 @@ func TestAMIConfigPrepare_Region(t *testing.T) {
t.Fatal("should have error")
}
}
func TestAMIConfigPrepare_regions(t *testing.T) {
c := testAMIConfig()
c.AMIRegions = nil
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.AMIRegions = []string{"foo"}
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
c.AMIRegions = []string{"us-east-1", "us-west-1", "us-east-1"}
if err := c.Prepare(nil); err != nil {
t.Fatalf("bad: %s", err)
}
expected := []string{"us-east-1", "us-west-1"}
if !reflect.DeepEqual(c.AMIRegions, expected) {
t.Fatalf("bad: %#v", c.AMIRegions)
}
}
@@ -0,0 +1,80 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepAMIRegionCopy struct {
Regions []string
Tags map[string]string
}
func (s *StepAMIRegionCopy) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
amis := state["amis"].(map[string]string)
ami := amis[ec2conn.Region.Name]
if len(s.Regions) == 0 {
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Copying AMI (%s) to other regions...", ami))
for _, region := range s.Regions {
ui.Message(fmt.Sprintf("Copying to: %s", region))
// Connect to the region where the AMI will be copied to
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
resp, err := regionconn.CopyImage(&ec2.CopyImage{
SourceRegion: ec2conn.Region.Name,
SourceImageId: ami,
})
if err != nil {
err := fmt.Errorf("Error Copying AMI (%s) to region (%s): %s", ami, region, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Waiting for AMI (%s) in region (%s) to become ready...", resp.ImageId, region))
if err := WaitForAMI(regionconn, resp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s", resp.ImageId, region, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Need to re-apply Tags since they are not copied with the AMI
if len(s.Tags) > 0 {
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", resp.ImageId))
var ec2Tags []ec2.Tag
for key, value := range s.Tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
}
_, err := regionconn.CreateTags([]string{resp.ImageId}, ec2Tags)
if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", resp.ImageId, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
amis[region] = resp.ImageId
}
state["amis"] = amis
return multistep.ActionContinue
}
func (s *StepAMIRegionCopy) Cleanup(state map[string]interface{}) {
// No cleanup...
}
+7 -27
View File
@@ -6,7 +6,6 @@
package ebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
@@ -25,9 +24,6 @@ type config struct {
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Tags for the AMI
Tags map[string]string
tpl *packer.ConfigTemplate
}
@@ -54,28 +50,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
// Accumulate any errors
newTags := make(map[string]string)
for k, v := range b.config.Tags {
k, err = b.config.tpl.Process(k, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing tag key %s: %s", k, err))
continue
}
v, err = b.config.tpl.Process(v, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing tag value '%s': %s", v, err))
continue
}
newTags[k] = v
}
b.config.Tags = newTags
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -130,12 +104,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&common.StepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
&awscommon.StepCreateTags{Tags: b.config.Tags},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
Tags: b.config.AMITags,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
+7 -2
View File
@@ -33,7 +33,6 @@ type Config struct {
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
Tags map[string]string
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
@@ -211,13 +210,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
&awscommon.StepCreateTags{Tags: b.config.Tags},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
ProductCodes: b.config.AMIProductCodes,
},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
Tags: b.config.AMITags,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
+1 -20
View File
@@ -27,25 +27,6 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
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 err error
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
@@ -65,7 +46,7 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
}
ui.Say("Bundling the volume...")
cmd = new(packer.RemoteCmd)
cmd := new(packer.RemoteCmd)
cmd.Command = config.BundleVolCommand
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
+14 -1
View File
@@ -43,7 +43,11 @@ type DigitalOceanClient struct {
// Creates a new client for communicating with DO
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
c := &DigitalOceanClient{
client: http.DefaultClient,
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
BaseURL: DIGITALOCEAN_API_URL,
ClientID: client,
APIKey: key,
@@ -111,6 +115,15 @@ func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
+1
View File
@@ -206,6 +206,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SSHWaitTimeout: 5 * time.Minute,
},
new(common.StepProvision),
new(stepShutdown),
new(stepPowerOff),
new(stepSnapshot),
}
+7 -8
View File
@@ -32,15 +32,14 @@ func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionHalt
}
ui.Say("Waiting for droplet to power off...")
log.Println("Waiting for poweroff event to complete...")
err = waitForDropletState("off", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// This arbitrary sleep is because we can't wait for the state
// of the droplet to be 'off', as stepShutdown should already
// have accomplished that, and the state indicator is the same.
// We just have to assume that this event will process quickly.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.eventDelay)
return multistep.ActionContinue
}
+49
View File
@@ -0,0 +1,49 @@
package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
c := state["config"].(config)
ui := state["ui"].(packer.Ui)
dropletId := state["droplet_id"].(uint)
// Sleep arbitrarily before sending the request
// 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)
err := client.ShutdownDroplet(dropletId)
if err != nil {
err := fmt.Errorf("Error shutting down droplet: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Waiting for droplet to shutdown...")
err = waitForDropletState("off", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state map[string]interface{}) {
// no cleanup
}
+73
View File
@@ -0,0 +1,73 @@
package openstack
import (
"fmt"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"os"
)
// AccessConfig is for common configuration related to openstack access
type AccessConfig struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Provider string `mapstructure:"provider"`
}
// Auth returns a valid Auth object for access to openstack services, or
// an error if the authentication couldn't be resolved.
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
username := c.Username
password := c.Password
provider := c.Provider
if username == "" {
username = os.Getenv("SDK_USERNAME")
}
if password == "" {
password = os.Getenv("SDK_PASSWORD")
}
if provider == "" {
provider = os.Getenv("SDK_PROVIDER")
}
authoptions := gophercloud.AuthOptions{
Username: username,
Password: password,
AllowReauth: true,
}
return gophercloud.Authenticate(provider, authoptions)
}
func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
templates := map[string]*string{
"username": &c.Username,
"password": &c.Password,
"provider": &c.Provider,
}
errs := make([]error, 0)
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if len(errs) > 0 {
return errs
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package openstack
import (
"testing"
)
func testAccessConfig() *AccessConfig {
return &AccessConfig{}
}
func TestAccessConfigPrepare_Region(t *testing.T) {
c := testAccessConfig()
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
+41
View File
@@ -0,0 +1,41 @@
package openstack
import (
"fmt"
"github.com/rackspace/gophercloud"
"log"
)
// Artifact is an artifact implementation that contains built images.
type Artifact struct {
// ImageId of built image
ImageId string
// BuilderId is the unique ID for the builder that created this image
BuilderIdValue string
// OpenStack connection for performing API stuff.
Conn gophercloud.CloudServersProvider
}
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*Artifact) Files() []string {
// We have no files
return nil
}
func (a *Artifact) Id() string {
return a.ImageId
}
func (a *Artifact) String() string {
return fmt.Sprintf("An image was created: %v", a.ImageId)
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d", a.ImageId)
return a.Conn.DeleteImageById(a.ImageId)
}
+39
View File
@@ -0,0 +1,39 @@
package openstack
import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer"
"testing"
)
func TestArtifact_Impl(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
var actual packer.Artifact
assert.Implementor(&Artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
expected := `b8cdf55b-c916-40bd-b190-389ec144c4ed`
a := &Artifact{
ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed",
}
result := a.Id()
assert.Equal(result, expected, "should match output")
}
func TestArtifactString(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
expected := "An image was created: b8cdf55b-c916-40bd-b190-389ec144c4ed"
a := &Artifact{
ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed",
}
result := a.String()
assert.Equal(result, expected, "should match output")
}
+130
View File
@@ -0,0 +1,130 @@
// The openstack package contains a packer.Builder implementation that
// builds Images for openstack.
package openstack
import (
//"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
)
// The unique ID for this builder
const BuilderId = "mitchellh.openstack"
type config struct {
common.PackerConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"`
tpl *packer.ConfigTemplate
}
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
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.ImageConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
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) {
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
api := &gophercloud.ApiCriteria{
Name: "cloudServersOpenStack",
Region: "DFW",
VersionId: "2",
UrlChoice: gophercloud.PublicURL,
}
csp, err := gophercloud.ServersApi(auth, *api)
if err != nil {
return nil, err
}
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = b.config
state["csp"] = csp
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&StepKeyPair{},
&StepRunSourceServer{
Name: b.config.ImageName,
Flavor: b.config.Flavor,
SourceImage: b.config.SourceImage,
},
&common.StepConnectSSH{
SSHAddress: SSHAddress(csp, b.config.SSHPort),
SSHConfig: SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepCreateImage{},
}
// 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)
}
// Build the artifact and return it
artifact := &Artifact{
ImageId: state["image"].(string),
BuilderIdValue: BuilderId,
Conn: csp,
}
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 openstack
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"username": "foo",
"password": "bar",
"provider": "foo",
"image_name": "foo",
"source_image": "foo",
"flavor": "foo",
"ssh_username": "root",
}
}
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{}{
"password": []string{},
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_ImageName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["image_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["image_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "image_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")
}
}
+45
View File
@@ -0,0 +1,45 @@
package openstack
import (
"fmt"
"github.com/mitchellh/packer/packer"
)
// ImageConfig is for common configuration related to creating Images.
type ImageConfig struct {
ImageName string `mapstructure:"image_name"`
}
func (c *ImageConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
templates := map[string]*string{
"image_name": &c.ImageName,
}
errs := make([]error, 0)
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if c.ImageName == "" {
errs = append(errs, fmt.Errorf("An image_name must be specified"))
}
if len(errs) > 0 {
return errs
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
package openstack
import (
"testing"
)
func testImageConfig() *ImageConfig {
return &ImageConfig{
ImageName: "foo",
}
}
func TestImageConfigPrepare_Region(t *testing.T) {
c := testImageConfig()
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.ImageName = ""
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
}
+86
View File
@@ -0,0 +1,86 @@
package openstack
import (
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
"time"
)
// RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image.
type RunConfig struct {
SourceImage string `mapstructure:"source_image"`
Flavor string `mapstructure:"flavor"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
// Defaults
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "1m"
}
// Validation
var err error
errs := make([]error, 0)
if c.SourceImage == "" {
errs = append(errs, errors.New("A source_image must be specified"))
}
if c.Flavor == "" {
errs = append(errs, errors.New("A flavor must be specified"))
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
templates := map[string]*string{
"flavlor": &c.Flavor,
"ssh_timeout": &c.RawSSHTimeout,
"ssh_username": &c.SSHUsername,
"source_image": &c.SourceImage,
}
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
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
}
+88
View File
@@ -0,0 +1,88 @@
package openstack
import (
"os"
"testing"
)
func init() {
// Clear out the openstack env vars so they don't
// affect our tests.
os.Setenv("SDK_USERNAME", "")
os.Setenv("SDK_PASSWORD", "")
os.Setenv("SDK_PROVIDER", "")
}
func testRunConfig() *RunConfig {
return &RunConfig{
SourceImage: "abcd",
Flavor: "m1.small",
SSHUsername: "root",
}
}
func TestRunConfigPrepare(t *testing.T) {
c := testRunConfig()
err := c.Prepare(nil)
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testRunConfig()
c.Flavor = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SourceImage(t *testing.T) {
c := testRunConfig()
c.SourceImage = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testRunConfig()
c.SSHPort = 0
if err := c.Prepare(nil); 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(nil); 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 := testRunConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testRunConfig()
c.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
+87
View File
@@ -0,0 +1,87 @@
package openstack
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"log"
"time"
)
// StateRefreshFunc is a function type used for StateChangeConf that is
// responsible for refreshing the item being watched for a state change.
//
// It returns three results. `result` is any object that will be returned
// as the final object after waiting for state change. This allows you to
// return the final updated object, for example an openstack instance after
// refreshing it.
//
// `state` is the latest state of that object. And `err` is any error that
// may have happened while refreshing the state.
type StateRefreshFunc func() (result interface{}, state string, progress int, err error)
// StateChangeConf is the configuration struct used for `WaitForState`.
type StateChangeConf struct {
Pending []string
Refresh StateRefreshFunc
StepState map[string]interface{}
Target string
}
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an openstacn server.
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc {
return func() (interface{}, string, int, error) {
resp, err := csp.ServerById(s.Id)
if err != nil {
log.Printf("Error on ServerStateRefresh: %s", err)
return nil, "", 0, err
}
return resp, resp.Status, resp.Progress, nil
}
}
// WaitForState watches an object and waits for it to achieve a certain
// state.
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
for {
var currentProgress int
var currentState string
i, currentState, currentProgress, err = conf.Refresh()
if err != nil {
return
}
if currentState == conf.Target {
return
}
if conf.StepState != nil {
if _, ok := conf.StepState[multistep.StateCancelled]; ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if currentState == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
return
}
log.Printf("Waiting for state to become: %s currently %s (%d%%)", conf.Target, currentState, currentProgress)
time.Sleep(2 * time.Second)
}
return
}
+54
View File
@@ -0,0 +1,54 @@
package openstack
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/rackspace/gophercloud"
"time"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the server AccessIPv4 setting..
func SSHAddress(csp gophercloud.CloudServersProvider, port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
for j := 0; j < 2; j++ {
s := state["server"].(*gophercloud.Server)
if s.AccessIPv4 != "" {
return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil
}
serverState, err := csp.ServerById(s.Id)
if err != nil {
return "", err
}
state["server"] = serverState
time.Sleep(1 * time.Second)
}
return "", errors.New("couldn't determine IP address for server")
}
}
// 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
}
}
+68
View File
@@ -0,0 +1,68 @@
package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
"time"
)
type stepCreateImage struct{}
func (s *stepCreateImage) Run(state map[string]interface{}) multistep.StepAction {
csp := state["csp"].(gophercloud.CloudServersProvider)
config := state["config"].(config)
server := state["server"].(*gophercloud.Server)
ui := state["ui"].(packer.Ui)
// Create the image
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
createOpts := gophercloud.CreateImage{
Name: config.ImageName,
}
imageId, err := csp.CreateImage(server.Id, createOpts)
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the Image ID in the state
ui.Say(fmt.Sprintf("Image: %s", imageId))
state["image"] = imageId
// Wait for the image to become ready
ui.Say("Waiting for image to become ready...")
if err := WaitForImage(csp, imageId); err != nil {
err := fmt.Errorf("Error waiting for image: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCreateImage) Cleanup(map[string]interface{}) {
// No cleanup...
}
// WaitForImage waits for the given Image ID to become ready.
func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error {
for {
image, err := csp.ImageById(imageId)
if err != nil {
return err
}
if image.Status == "ACTIVE" {
return nil
}
log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress)
time.Sleep(2 * time.Second)
}
}
+55
View File
@@ -0,0 +1,55 @@
package openstack
import (
"cgl.tideland.biz/identifier"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
)
type StepKeyPair struct {
keyName string
}
func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
csp := state["csp"].(gophercloud.CloudServersProvider)
ui := state["ui"].(packer.Ui)
ui.Say("Creating temporary keypair for this instance...")
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName})
if err != nil {
state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err)
return multistep.ActionHalt
}
// Set the keyname so we know to delete it later
s.keyName = keyName
// Set some state data for use in future steps
state["keyPair"] = keyName
state["privateKey"] = keyResp.PrivateKey
return multistep.ActionContinue
}
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
}
csp := state["csp"].(gophercloud.CloudServersProvider)
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary keypair...")
err := csp.DeleteKeyPair(s.keyName)
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
}
}
@@ -0,0 +1,86 @@
package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
)
type StepRunSourceServer struct {
Flavor string
Name string
SourceImage string
server *gophercloud.Server
}
func (s *StepRunSourceServer) Run(state map[string]interface{}) multistep.StepAction {
csp := state["csp"].(gophercloud.CloudServersProvider)
keyName := state["keyPair"].(string)
ui := state["ui"].(packer.Ui)
// XXX - validate image and flavor is available
server := gophercloud.NewServer{
Name: s.Name,
ImageRef: s.SourceImage,
FlavorRef: s.Flavor,
KeyPairName: keyName,
}
serverResp, err := csp.CreateServer(server)
if err != nil {
err := fmt.Errorf("Error launching source server: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.server, err = csp.ServerById(serverResp.Id)
log.Printf("server id: %s", s.server.Id)
ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id))
stateChange := StateChangeConf{
Pending: []string{"BUILD"},
Target: "ACTIVE",
Refresh: ServerStateRefreshFunc(csp, s.server),
StepState: state,
}
latestServer, err := WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.server = latestServer.(*gophercloud.Server)
state["server"] = s.server
return multistep.ActionContinue
}
func (s *StepRunSourceServer) Cleanup(state map[string]interface{}) {
if s.server == nil {
return
}
csp := state["csp"].(gophercloud.CloudServersProvider)
ui := state["ui"].(packer.Ui)
ui.Say("Terminating the source server...")
if err := csp.DeleteServerById(s.server.Id); err != nil {
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
Refresh: ServerStateRefreshFunc(csp, s.server),
Target: "DELETED",
}
WaitForState(&stateChange)
}
+22
View File
@@ -27,6 +27,7 @@ type config struct {
BootCommand []string `mapstructure:"boot_command"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
Format string `mapstructure:"format"`
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
@@ -42,6 +43,7 @@ type config struct {
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
@@ -131,6 +133,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
if b.config.Format == "" {
b.config.Format = "ovf"
}
// Errors
templates := map[string]*string{
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
@@ -145,6 +151,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
"ssh_username": &b.config.SSHUser,
"virtualbox_version_file": &b.config.VBoxVersionFile,
"vm_name": &b.config.VMName,
"format": &b.config.Format,
"boot_wait": &b.config.RawBootWait,
"shutdown_timeout": &b.config.RawShutdownTimeout,
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
@@ -197,6 +204,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if !(b.config.Format == "ovf" || b.config.Format == "ova") {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed"))
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
@@ -271,6 +283,16 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHKeyPath != "" {
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
+111
View File
@@ -8,6 +8,36 @@ import (
"testing"
)
var testPem = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
-----END RSA PRIVATE KEY-----
`
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_checksum": "foo",
@@ -58,6 +88,10 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
if b.config.VMName != "packer-foo" {
t.Errorf("bad vm name: %s", b.config.VMName)
}
if b.config.Format != "ovf" {
t.Errorf("bad format: %s", b.config.Format)
}
}
func TestBuilderPrepare_BootWait(t *testing.T) {
@@ -248,6 +282,34 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
}
}
func TestBuilderPrepare_Format(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["format"] = "illegal value"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Good
config["format"] = "ova"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Good
config["format"] = "ovf"
b = Builder{}
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()
@@ -452,6 +514,55 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
}
}
func TestBuilderPrepare_sshKeyPath(t *testing.T) {
var b Builder
config := testConfig()
config["ssh_key_path"] = ""
b = Builder{}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["ssh_key_path"] = "/i/dont/exist"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad contents
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err := tf.Write([]byte("HELLO!")); err != nil {
t.Fatalf("err: %s", err)
}
config["ssh_key_path"] = tf.Name()
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good contents
tf.Seek(0, 0)
tf.Truncate(0)
tf.Write([]byte(testPem))
config["ssh_key_path"] = tf.Name()
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestBuilderPrepare_SSHUser(t *testing.T) {
var b Builder
config := testConfig()
+38 -5
View File
@@ -4,6 +4,8 @@ import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"os"
)
func sshAddress(state map[string]interface{}) (string, error) {
@@ -14,12 +16,43 @@ func sshAddress(state map[string]interface{}) (string, error) {
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
auth := []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
}
if config.SSHKeyPath != "" {
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
if err != nil {
return nil, err
}
auth = append(auth, gossh.ClientAuthKeyring(keyring))
}
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
Auth: auth,
}, nil
}
func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
return nil, err
}
return keyring, nil
}
+1 -1
View File
@@ -50,7 +50,7 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
}
// Export the VM to an OVF
outputPath := filepath.Join(config.OutputDir, "packer.ovf")
outputPath := filepath.Join(config.OutputDir, "packer."+config.Format)
command = []string{
"export",
+45
View File
@@ -6,6 +6,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"math/rand"
"os"
@@ -27,6 +28,7 @@ type config struct {
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
DiskTypeId string `mapstructure:"disk_type_id"`
FloppyFiles []string `mapstructure:"floppy_files"`
GuestOSType string `mapstructure:"guest_os_type"`
ISOChecksum string `mapstructure:"iso_checksum"`
@@ -42,11 +44,14 @@ type config struct {
SkipCompaction bool `mapstructure:"skip_compaction"`
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHUser string `mapstructure:"ssh_username"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHSkipRequestPty bool `mapstructure:"ssh_skip_request_pty"`
ToolsUploadFlavor string `mapstructure:"tools_upload_flavor"`
ToolsUploadPath string `mapstructure:"tools_upload_path"`
VMXData map[string]string `mapstructure:"vmx_data"`
VMXTemplatePath string `mapstructure:"vmx_template_path"`
VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
@@ -84,6 +89,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.DiskSize = 40000
}
if b.config.DiskTypeId == "" {
// Default is growable virtual disk split in 2GB files.
b.config.DiskTypeId = "1"
}
if b.config.FloppyFiles == nil {
b.config.FloppyFiles = make([]string, 0)
}
@@ -145,6 +155,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
"boot_wait": &b.config.RawBootWait,
"shutdown_timeout": &b.config.RawShutdownTimeout,
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
"vmx_template_path": &b.config.VMXTemplatePath,
}
for n, ptr := range templates {
@@ -253,6 +264,16 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if b.config.SSHKeyPath != "" {
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
if b.config.SSHUser == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
@@ -291,6 +312,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs, fmt.Errorf("tools_upload_path invalid: %s", err))
}
if b.config.VMXTemplatePath != "" {
if err := b.validateVMXTemplatePath(); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vmx_template_path is invalid: %s", err))
}
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
@@ -336,6 +365,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
NoPty: b.config.SSHSkipRequestPty,
},
&stepUploadTools{},
&common.StepProvision{},
@@ -406,3 +436,18 @@ func (b *Builder) Cancel() {
b.runner.Cancel()
}
}
func (b *Builder) validateVMXTemplatePath() error {
f, err := os.Open(b.config.VMXTemplatePath)
if err != nil {
return err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return err
}
return b.config.tpl.Validate(string(data))
}
+129
View File
@@ -9,6 +9,36 @@ import (
"time"
)
var testPem = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
-----END RSA PRIVATE KEY-----
`
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_checksum": "foo",
@@ -339,6 +369,55 @@ func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
}
}
func TestBuilderPrepare_sshKeyPath(t *testing.T) {
var b Builder
config := testConfig()
config["ssh_key_path"] = ""
b = Builder{}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["ssh_key_path"] = "/i/dont/exist"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad contents
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err := tf.Write([]byte("HELLO!")); err != nil {
t.Fatalf("err: %s", err)
}
config["ssh_key_path"] = tf.Name()
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good contents
tf.Seek(0, 0)
tf.Truncate(0)
tf.Write([]byte(testPem))
config["ssh_key_path"] = tf.Name()
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestBuilderPrepare_SSHUser(t *testing.T) {
var b Builder
config := testConfig()
@@ -437,6 +516,56 @@ func TestBuilderPrepare_ToolsUploadPath(t *testing.T) {
}
}
func TestBuilderPrepare_VMXTemplatePath(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["vmx_template_path"] = "/i/dont/exist/forreal"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err := tf.Write([]byte("HELLO!")); err != nil {
t.Fatalf("err: %s", err)
}
config["vmx_template_path"] = tf.Name()
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Bad template
tf2, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf2.Name())
defer tf2.Close()
if _, err := tf2.Write([]byte("{{foo}")); err != nil {
t.Fatalf("err: %s", err)
}
config["vmx_template_path"] = tf2.Name()
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_VNCPort(t *testing.T) {
var b Builder
config := testConfig()
+1 -1
View File
@@ -15,7 +15,7 @@ type Driver interface {
CompactDisk(string) error
// CreateDisk creates a virtual disk with the given size.
CreateDisk(string, string) error
CreateDisk(string, string, string) error
// Checks if the VMX file at the given path is running.
IsRunning(string) (bool, error)
+2 -2
View File
@@ -28,8 +28,8 @@ func (d *Fusion5Driver) CompactDisk(diskPath string) error {
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
func (d *Fusion5Driver) CreateDisk(output string, size string, type_id string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
+2 -2
View File
@@ -51,12 +51,12 @@ func (d *Player5LinuxDriver) qemuCompactDisk(diskPath string) error {
return nil
}
func (d *Player5LinuxDriver) CreateDisk(output string, size string) error {
func (d *Player5LinuxDriver) CreateDisk(output string, size string, type_id string) error {
var cmd *exec.Cmd
if d.QemuImgPath != "" {
cmd = exec.Command(d.QemuImgPath, "create", "-f", "vmdk", "-o", "compat6", output, size)
} else {
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
}
if _, _, err := runAndLog(cmd); err != nil {
return err
+2 -2
View File
@@ -31,8 +31,8 @@ func (d *Workstation9Driver) CompactDisk(diskPath string) error {
return nil
}
func (d *Workstation9Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
func (d *Workstation9Driver) CreateDisk(output string, size string, type_id string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
+36 -5
View File
@@ -61,12 +61,43 @@ func sshAddress(state map[string]interface{}) (string, error) {
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
auth := []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
}
if config.SSHKeyPath != "" {
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
if err != nil {
return nil, err
}
auth = append(auth, gossh.ClientAuthKeyring(keyring))
}
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
Auth: auth,
}, nil
}
func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
return nil, err
}
return keyring, nil
}
+1 -1
View File
@@ -25,7 +25,7 @@ func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Creating virtual machine disk")
full_disk_path := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskTypeId); err != nil {
err := fmt.Errorf("Error creating disk: %s", err)
state["error"] = err
ui.Error(err.Error())
+32 -6
View File
@@ -1,13 +1,13 @@
package vmware
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"path/filepath"
"text/template"
)
type vmxTemplateData struct {
@@ -42,11 +42,37 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
ISOPath: isoPath,
}
var buf bytes.Buffer
t := template.Must(template.New("vmx").Parse(DefaultVMXTemplate))
t.Execute(&buf, tplData)
vmxTemplate := DefaultVMXTemplate
if config.VMXTemplatePath != "" {
f, err := os.Open(config.VMXTemplatePath)
if err != nil {
err := fmt.Errorf("Error reading VMX template: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
defer f.Close()
vmxData := ParseVMX(buf.String())
rawBytes, err := ioutil.ReadAll(f)
if err != nil {
err := fmt.Errorf("Error reading VMX template: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxTemplate = string(rawBytes)
}
vmxContents, err := config.tpl.Process(vmxTemplate, tplData)
if err != nil {
err := fmt.Errorf("Error procesing VMX template: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxData := ParseVMX(vmxContents)
if config.VMXData != nil {
log.Println("Setting custom VMX data...")
for k, v := range config.VMXData {
+11
View File
@@ -187,8 +187,18 @@ func (c Command) Run(env packer.Environment, args []string) int {
}
if len(errors) > 0 {
env.Ui().Machine("error-count", strconv.FormatInt(int64(len(errors)), 10))
env.Ui().Error("\n==> Some builds didn't complete successfully and had errors:")
for name, err := range errors {
// Create a UI for the machine readable stuff to be targetted
ui := &packer.TargettedUi{
Target: name,
Ui: env.Ui(),
}
ui.Machine("error", err.Error())
env.Ui().Error(fmt.Sprintf("--> %s: %s", name, err))
}
}
@@ -233,6 +243,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
ui.Machine("artifact", iStr, "nil")
}
ui.Machine("artifact", iStr, "end")
env.Ui().Say(message.String())
}
}
+21
View File
@@ -65,6 +65,27 @@ func (f *BuildOptions) AllUserVars() (map[string]string, error) {
// configured options.
func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([]packer.Build, error) {
buildNames := t.BuildNames()
checks := make(map[string][]string)
checks["except"] = f.Except
checks["only"] = f.Only
for t, ns := range checks {
for _, n := range ns {
found := false
for _, actual := range buildNames {
if actual == n {
found = true
break
}
}
if !found {
return nil, fmt.Errorf(
"Unknown build in '%s' flag: %s", t, n)
}
}
}
builds := make([]packer.Build, 0, len(buildNames))
for _, buildName := range buildNames {
if len(f.Except) > 0 {
+93
View File
@@ -1,9 +1,102 @@
package command
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testTemplate() (*packer.Template, *packer.ComponentFinder) {
tplData := `{
"builders": [
{
"type": "foo"
},
{
"type": "bar"
}
]
}`
tpl, err := packer.ParseTemplate([]byte(tplData))
if err != nil {
panic(err)
}
cf := &packer.ComponentFinder{
Builder: func(string) (packer.Builder, error) { return new(packer.MockBuilder), nil },
}
return tpl, cf
}
func TestBuildOptionsBuilds(t *testing.T) {
opts := new(BuildOptions)
bs, err := opts.Builds(testTemplate())
if err != nil {
t.Fatalf("err: %s", err)
}
if len(bs) != 2 {
t.Fatalf("bad: %d", len(bs))
}
}
func TestBuildOptionsBuilds_except(t *testing.T) {
opts := new(BuildOptions)
opts.Except = []string{"foo"}
bs, err := opts.Builds(testTemplate())
if err != nil {
t.Fatalf("err: %s", err)
}
if len(bs) != 1 {
t.Fatalf("bad: %d", len(bs))
}
if bs[0].Name() != "bar" {
t.Fatalf("bad: %s", bs[0].Name())
}
}
func TestBuildOptionsBuilds_only(t *testing.T) {
opts := new(BuildOptions)
opts.Only = []string{"foo"}
bs, err := opts.Builds(testTemplate())
if err != nil {
t.Fatalf("err: %s", err)
}
if len(bs) != 1 {
t.Fatalf("bad: %d", len(bs))
}
if bs[0].Name() != "foo" {
t.Fatalf("bad: %s", bs[0].Name())
}
}
func TestBuildOptionsBuilds_exceptNonExistent(t *testing.T) {
opts := new(BuildOptions)
opts.Except = []string{"i-dont-exist"}
_, err := opts.Builds(testTemplate())
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestBuildOptionsBuilds_onlyNonExistent(t *testing.T) {
opts := new(BuildOptions)
opts.Only = []string{"i-dont-exist"}
_, err := opts.Builds(testTemplate())
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestBuildOptionsValidate(t *testing.T) {
bf := new(BuildOptions)
+15 -1
View File
@@ -5,6 +5,7 @@ import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
@@ -60,6 +61,8 @@ func HashForType(t string) hash.Hash {
return sha1.New()
case "sha256":
return sha256.New()
case "sha512":
return sha512.New()
default:
return nil
}
@@ -188,7 +191,18 @@ func (*HTTPDownloader) Cancel() {
func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error {
log.Printf("Starting download: %s", src.String())
resp, err := http.Get(src.String())
req, err := http.NewRequest("GET", src.String(), nil)
if err != nil {
return err
}
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
+13
View File
@@ -81,6 +81,19 @@ func TestHashForType(t *testing.T) {
}
}
if h := HashForType("sha512"); h == nil {
t.Fatalf("sha512 hash is nil")
} else {
h.Write([]byte("foo"))
result := h.Sum(nil)
expected := "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7"
actual := hex.EncodeToString(result)
if actual != expected {
t.Fatalf("bad hash: %s", actual)
}
}
if HashForType("fake") != nil {
t.Fatalf("fake hash is not nil")
}
+5 -1
View File
@@ -34,6 +34,9 @@ type StepConnectSSH struct {
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
// NoPty, if true, will not request a Pty from the remote end.
NoPty bool
comm packer.Communicator
}
@@ -116,7 +119,7 @@ func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan
}
// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address, 5*time.Minute)
connFunc := ssh.ConnectFunc("tcp", address)
nc, err := connFunc()
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
@@ -128,6 +131,7 @@ func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan
config := &ssh.Config{
Connection: connFunc,
SSHConfig: sshConfig,
NoPty: s.NoPty,
}
log.Println("Attempting SSH connection...")
+271 -95
View File
@@ -10,7 +10,10 @@ import (
"io"
"log"
"net"
"os"
"path/filepath"
"sync"
"time"
)
type comm struct {
@@ -28,6 +31,9 @@ type Config struct {
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
// NoPty, if true, will not request a pty from the remote end.
NoPty bool
}
// Creates a new packer.Communicator implementation over SSH. This takes
@@ -57,15 +63,17 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if !c.config.NoPty {
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
return
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
return
}
}
log.Printf("starting remote command: %s", cmd.Command)
@@ -74,10 +82,16 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
return
}
// A channel to keep track of our done state
doneCh := make(chan struct{})
sessionLock := new(sync.Mutex)
timedOut := false
// Start a goroutine to wait for the session to end and set the
// exit boolean and status.
go func() {
defer session.Close()
err := session.Wait()
exitStatus := 0
if err != nil {
@@ -87,46 +101,61 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
}
}
sessionLock.Lock()
defer sessionLock.Unlock()
if timedOut {
// We timed out, so set the exit status to -1
exitStatus = -1
}
log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command)
cmd.SetExited(exitStatus)
close(doneCh)
}()
go func() {
failures := 0
for {
dummy, err := c.config.Connection()
if err == nil {
failures = 0
dummy.Close()
}
select {
case <-doneCh:
return
default:
}
if err != nil {
log.Printf("background SSH connection checker failure: %s", err)
failures += 1
}
if failures < 5 {
time.Sleep(5 * time.Second)
continue
}
// Acquire a lock in order to modify session state
sessionLock.Lock()
defer sessionLock.Unlock()
// Kill the connection and mark that we timed out.
log.Printf("Too many SSH connection failures. Killing it!")
c.conn.Close()
timedOut = true
return
}
}()
return
}
func (c *comm) Upload(path string, input io.Reader) error {
session, err := c.newSession()
if err != nil {
return err
}
defer session.Close()
// Get a pipe to stdin so that we can send data down
w, err := session.StdinPipe()
if err != nil {
return err
}
// 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() {
if w != nil {
w.Close()
}
}()
// 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)
@@ -136,69 +165,41 @@ func (c *comm) Upload(path string, input io.Reader) error {
// 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")
if err = session.Start("scp -vt " + target_dir); err != nil {
return err
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
return scpUploadFile(target_file, input, w, stdoutR)
}
// Determine the length of the upload content by copying it
// into an in-memory buffer. Note that this means what we upload
// must fit into memory.
log.Println("Copying input data into in-memory buffer so we can get the length")
input_memory := new(bytes.Buffer)
if _, err = io.Copy(input_memory, input); err != nil {
return err
}
return c.scpSession("scp -vt "+target_dir, scpFunc)
}
// 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")
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
// the Go SSH package.
log.Println("Upload complete, closing stdin pipe")
w.Close()
w = nil
// Wait for the SCP connection to close, meaning it has consumed all
// our data and has completed. Or has errored.
log.Println("Waiting for SSH session to complete")
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
// Otherwise, we have an ExitErorr, meaning we can just read
// the exit status
log.Printf("non-zero exit status: %d", exitErr.ExitStatus())
// If we exited with status 127, it means SCP isn't available.
// Return a more descriptive error for that.
if exitErr.ExitStatus() == 127 {
return errors.New(
"SCP failed to start. This usually means that SCP is not\n" +
"properly installed on the remote system.")
func (c *comm) UploadDir(dst string, src string, excl []string) error {
log.Printf("Upload dir '%s' to '%s'", src, dst)
scpFunc := func(w io.Writer, r *bufio.Reader) error {
uploadEntries := func() error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(src, entries, w, r)
}
return err
if src[len(src)-1] != '/' {
log.Printf("No trailing slash, creating the source directory name")
return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries)
} else {
// Trailing slash, so only upload the contents
return uploadEntries()
}
}
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
return c.scpSession("scp -rvt "+dst, scpFunc)
}
func (c *comm) Download(string, io.Writer) error {
@@ -250,6 +251,84 @@ func (c *comm) reconnect() (err error) {
return
}
func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.newSession()
if err != nil {
return err
}
defer session.Close()
// Get a pipe to stdin so that we can send data down
stdinW, err := session.StdinPipe()
if err != nil {
return err
}
// 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() {
if stdinW != nil {
stdinW.Close()
}
}()
// 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
// 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: %s", scpCommand)
if err := session.Start(scpCommand); err != nil {
return err
}
// Call our callback that executes in the context of SCP
log.Println("Started SCP session, beginning transfers...")
if err := f(stdinW, stdoutR); 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
// the Go SSH package.
log.Println("SCP session complete, closing stdin pipe.")
stdinW.Close()
stdinW = nil
// Wait for the SCP connection to close, meaning it has consumed all
// our data and has completed. Or has errored.
log.Println("Waiting for SSH session to complete.")
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
// Otherwise, we have an ExitErorr, meaning we can just read
// the exit status
log.Printf("non-zero exit status: %d", exitErr.ExitStatus())
// If we exited with status 127, it means SCP isn't available.
// Return a more descriptive error for that.
if exitErr.ExitStatus() == 127 {
return errors.New(
"SCP failed to start. This usually means that SCP is not\n" +
"properly installed on the remote system.")
}
}
return err
}
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
@@ -271,3 +350,100 @@ func checkSCPStatus(r *bufio.Reader) error {
return nil
}
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader) error {
// Determine the length of the upload content by copying it
// into an in-memory buffer. Note that this means what we upload
// must fit into memory.
log.Println("Copying input data into in-memory buffer so we can get the length")
inputBuf := new(bytes.Buffer)
if _, err := io.Copy(inputBuf, src); err != nil {
return err
}
// Start the protocol
log.Println("Beginning file upload...")
fmt.Fprintln(w, "C0644", inputBuf.Len(), dst)
err := checkSCPStatus(r)
if err != nil {
return err
}
if _, err := io.Copy(w, inputBuf); err != nil {
return err
}
fmt.Fprint(w, "\x00")
err = checkSCPStatus(r)
if err != nil {
return err
}
return nil
}
func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error) error {
log.Printf("SCP: starting directory upload: %s", name)
fmt.Fprintln(w, "D0755 0", name)
err := checkSCPStatus(r)
if err != nil {
return err
}
if err := f(); err != nil {
return err
}
fmt.Fprintln(w, "E")
if err != nil {
return err
}
return nil
}
func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error {
for _, fi := range fs {
realPath := filepath.Join(root, fi.Name())
if !fi.IsDir() {
// It is a regular file, just upload it
f, err := os.Open(realPath)
if err != nil {
return err
}
err = func() error {
defer f.Close()
return scpUploadFile(fi.Name(), f, w, r)
}()
if err != nil {
return err
}
continue
}
// It is a directory, recursively upload
err := scpUploadDirProtocol(fi.Name(), w, r, func() error {
f, err := os.Open(realPath)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(realPath, entries, w, r)
})
if err != nil {
return err
}
}
return nil
}
+2
View File
@@ -1,3 +1,5 @@
// +build !race
package ssh
import (
+3 -19
View File
@@ -1,7 +1,6 @@
package ssh
import (
"errors"
"log"
"net"
"time"
@@ -10,24 +9,9 @@ import (
// 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) {
func ConnectFunc(network, addr string) 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)
}
log.Printf("Opening conn for SSH to %s %s", network, addr)
return net.DialTimeout(network, addr, 15*time.Second)
}
}
+4
View File
@@ -20,6 +20,10 @@ type SimpleKeychain struct {
// AddPEMKey adds a simple PEM encoded private key to the keychain.
func (k *SimpleKeychain) AddPEMKey(key string) (err error) {
block, _ := pem.Decode([]byte(key))
if block == nil {
return errors.New("no block in key")
}
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return
+2
View File
@@ -23,6 +23,7 @@ const defaultConfig = `
"amazon-chroot": "packer-builder-amazon-chroot",
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"openstack": "packer-builder-openstack",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
},
@@ -39,6 +40,7 @@ const defaultConfig = `
},
"provisioners": {
"chef-solo": "packer-provisioner-chef-solo",
"file": "packer-provisioner-file",
"shell": "packer-provisioner-shell",
"salt-masterless": "packer-provisioner-salt-masterless"
+33
View File
@@ -0,0 +1,33 @@
package packer
// MockArtifact is an implementation of Artifact that can be used for tests.
type MockArtifact struct {
IdValue string
DestroyCalled bool
}
func (*MockArtifact) BuilderId() string {
return "bid"
}
func (*MockArtifact) Files() []string {
return []string{"a", "b"}
}
func (a *MockArtifact) Id() string {
id := a.IdValue
if id == "" {
id = "id"
}
return id
}
func (*MockArtifact) String() string {
return "string"
}
func (a *MockArtifact) Destroy() error {
a.DestroyCalled = true
return nil
}
+1 -1
View File
@@ -90,7 +90,7 @@ type coreBuild struct {
type coreBuildPostProcessor struct {
processor PostProcessor
processorType string
config interface{}
config map[string]interface{}
keepInputArtifact bool
}
+10 -10
View File
@@ -20,7 +20,7 @@ func testBuild() *coreBuild {
},
postProcessors: [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", make(map[string]interface{}), true},
},
},
variables: make(map[string]string),
@@ -66,7 +66,7 @@ func TestBuild_Prepare(t *testing.T) {
corePP := build.postProcessors[0][0]
pp := corePP.processor.(*TestPostProcessor)
assert.True(pp.configCalled, "config should be called")
assert.Equal(pp.configVal, []interface{}{42, packerConfig}, "config should have right value")
assert.Equal(pp.configVal, []interface{}{make(map[string]interface{}), packerConfig}, "config should have right value")
}
func TestBuild_Prepare_Twice(t *testing.T) {
@@ -231,7 +231,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", make(map[string]interface{}), false},
},
}
@@ -256,10 +256,10 @@ func TestBuild_Run_Artifacts(t *testing.T) {
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", make(map[string]interface{}), false},
},
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", 42, true},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", make(map[string]interface{}), true},
},
}
@@ -284,12 +284,12 @@ func TestBuild_Run_Artifacts(t *testing.T) {
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", 42, true},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", make(map[string]interface{}), false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", make(map[string]interface{}), true},
},
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", make(map[string]interface{}), false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", make(map[string]interface{}), false},
},
}
@@ -315,7 +315,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{
&TestPostProcessor{artifactId: "pp", keep: true}, "pp", 42, false,
&TestPostProcessor{artifactId: "pp", keep: true}, "pp", make(map[string]interface{}), false,
},
},
}
+36
View File
@@ -0,0 +1,36 @@
package packer
// MockBuilder is an implementation of Builder that can be used for tests.
// You can set some fake return values and you can keep track of what
// methods were called on the builder. It is fairly basic.
type MockBuilder struct {
ArtifactId string
PrepareCalled bool
PrepareConfig []interface{}
RunCalled bool
RunCache Cache
RunHook Hook
RunUi Ui
CancelCalled bool
}
func (tb *MockBuilder) Prepare(config ...interface{}) error {
tb.PrepareCalled = true
tb.PrepareConfig = config
return nil
}
func (tb *MockBuilder) Run(ui Ui, h Hook, c Cache) (Artifact, error) {
tb.RunCalled = true
tb.RunHook = h
tb.RunUi = ui
tb.RunCache = c
return &MockArtifact{
IdValue: tb.ArtifactId,
}, nil
}
func (tb *MockBuilder) Cancel() {
tb.CancelCalled = true
}
+21 -6
View File
@@ -33,9 +33,11 @@ type RemoteCmd struct {
// Once Exited is true, this will contain the exit code of the process.
ExitStatus int
// Internal locks and such used for safely setting some shared variables
l sync.Mutex
// Internal fields
exitCh chan struct{}
// This thing is a mutex, lock when making modifications concurrently
sync.Mutex
}
// A Communicator is the interface used to communicate with the machine
@@ -57,6 +59,16 @@ type Communicator interface {
// it completes.
Upload(string, io.Reader) error
// UploadDir uploads the contents of a directory recursively to
// the remote path. It also takes an optional slice of paths to
// ignore when uploading.
//
// The folder name of the source folder should be created unless there
// is a trailing slash on the source "/". For example: "/tmp/src" as
// the source will create a "src" directory in the destination unless
// a trailing slash is added. This is identical behavior to rsync(1).
UploadDir(string, string, []string) error
// Download downloads a file from the machine from the given remote path
// with the contents writing to the given writer. This method will
// block until it completes.
@@ -76,6 +88,9 @@ func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
originalStdout := r.Stdout
originalStderr := r.Stderr
defer func() {
r.Lock()
defer r.Unlock()
r.Stdout = originalStdout
r.Stderr = originalStderr
}()
@@ -141,8 +156,8 @@ OutputLoop:
// should be called by communicators who are running a remote command in
// order to set that the command is done.
func (r *RemoteCmd) SetExited(status int) {
r.l.Lock()
defer r.l.Unlock()
r.Lock()
defer r.Unlock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
@@ -156,11 +171,11 @@ func (r *RemoteCmd) SetExited(status int) {
// Wait waits for the remote command to complete.
func (r *RemoteCmd) Wait() {
// Make sure our condition variable is initialized.
r.l.Lock()
r.Lock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.l.Unlock()
r.Unlock()
<-r.exitCh
}
+99
View File
@@ -0,0 +1,99 @@
package packer
import (
"bytes"
"io"
"sync"
)
// MockCommunicator is a valid Communicator implementation that can be
// used for tests.
type MockCommunicator struct {
StartCalled bool
StartCmd *RemoteCmd
StartStderr string
StartStdout string
StartStdin string
StartExitStatus int
UploadCalled bool
UploadPath string
UploadData string
UploadDirDst string
UploadDirSrc string
UploadDirExclude []string
DownloadCalled bool
DownloadPath string
DownloadData string
}
func (c *MockCommunicator) Start(rc *RemoteCmd) error {
c.StartCalled = true
c.StartCmd = rc
go func() {
var wg sync.WaitGroup
if rc.Stdout != nil && c.StartStdout != "" {
wg.Add(1)
go func() {
rc.Stdout.Write([]byte(c.StartStdout))
wg.Done()
}()
}
if rc.Stderr != nil && c.StartStderr != "" {
wg.Add(1)
go func() {
rc.Stderr.Write([]byte(c.StartStderr))
wg.Done()
}()
}
if rc.Stdin != nil {
wg.Add(1)
go func() {
defer wg.Done()
var data bytes.Buffer
io.Copy(&data, rc.Stdin)
c.StartStdin = data.String()
}()
}
wg.Wait()
rc.SetExited(c.StartExitStatus)
}()
return nil
}
func (c *MockCommunicator) Upload(path string, r io.Reader) error {
c.UploadCalled = true
c.UploadPath = path
var data bytes.Buffer
if _, err := io.Copy(&data, r); err != nil {
panic(err)
}
c.UploadData = data.String()
return nil
}
func (c *MockCommunicator) UploadDir(dst string, src string, excl []string) error {
c.UploadDirDst = dst
c.UploadDirSrc = src
c.UploadDirExclude = excl
return nil
}
func (c *MockCommunicator) Download(path string, w io.Writer) error {
c.DownloadCalled = true
c.DownloadPath = path
w.Write([]byte(c.DownloadData))
return nil
}
+13
View File
@@ -0,0 +1,13 @@
package packer
import (
"testing"
)
func TestMockCommunicator_impl(t *testing.T) {
var raw interface{}
raw = new(MockCommunicator)
if _, ok := raw.(Communicator); !ok {
t.Fatal("should be a communicator")
}
}
+8 -42
View File
@@ -2,51 +2,19 @@ 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,
}
testComm := new(MockCommunicator)
testComm.StartStdout = data
testUi := &BasicUi{
Reader: new(bytes.Buffer),
Writer: uiOutput,
@@ -57,22 +25,20 @@ func TestRemoteCmd_StartWithUi(t *testing.T) {
Stdout: originalOutput,
}
go func() {
time.Sleep(100 * time.Millisecond)
rc.SetExited(0)
}()
err := rc.StartWithUi(testComm, testUi)
if err != nil {
t.Fatalf("err: %s", err)
}
if uiOutput.String() != strings.TrimSpace(data)+"\n" {
rc.Wait()
expected := strings.TrimSpace(data)
if uiOutput.String() != expected+"\n" {
t.Fatalf("bad output: '%s'", uiOutput.String())
}
if originalOutput.String() != data {
t.Fatalf("original is bad: '%s'", originalOutput.String())
if originalOutput.String() != expected {
t.Fatalf("bad: %#v", originalOutput.String())
}
}
-4
View File
@@ -37,10 +37,6 @@ func MultiErrorAppend(err error, errs ...error) *MultiError {
err = new(MultiError)
}
if err.Errors == nil {
err.Errors = make([]error, 0, len(errs))
}
err.Errors = append(err.Errors, errs...)
return err
default:
+1 -1
View File
@@ -40,7 +40,7 @@ func (b *cmdBuilder) Cancel() {
func (c *cmdBuilder) checkExit(p interface{}, cb func()) {
if c.client.Exited() && cb != nil {
cb()
} else if p != nil {
} else if p != nil && !Killed {
log.Panic(p)
}
}
+65 -36
View File
@@ -2,7 +2,6 @@ package plugin
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
@@ -10,6 +9,7 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/rpc"
"os"
"os/exec"
@@ -19,6 +19,10 @@ import (
"unicode"
)
// If this is true, then the "unexpected EOF" panic will not be
// raised throughout the clients.
var Killed = false
// This is a slice of the "managed" clients which are cleaned up when
// calling Cleanup
var managedClients = make([]*Client, 0, 5)
@@ -68,6 +72,9 @@ type ClientConfig struct {
//
// This must only be called _once_.
func CleanupClients() {
// Set the killed to true so that we don't get unexpected panics
Killed = true
// Kill all the managed clients in parallel and use a WaitGroup
// to wait for them all to finish up.
var wg sync.WaitGroup
@@ -115,6 +122,8 @@ func NewClient(config *ClientConfig) (c *Client) {
// Tells whether or not the underlying process has exited.
func (c *Client) Exited() bool {
c.l.Lock()
defer c.l.Unlock()
return c.exited
}
@@ -214,7 +223,7 @@ func (c *Client) Start() (address string, err error) {
fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
}
stdout := new(bytes.Buffer)
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
cmd := c.config.Cmd
@@ -222,7 +231,7 @@ func (c *Client) Start() (address string, err error) {
cmd.Env = append(cmd.Env, env...)
cmd.Stdin = os.Stdin
cmd.Stderr = stderr_w
cmd.Stdout = stdout
cmd.Stdout = stdout_w
log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args)
err = cmd.Start()
@@ -244,10 +253,12 @@ func (c *Client) Start() (address string, err error) {
}()
// Start goroutine to wait for process to exit
exitCh := make(chan struct{})
go func() {
// Make sure we close the write end of our stderr listener so
// that the log goroutine ends properly.
// Make sure we close the write end of our stderr/stdout so
// that the readers send EOF properly.
defer stderr_w.Close()
defer stdout_w.Close()
// Wait for the command to end.
cmd.Wait()
@@ -257,46 +268,60 @@ func (c *Client) Start() (address string, err error) {
os.Stderr.Sync()
// Mark that we exited
close(exitCh)
// Set that we exited, which takes a lock
c.l.Lock()
defer c.l.Unlock()
c.exited = true
}()
// Start goroutine that logs the stderr
go c.logStderr(stderr_r)
// Start a goroutine that is going to be reading the lines
// out of stdout
linesCh := make(chan []byte)
go func() {
defer close(linesCh)
buf := bufio.NewReader(stdout_r)
for {
line, err := buf.ReadBytes('\n')
if line != nil {
linesCh <- line
}
if err == io.EOF {
return
}
}
}()
// Make sure after we exit we read the lines from stdout forever
// so they dont' block since it is an io.Pipe
defer func() {
go func() {
for _ = range linesCh {
}
}()
}()
// Some channels for the next step
timeout := time.After(c.config.StartTimeout)
// Start looking for the address
log.Printf("Waiting for RPC address for: %s", cmd.Path)
for done := false; !done; {
select {
case <-timeout:
err = errors.New("timeout while waiting for plugin to start")
done = true
default:
}
if err == nil && c.Exited() {
err = errors.New("plugin exited before we could connect")
done = true
}
if line, lerr := stdout.ReadBytes('\n'); lerr == nil {
// Trim the address and reset the err since we were able
// to read some sort of address.
c.address = strings.TrimSpace(string(line))
address = c.address
err = nil
break
}
// If error is nil from previously, return now
if err != nil {
return
}
// Wait a bit
time.Sleep(10 * time.Millisecond)
select {
case <-timeout:
err = errors.New("timeout while waiting for plugin to start")
case <-exitCh:
err = errors.New("plugin exited before we could connect")
case line := <-linesCh:
// Trim the address and reset the err since we were able
// to read some sort of address.
c.address = strings.TrimSpace(string(line))
address = c.address
}
return
@@ -328,10 +353,14 @@ func (c *Client) rpcClient() (*rpc.Client, error) {
return nil, err
}
client, err := rpc.Dial("tcp", address)
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
return client, nil
// Make sure to set keep alive so that the connection doesn't die
tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlive(true)
return rpc.NewClient(tcpConn), nil
}
+1 -1
View File
@@ -45,7 +45,7 @@ func (c *cmdCommand) Synopsis() (result string) {
func (c *cmdCommand) checkExit(p interface{}, cb func()) {
if c.client.Exited() {
cb()
} else if p != nil {
} else if p != nil && !Killed {
log.Panic(p)
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data
func (c *cmdHook) checkExit(p interface{}, cb func()) {
if c.client.Exited() {
cb()
} else if p != nil {
} else if p != nil && !Killed {
log.Panic(p)
}
}
+4 -2
View File
@@ -94,8 +94,10 @@ func swallowInterrupts() {
signal.Notify(ch, os.Interrupt)
go func() {
<-ch
log.Println("Received interrupt signal. Ignoring.")
for {
<-ch
log.Println("Received interrupt signal. Ignoring.")
}
}()
}
+1 -1
View File
@@ -31,7 +31,7 @@ func (c *cmdPostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.
func (c *cmdPostProcessor) checkExit(p interface{}, cb func()) {
if c.client.Exited() {
cb()
} else if p != nil {
} else if p != nil && !Killed {
log.Panic(p)
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
if c.client.Exited() {
cb()
} else if p != nil {
} else if p != nil && !Killed {
log.Panic(p)
}
}
+9 -8
View File
@@ -2,6 +2,7 @@ package rpc
import (
"encoding/gob"
"fmt"
"github.com/mitchellh/packer/packer"
"log"
"net"
@@ -60,20 +61,20 @@ func (b *builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
go func() {
defer responseL.Close()
var response BuilderRunResponse
defer func() { runResponseCh <- &response }()
conn, err := responseL.Accept()
if err != nil {
log.Panic(err)
response.Err = err
return
}
defer conn.Close()
decoder := gob.NewDecoder(conn)
var response BuilderRunResponse
if err := decoder.Decode(&response); err != nil {
log.Panic(err)
response.Err = fmt.Errorf("Error waiting for Run: %s", err)
}
runResponseCh <- &response
}()
args := &BuilderRunArgs{
@@ -104,7 +105,7 @@ func (b *builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
func (b *builder) Cancel() {
if err := b.client.Call("Builder.Cancel", new(interface{}), new(interface{})); err != nil {
panic(err)
log.Printf("Error cancelling builder: %s", err)
}
}
@@ -153,7 +154,7 @@ func (b *BuilderServer) Run(args *BuilderRunArgs, reply *interface{}) error {
err := responseWriter.Encode(&BuilderRunResponse{responseErr, responseAddress})
if err != nil {
panic(err)
log.Printf("BuildServer.Run error: %s", err)
}
}()
+30 -2
View File
@@ -44,6 +44,12 @@ type CommunicatorUploadArgs struct {
ReaderAddress string
}
type CommunicatorUploadDirArgs struct {
Dst string
Src string
Exclude []string
}
func Communicator(client *rpc.Client) *communicator {
return &communicator{client}
}
@@ -78,7 +84,8 @@ func (c *communicator) Start(cmd *packer.RemoteCmd) (err error) {
conn, err := responseL.Accept()
if err != nil {
log.Panic(err)
cmd.SetExited(123)
return
}
defer conn.Close()
@@ -87,7 +94,8 @@ func (c *communicator) Start(cmd *packer.RemoteCmd) (err error) {
var finished CommandFinished
if err := decoder.Decode(&finished); err != nil {
log.Panic(err)
cmd.SetExited(123)
return
}
cmd.SetExited(finished.ExitStatus)
@@ -121,6 +129,22 @@ func (c *communicator) Upload(path string, r io.Reader) (err error) {
return
}
func (c *communicator) UploadDir(dst string, src string, exclude []string) error {
args := &CommunicatorUploadDirArgs{
Dst: dst,
Src: src,
Exclude: exclude,
}
var reply error
err := c.client.Call("Communicator.UploadDir", args, &reply)
if err == nil {
err = reply
}
return err
}
func (c *communicator) Download(path string, w io.Writer) (err error) {
// We need to create a server that can proxy that data downloaded
// into the writer because we can't gob encode a writer directly.
@@ -221,6 +245,10 @@ func (c *CommunicatorServer) Upload(args *CommunicatorUploadArgs, reply *interfa
return
}
func (c *CommunicatorServer) UploadDir(args *CommunicatorUploadDirArgs, reply *error) error {
return c.c.UploadDir(args.Dst, args.Src, args.Exclude)
}
func (c *CommunicatorServer) Download(args *CommunicatorDownloadArgs, reply *interface{}) (err error) {
writerC, err := net.Dial("tcp", args.WriterAddress)
if err != nil {
+110 -89
View File
@@ -2,52 +2,16 @@ package rpc
import (
"bufio"
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer"
"io"
"net/rpc"
"reflect"
"testing"
"time"
)
type testCommunicator struct {
startCalled bool
startCmd *packer.RemoteCmd
uploadCalled bool
uploadPath string
uploadData string
downloadCalled bool
downloadPath string
}
func (t *testCommunicator) Start(cmd *packer.RemoteCmd) error {
t.startCalled = true
t.startCmd = cmd
return nil
}
func (t *testCommunicator) Upload(path string, reader io.Reader) (err error) {
t.uploadCalled = true
t.uploadPath = path
t.uploadData, err = bufio.NewReader(reader).ReadString('\n')
return
}
func (t *testCommunicator) Download(path string, writer io.Writer) error {
t.downloadCalled = true
t.downloadPath = path
writer.Write([]byte("download\n"))
return nil
}
func TestCommunicatorRPC(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
// Create the interface to test
c := new(testCommunicator)
c := new(packer.MockCommunicator)
// Start the server
server := rpc.NewServer()
@@ -56,7 +20,9 @@ func TestCommunicatorRPC(t *testing.T) {
// Create the client over RPC and run some methods to verify it works
client, err := rpc.Dial("tcp", address)
assert.Nil(err, "should be able to connect")
if err != nil {
t.Fatalf("err: %s", err)
}
remote := Communicator(client)
// The remote command we'll use
@@ -70,53 +36,95 @@ func TestCommunicatorRPC(t *testing.T) {
cmd.Stdout = stdout_w
cmd.Stderr = stderr_w
// Send some data on stdout and stderr from the mock
c.StartStdout = "outfoo\n"
c.StartStderr = "errfoo\n"
c.StartExitStatus = 42
// Test Start
err = remote.Start(&cmd)
assert.Nil(err, "should not have an error")
// Test that we can read from stdout
c.startCmd.Stdout.Write([]byte("outfoo\n"))
bufOut := bufio.NewReader(stdout_r)
data, err := bufOut.ReadString('\n')
assert.Nil(err, "should have no problem reading stdout")
assert.Equal(data, "outfoo\n", "should be correct stdout")
// Test that we can read from stderr
c.startCmd.Stderr.Write([]byte("errfoo\n"))
bufErr := bufio.NewReader(stderr_r)
data, err = bufErr.ReadString('\n')
assert.Nil(err, "should have no problem reading stderr")
assert.Equal(data, "errfoo\n", "should be correct stderr")
// Test that we can write to stdin
stdin_w.Write([]byte("infoo\n"))
bufIn := bufio.NewReader(c.startCmd.Stdin)
data, err = bufIn.ReadString('\n')
assert.Nil(err, "should have no problem reading stdin")
assert.Equal(data, "infoo\n", "should be correct stdin")
// Test that we can get the exit status properly
c.startCmd.SetExited(42)
for i := 0; i < 5; i++ {
if cmd.Exited {
assert.Equal(cmd.ExitStatus, 42, "should have proper exit status")
break
}
time.Sleep(50 * time.Millisecond)
if err != nil {
t.Fatalf("err: %s", err)
}
assert.True(cmd.Exited, "should have exited")
// Test that we can read from stdout
bufOut := bufio.NewReader(stdout_r)
data, err := bufOut.ReadString('\n')
if err != nil {
t.Fatalf("err: %s", err)
}
if data != "outfoo\n" {
t.Fatalf("bad data: %s", data)
}
// Test that we can read from stderr
bufErr := bufio.NewReader(stderr_r)
data, err = bufErr.ReadString('\n')
if err != nil {
t.Fatalf("err: %s", err)
}
if data != "errfoo\n" {
t.Fatalf("bad data: %s", data)
}
// Test that we can write to stdin
stdin_w.Write([]byte("info\n"))
stdin_w.Close()
cmd.Wait()
if c.StartStdin != "info\n" {
t.Fatalf("bad data: %s", data)
}
// Test that we can get the exit status properly
if cmd.ExitStatus != 42 {
t.Fatalf("bad exit: %d", cmd.ExitStatus)
}
// Test that we can upload things
uploadR, uploadW := io.Pipe()
go uploadW.Write([]byte("uploadfoo\n"))
go func() {
defer uploadW.Close()
uploadW.Write([]byte("uploadfoo\n"))
}()
err = remote.Upload("foo", uploadR)
assert.Nil(err, "should not error")
assert.True(c.uploadCalled, "should be called")
assert.Equal(c.uploadPath, "foo", "should be correct path")
assert.Equal(c.uploadData, "uploadfoo\n", "should have the proper data")
if err != nil {
t.Fatalf("err: %s", err)
}
if !c.UploadCalled {
t.Fatal("should have uploaded")
}
if c.UploadPath != "foo" {
t.Fatalf("path: %s", c.UploadPath)
}
if c.UploadData != "uploadfoo\n" {
t.Fatalf("bad: %s", c.UploadData)
}
// Test that we can upload directories
dirDst := "foo"
dirSrc := "bar"
dirExcl := []string{"foo"}
err = remote.UploadDir(dirDst, dirSrc, dirExcl)
if err != nil {
t.Fatalf("err: %s", err)
}
if c.UploadDirDst != dirDst {
t.Fatalf("bad: %s", c.UploadDirDst)
}
if c.UploadDirSrc != dirSrc {
t.Fatalf("bad: %s", c.UploadDirSrc)
}
if !reflect.DeepEqual(c.UploadDirExclude, dirExcl) {
t.Fatalf("bad: %#v", c.UploadDirExclude)
}
// Test that we can download things
downloadR, downloadW := io.Pipe()
@@ -130,21 +138,34 @@ func TestCommunicatorRPC(t *testing.T) {
downloadDone <- true
}()
c.DownloadData = "download\n"
err = remote.Download("bar", downloadW)
assert.Nil(err, "should not error")
assert.True(c.downloadCalled, "should have called download")
assert.Equal(c.downloadPath, "bar", "should have correct download path")
if err != nil {
t.Fatalf("err: %s", err)
}
if !c.DownloadCalled {
t.Fatal("download should be called")
}
if c.DownloadPath != "bar" {
t.Fatalf("bad: %s", c.DownloadPath)
}
<-downloadDone
assert.Nil(downloadErr, "should not error reading download data")
assert.Equal(downloadData, "download\n", "should have the proper data")
if downloadErr != nil {
t.Fatalf("err: %s", downloadErr)
}
if downloadData != "download\n" {
t.Fatalf("bad: %s", downloadData)
}
}
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
var r packer.Communicator
c := Communicator(nil)
assert.Implementor(c, &r, "should be a Communicator")
var raw interface{}
raw = Communicator(nil)
if _, ok := raw.(packer.Communicator); !ok {
t.Fatal("should be a Communicator")
}
}
+1 -1
View File
@@ -52,7 +52,7 @@ func TestProvisionerRPC(t *testing.T) {
// Test Provision
ui := &testUi{}
comm := &testCommunicator{}
comm := new(packer.MockCommunicator)
pClient.Provision(ui, comm)
assert.True(p.provCalled, "provision should be called")
+5 -4
View File
@@ -2,6 +2,7 @@ package rpc
import (
"github.com/mitchellh/packer/packer"
"log"
"net/rpc"
)
@@ -30,7 +31,7 @@ func (u *Ui) Ask(query string) (result string, err error) {
func (u *Ui) Error(message string) {
if err := u.client.Call("Ui.Error", message, new(interface{})); err != nil {
panic(err)
log.Printf("Error in Ui RPC call: %s", err)
}
}
@@ -41,19 +42,19 @@ func (u *Ui) Machine(t string, args ...string) {
}
if err := u.client.Call("Ui.Machine", rpcArgs, new(interface{})); err != nil {
panic(err)
log.Printf("Error in Ui RPC call: %s", err)
}
}
func (u *Ui) Message(message string) {
if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil {
panic(err)
log.Printf("Error in Ui RPC call: %s", err)
}
}
func (u *Ui) Say(message string) {
if err := u.client.Call("Ui.Say", message, new(interface{})); err != nil {
panic(err)
log.Printf("Error in Ui RPC call: %s", err)
}
}
+14 -3
View File
@@ -50,7 +50,7 @@ type RawBuilderConfig struct {
type RawPostProcessorConfig struct {
Type string
KeepInputArtifact bool `mapstructure:"keep_input_artifact"`
RawConfig interface{}
RawConfig map[string]interface{}
}
// RawProvisionerConfig represents a raw, unprocessed provisioner configuration.
@@ -162,7 +162,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// are actually three different formats that the user can use to define
// a post-processor.
for i, rawV := range rawTpl.PostProcessors {
rawPP, err := parsePostProvisioner(i, rawV)
rawPP, err := parsePostProcessor(i, rawV)
if err != nil {
errors = append(errors, err...)
continue
@@ -189,6 +189,9 @@ func ParseTemplate(data []byte) (t *Template, err error) {
continue
}
// Remove the input keep_input_artifact option
delete(pp, "keep_input_artifact")
config.RawConfig = pp
}
}
@@ -218,6 +221,14 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// actively reject them as invalid configuration.
delete(v, "override")
// Verify that the override keys exist...
for name, _ := range raw.Override {
if _, ok := t.Builders[name]; !ok {
errors = append(
errors, fmt.Errorf("provisioner %d: build '%s' not found for override", i+1, name))
}
}
raw.RawConfig = v
}
@@ -260,7 +271,7 @@ func ParseTemplateFile(path string) (*Template, error) {
return ParseTemplate(data)
}
func parsePostProvisioner(i int, rawV interface{}) (result []map[string]interface{}, errors []error) {
func parsePostProcessor(i int, rawV interface{}) (result []map[string]interface{}, errors []error) {
switch v := rawV.(type) {
case string:
result = []map[string]interface{}{
+33
View File
@@ -623,6 +623,11 @@ func TestTemplate_Build(t *testing.T) {
assert.Equal(len(coreBuild.postProcessors[1]), 2, "should have correct number")
assert.False(coreBuild.postProcessors[1][0].keepInputArtifact, "shoule be correct")
assert.True(coreBuild.postProcessors[1][1].keepInputArtifact, "shoule be correct")
config := coreBuild.postProcessors[1][1].config
if _, ok := config["keep_input_artifact"]; ok {
t.Fatal("should not have keep_input_artifact")
}
}
func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
@@ -695,6 +700,34 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
assert.Equal(len(coreBuild.provisioners[0].config), 2, "should have two configs on the provisioner")
}
func TestTemplate_Build_ProvisionerOverrideBad(t *testing.T) {
data := `
{
"builders": [
{
"name": "test1",
"type": "test-builder"
}
],
"provisioners": [
{
"type": "test-prov",
"override": {
"testNope": {}
}
}
]
}
`
_, err := ParseTemplate([]byte(data))
if err == nil {
t.Fatal("should have error")
}
}
func TestTemplateBuild_variables(t *testing.T) {
data := `
{
+1 -1
View File
@@ -10,7 +10,7 @@ import (
var GitCommit string
// The version of packer.
const Version = "0.3.2"
const Version = "0.3.5"
// Any pre-release marker for the version. If this is "" (empty string),
// then it means that it is a final release. Otherwise, this is the
+10
View File
@@ -0,0 +1,10 @@
package main
import (
"github.com/mitchellh/packer/builder/openstack"
"github.com/mitchellh/packer/packer/plugin"
)
func main() {
plugin.ServeBuilder(new(openstack.Builder))
}
+1
View File
@@ -0,0 +1 @@
package main
+10
View File
@@ -0,0 +1,10 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/provisioner/chef-solo"
)
func main() {
plugin.ServeProvisioner(new(chefsolo.Provisioner))
}
@@ -0,0 +1 @@
package main
+7 -16
View File
@@ -43,20 +43,8 @@ func (p *AWSBoxPostProcessor) Configure(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
templates := map[string]*string{
"output": &p.config.OutputPath,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
}
@@ -90,8 +78,11 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact
}
// Compile the output path
outputPath, err := ProcessOutputPath(p.config.OutputPath,
p.config.PackerBuildName, "aws", artifact)
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "aws",
})
if err != nil {
return nil, false, err
}
@@ -143,7 +134,7 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact
}
// Compress the directory to the given output path
if err := DirToBox(outputPath, dir); err != nil {
if err := DirToBox(outputPath, dir, ui); err != nil {
err = fmt.Errorf("error creating box: %s", err)
return nil, false, err
}
+24 -14
View File
@@ -6,9 +6,9 @@ package vagrant
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
)
var builtins = map[string]string{
@@ -18,6 +18,8 @@ var builtins = map[string]string{
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
OutputPath string `mapstructure:"output"`
}
@@ -31,22 +33,29 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
// Store the raw configs for usage later
p.rawConfigs = raws
for _, raw := range raws {
err := mapstructure.Decode(raw, &p.config)
if err != nil {
return err
}
_, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
tpl, err := packer.NewConfigTemplate()
if err != nil {
return err
}
tpl.UserVars = p.config.PackerUserVars
// Defaults
ppExtraConfig := make(map[string]interface{})
if p.config.OutputPath == "" {
p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box"
ppExtraConfig["output"] = p.config.OutputPath
}
_, err := template.New("output").Parse(p.config.OutputPath)
if err != nil {
return fmt.Errorf("output invalid template: %s", err)
// Accumulate any errors
errs := new(packer.MultiError)
if err := tpl.Validate(p.config.OutputPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing output template: %s", err))
}
// Store the extra configuration for post-processors
@@ -55,11 +64,12 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
// TODO(mitchellh): Properly handle multiple raw configs
var mapConfig map[string]interface{}
if err := mapstructure.Decode(raws[0], &mapConfig); err != nil {
return err
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Failed to decode config: %s", err))
return errs
}
p.premade = make(map[string]packer.PostProcessor)
errors := make([]error, 0)
for k, raw := range mapConfig {
pp := keyToPostProcessor(k)
if pp == nil {
@@ -72,14 +82,14 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
ppConfigs = append(ppConfigs, raw)
if err := pp.Configure(ppConfigs...); err != nil {
errors = append(errors, err)
errs = packer.MultiErrorAppend(errs, err)
}
p.premade[k] = pp
}
if len(errors) > 0 {
return &packer.MultiError{errors}
if len(errs.Errors) > 0 {
return errs
}
return nil
+6 -23
View File
@@ -2,15 +2,14 @@ package vagrant
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"github.com/mitchellh/packer/packer"
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// OutputPathTemplate is the structure that is availalable within the
@@ -45,7 +44,7 @@ func CopyContents(dst, src string) error {
// DirToBox takes the directory and compresses it into a Vagrant-compatible
// box. This function does not perform checks to verify that dir is
// actually a proper box. This is an expected precondition.
func DirToBox(dst, dir string) error {
func DirToBox(dst, dir string, ui packer.Ui) error {
log.Printf("Turning dir into box: %s => %s", dir, dst)
dstF, err := os.Create(dst)
if err != nil {
@@ -93,6 +92,10 @@ func DirToBox(dst, dir string) error {
return err
}
if ui != nil {
ui.Message(fmt.Sprintf("Compressing: %s", header.Name))
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
@@ -108,26 +111,6 @@ func DirToBox(dst, dir string) error {
return filepath.Walk(dir, tarWalk)
}
// ProcessOutputPath takes an output path template and executes it,
// replacing variables with their respective values.
func ProcessOutputPath(path string, buildName string, provider string, artifact packer.Artifact) (string, error) {
var buf bytes.Buffer
tplData := &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: buildName,
Provider: provider,
}
t, err := template.New("output").Parse(path)
if err != nil {
return "", err
}
err = t.Execute(&buf, tplData)
return buf.String(), err
}
// WriteMetadata writes the "metadata.json" file for a Vagrant box.
func WriteMetadata(dir string, contents interface{}) error {
f, err := os.Create(filepath.Join(dir, "metadata.json"))
+99 -43
View File
@@ -1,16 +1,17 @@
package vagrant
import (
"archive/tar"
"errors"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
type VBoxBoxConfig struct {
@@ -45,20 +46,8 @@ func (p *VBoxBoxPostProcessor) Configure(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
templates := map[string]*string{
"output": &p.config.OutputPath,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
}
@@ -78,15 +67,13 @@ func (p *VBoxBoxPostProcessor) Configure(raws ...interface{}) error {
func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
var err error
tplData := &VBoxVagrantfileTemplate{}
tplData.BaseMacAddress, err = p.findBaseMacAddress(artifact)
if err != nil {
return nil, false, err
}
// Compile the output path
outputPath, err := ProcessOutputPath(p.config.OutputPath,
p.config.PackerBuildName, "virtualbox", artifact)
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "virtualbox",
})
if err != nil {
return nil, false, err
}
@@ -100,15 +87,31 @@ func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifac
// Copy all of the original contents into the temporary directory
for _, path := range artifact.Files() {
ui.Message(fmt.Sprintf("Copying: %s", path))
dstPath := filepath.Join(dir, filepath.Base(path))
if err := CopyContents(dstPath, path); err != nil {
return nil, false, err
// We treat OVA files specially, we unpack those into the temporary
// directory so we can get the resulting disk and OVF.
if extension := filepath.Ext(path); extension == ".ova" {
ui.Message(fmt.Sprintf("Unpacking OVA: %s", path))
if err := DecompressOva(dir, filepath.Base(path)); err != nil {
return nil, false, err
}
} else {
ui.Message(fmt.Sprintf("Copying: %s", path))
dstPath := filepath.Join(dir, filepath.Base(path))
if err := CopyContents(dstPath, path); err != nil {
return nil, false, err
}
}
}
// Create the Vagrantfile from the template
tplData := &VBoxVagrantfileTemplate{}
tplData.BaseMacAddress, err = p.findBaseMacAddress(dir)
if err != nil {
return nil, false, err
}
vf, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
@@ -152,28 +155,49 @@ func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifac
// Compress the directory to the given output path
ui.Message(fmt.Sprintf("Compressing box..."))
if err := DirToBox(outputPath, dir); err != nil {
if err := DirToBox(outputPath, dir, ui); err != nil {
return nil, false, err
}
return NewArtifact("virtualbox", outputPath), false, nil
}
func (p *VBoxBoxPostProcessor) findBaseMacAddress(a packer.Artifact) (string, error) {
log.Println("Looking for OVF for base mac address...")
var ovf string
for _, f := range a.Files() {
if strings.HasSuffix(f, ".ovf") {
log.Printf("OVF found: %s", f)
ovf = f
break
}
func (p *VBoxBoxPostProcessor) findOvf(dir string) (string, error) {
log.Println("Looking for OVF in artifact...")
file_matches, err := filepath.Glob(filepath.Join(dir, "*.ovf"))
if err != nil {
return "", err
}
if ovf == "" {
if len(file_matches) > 1 {
return "", errors.New("More than one OVF file in VirtualBox artifact.")
}
if len(file_matches) < 1 {
return "", errors.New("ovf file couldn't be found")
}
return file_matches[0], err
}
func (p *VBoxBoxPostProcessor) renameOVF(dir string) error {
log.Println("Looking for OVF to rename...")
ovf, err := p.findOvf(dir)
if err != nil {
return err
}
log.Printf("Renaming: '%s' => box.ovf", ovf)
return os.Rename(ovf, filepath.Join(dir, "box.ovf"))
}
func (p *VBoxBoxPostProcessor) findBaseMacAddress(dir string) (string, error) {
log.Println("Looking for OVF for base mac address...")
ovf, err := p.findOvf(dir)
if err != nil {
return "", err
}
f, err := os.Open(ovf)
if err != nil {
return "", err
@@ -195,19 +219,51 @@ func (p *VBoxBoxPostProcessor) findBaseMacAddress(a packer.Artifact) (string, er
return string(matches[1]), nil
}
func (p *VBoxBoxPostProcessor) renameOVF(dir string) error {
log.Println("Looking for OVF to rename...")
matches, err := filepath.Glob(filepath.Join(dir, "*.ovf"))
// DecompressOva takes an ova file and decompresses it into the target
// directory.
func DecompressOva(dir, src string) error {
log.Printf("Turning ova to dir: %s => %s", src, dir)
srcF, err := os.Open(src)
if err != nil {
return err
}
defer srcF.Close()
if len(matches) > 1 {
return errors.New("More than one OVF file in VirtualBox artifact.")
tarReader := tar.NewReader(srcF)
for {
hdr, err := tarReader.Next()
if hdr == nil || err == io.EOF {
break
}
info := hdr.FileInfo()
// Shouldn't be any directories, skip them
if info.IsDir() {
continue
}
// We wrap this in an anonymous function so that the defers
// inside are handled more quickly so we can give up file handles.
err = func() error {
path := filepath.Join(dir, info.Name())
output, err := os.Create(path)
if err != nil {
return err
}
defer output.Close()
os.Chmod(path, info.Mode())
os.Chtimes(path, hdr.AccessTime, hdr.ModTime)
_, err = io.Copy(output, tarReader)
return err
}()
if err != nil {
return err
}
}
log.Printf("Renaming: '%s' => box.ovf", matches[0])
return os.Rename(matches[0], filepath.Join(dir, "box.ovf"))
return nil
}
var defaultVBoxVagrantfile = `
+7 -16
View File
@@ -37,20 +37,8 @@ func (p *VMwareBoxPostProcessor) Configure(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
templates := map[string]*string{
"output": &p.config.OutputPath,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
}
@@ -70,8 +58,11 @@ func (p *VMwareBoxPostProcessor) Configure(raws ...interface{}) error {
func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
// Compile the output path
outputPath, err := ProcessOutputPath(p.config.OutputPath,
p.config.PackerBuildName, "vmware", artifact)
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "vmware",
})
if err != nil {
return nil, false, err
}
@@ -128,7 +119,7 @@ func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artif
// Compress the directory to the given output path
ui.Message(fmt.Sprintf("Compressing box..."))
if err := DirToBox(outputPath, dir); err != nil {
if err := DirToBox(outputPath, dir, ui); err != nil {
return nil, false, err
}
+318
View File
@@ -0,0 +1,318 @@
// This package implements a provisioner for Packer that uses
// Chef to provision the remote machine, specifically with chef-solo (that is,
// without a Chef server).
package chefsolo
import (
"bytes"
"encoding/json"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"path/filepath"
"strings"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
CookbookPaths []string `mapstructure:"cookbook_paths"`
ExecuteCommand string `mapstructure:"execute_command"`
InstallCommand string `mapstructure:"install_command"`
RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"`
Json map[string]interface{}
PreventSudo bool `mapstructure:"prevent_sudo"`
RunList []string `mapstructure:"run_list"`
SkipInstall bool `mapstructure:"skip_install"`
StagingDir string `mapstructure:"staging_directory"`
tpl *packer.ConfigTemplate
}
type Provisioner struct {
config Config
}
type ConfigTemplate struct {
CookbookPaths string
}
type ExecuteTemplate struct {
ConfigPath string
JsonPath string
Sudo bool
}
type InstallChefTemplate struct {
Sudo bool
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
p.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}"
}
if p.config.InstallCommand == "" {
p.config.InstallCommand = "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo {{end}}bash"
}
if p.config.RunList == nil {
p.config.RunList = make([]string, 0)
}
if p.config.StagingDir == "" {
p.config.StagingDir = "/tmp/packer-chef-solo"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
templates := map[string]*string{
"staging_dir": &p.config.StagingDir,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
sliceTemplates := map[string][]string{
"cookbook_paths": p.config.CookbookPaths,
"remote_cookbook_paths": p.config.RemoteCookbookPaths,
"run_list": p.config.RunList,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = p.config.tpl.Process(elem, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
validates := map[string]*string{
"execute_command": &p.config.ExecuteCommand,
"install_command": &p.config.InstallCommand,
}
for n, ptr := range validates {
if err := p.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
for _, path := range p.config.CookbookPaths {
pFileInfo, err := os.Stat(path)
if err != nil || !pFileInfo.IsDir() {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
if !p.config.SkipInstall {
if err := p.installChef(ui, comm); err != nil {
return fmt.Errorf("Error installing Chef: %s", err)
}
}
if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
return fmt.Errorf("Error creating staging directory: %s", err)
}
cookbookPaths := make([]string, 0, len(p.config.CookbookPaths))
for i, path := range p.config.CookbookPaths {
targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i)
if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
return fmt.Errorf("Error uploading cookbooks: %s", err)
}
cookbookPaths = append(cookbookPaths, targetPath)
}
configPath, err := p.createConfig(ui, comm, cookbookPaths)
if err != nil {
return fmt.Errorf("Error creating Chef config file: %s", err)
}
jsonPath, err := p.createJson(ui, comm)
if err != nil {
return fmt.Errorf("Error creating JSON attributes: %s", err)
}
if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil {
return fmt.Errorf("Error executing Chef: %s", err)
}
return nil
}
func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
if err := p.createDir(ui, comm, dst); err != nil {
return err
}
// Make sure there is a trailing "/" so that the directory isn't
// created on the other side.
if src[len(src)-1] != '/' {
src = src + "/"
}
return comm.UploadDir(dst, src, nil)
}
func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string) (string, error) {
ui.Message("Creating configuration file 'solo.rb'")
cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks))
for i, path := range p.config.RemoteCookbookPaths {
cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
}
for i, path := range localCookbooks {
i = len(p.config.RemoteCookbookPaths) + i
cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
}
configString, err := p.config.tpl.Process(DefaultConfigTemplate, &ConfigTemplate{
CookbookPaths: strings.Join(cookbook_paths, ","),
})
if err != nil {
return "", err
}
remotePath := filepath.Join(p.config.StagingDir, "solo.rb")
if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString))); err != nil {
return "", err
}
return remotePath, nil
}
func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
ui.Message("Creating JSON attribute file")
jsonData := make(map[string]interface{})
// Copy the configured JSON
for k, v := range p.config.Json {
jsonData[k] = v
}
// Set the run list if it was specified
if len(p.config.RunList) > 0 {
jsonData["run_list"] = p.config.RunList
}
jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return "", err
}
// Upload the bytes
remotePath := filepath.Join(p.config.StagingDir, "node.json")
if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes)); err != nil {
return "", err
}
return remotePath, nil
}
func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir))
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("mkdir -p '%s'", dir),
}
if err := cmd.StartWithUi(comm, ui); err != nil {
return err
}
if cmd.ExitStatus != 0 {
return fmt.Errorf("Non-zero exit status.")
}
return nil
}
func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteTemplate{
ConfigPath: config,
JsonPath: json,
Sudo: !p.config.PreventSudo,
})
if err != nil {
return err
}
ui.Message(fmt.Sprintf("Executing Chef: %s", command))
cmd := &packer.RemoteCmd{
Command: command,
}
if err := cmd.StartWithUi(comm, ui); err != nil {
return err
}
if cmd.ExitStatus != 0 {
return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
}
return nil
}
func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error {
ui.Message("Installing Chef...")
command, err := p.config.tpl.Process(p.config.InstallCommand, &InstallChefTemplate{
Sudo: !p.config.PreventSudo,
})
if err != nil {
return err
}
cmd := &packer.RemoteCmd{Command: command}
if err := cmd.StartWithUi(comm, ui); err != nil {
return err
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Install script exited with non-zero exit status %d", cmd.ExitStatus)
}
return nil
}
var DefaultConfigTemplate = `
cookbook_path [{{.CookbookPaths}}]
`
+53
View File
@@ -0,0 +1,53 @@
package chefsolo
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{}
}
func TestProvisioner_Impl(t *testing.T) {
var raw interface{}
raw = &Provisioner{}
if _, ok := raw.(packer.Provisioner); !ok {
t.Fatalf("must be a Provisioner")
}
}
func TestProvisionerPrepare_cookbookPaths(t *testing.T) {
var p Provisioner
path1, err := ioutil.TempDir("", "cookbooks_one")
if err != nil {
t.Fatalf("err: %s", err)
}
path2, err := ioutil.TempDir("", "cookbooks_two")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(path1)
defer os.Remove(path2)
config := testConfig()
config["cookbook_paths"] = []string{path1, path2}
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(p.config.CookbookPaths) != 2 {
t.Fatalf("unexpected: %#v", p.config.CookbookPaths)
}
if p.config.CookbookPaths[0] != path1 || p.config.CookbookPaths[1] != path2 {
t.Fatalf("unexpected: %#v", p.config.CookbookPaths)
}
}
+3 -24
View File
@@ -2,7 +2,6 @@ package file
import (
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"os"
"strings"
@@ -75,26 +74,6 @@ func TestProvisionerPrepare_EmptyDestination(t *testing.T) {
}
}
type stubUploadCommunicator struct {
dest string
data []byte
}
func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error {
return nil
}
func (suc *stubUploadCommunicator) Upload(dest string, data io.Reader) error {
var err error
suc.dest = dest
suc.data, err = ioutil.ReadAll(data)
return err
}
func (suc *stubUploadCommunicator) Start(cmd *packer.RemoteCmd) error {
return nil
}
type stubUi struct {
sayMessages string
}
@@ -138,7 +117,7 @@ func TestProvisionerProvision_SendsFile(t *testing.T) {
}
ui := &stubUi{}
comm := &stubUploadCommunicator{}
comm := &packer.MockCommunicator{}
err = p.Provision(ui, comm)
if err != nil {
t.Fatalf("should successfully provision: %s", err)
@@ -152,11 +131,11 @@ func TestProvisionerProvision_SendsFile(t *testing.T) {
t.Fatalf("should print destination filename")
}
if comm.dest != "something" {
if comm.UploadPath != "something" {
t.Fatalf("should upload to configured destination")
}
if string(comm.data) != "hello" {
if comm.UploadData != "hello" {
t.Fatalf("should upload with source file's data")
}
}
+86 -6
View File
@@ -21,9 +21,15 @@ type Config struct {
SkipBootstrap bool `mapstructure:"skip_bootstrap"`
BootstrapArgs string `mapstructure:"bootstrap_args"`
// Local path to the minion config
MinionConfig string `mapstructure:"minion_config"`
// Local path to the salt state tree
LocalStateTree string `mapstructure:"local_state_tree"`
// Local path to the salt pillar roots
LocalPillarRoots string `mapstructure:"local_pillar_roots"`
// Where files will be copied before moving to the /srv/salt directory
TempConfigDir string `mapstructure:"temp_config_dir"`
@@ -54,9 +60,11 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
errs := common.CheckUnusedConfig(md)
templates := map[string]*string{
"bootstrap_args": &p.config.BootstrapArgs,
"local_state_tree": &p.config.LocalStateTree,
"temp_config_dir": &p.config.TempConfigDir,
"bootstrap_args": &p.config.BootstrapArgs,
"minion_config": &p.config.MinionConfig,
"local_state_tree": &p.config.LocalStateTree,
"local_pillar_roots": &p.config.LocalPillarRoots,
"temp_config_dir": &p.config.TempConfigDir,
}
for n, ptr := range templates {
@@ -75,6 +83,20 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
}
if p.config.LocalPillarRoots != "" {
if _, err := os.Stat(p.config.LocalPillarRoots); err != nil {
errs = packer.MultiErrorAppend(errs,
errors.New("local_pillar_roots must exist and be accessible"))
}
}
if p.config.MinionConfig != "" {
if _, err := os.Stat(p.config.MinionConfig); err != nil {
errs = packer.MultiErrorAppend(errs,
errors.New("minion_config must exist and be accessible"))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -106,19 +128,63 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return fmt.Errorf("Error creating remote salt state directory: %s", err)
}
if p.config.MinionConfig != "" {
ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig))
if err = uploadMinionConfig(comm, fmt.Sprintf("%s/minion", p.config.TempConfigDir), p.config.MinionConfig); err != nil {
return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
}
ui.Message(fmt.Sprintf("Moving %s/minion to /etc/salt/minion", p.config.TempConfigDir))
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s/minion /etc/salt/minion", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %d", p.config.TempConfigDir, err)
}
}
ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
if err = UploadLocalDirectory(p.config.LocalStateTree, p.config.TempConfigDir, comm, ui); err != nil {
if err = UploadLocalDirectory(p.config.LocalStateTree, fmt.Sprintf("%s/states", p.config.TempConfigDir), comm, ui); err != nil {
return fmt.Errorf("Error uploading local state tree to remote: %s", err)
}
ui.Message(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir))
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)}
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s/states /srv/salt/", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err)
return fmt.Errorf("Unable to move %s/states to /srv/salt: %d", p.config.TempConfigDir, err)
}
if p.config.LocalPillarRoots != "" {
ui.Message(fmt.Sprintf("Creating remote pillar directory: %s/pillar", p.config.TempConfigDir))
cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s/pillar", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Error creating remote pillar directory: %s", err)
}
ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots))
if err = UploadLocalDirectory(p.config.LocalPillarRoots, fmt.Sprintf("%s/pillar", p.config.TempConfigDir), comm, ui); err != nil {
return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
}
ui.Message(fmt.Sprintf("Moving %s/pillar to /srv/pillar", p.config.TempConfigDir))
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s/pillar /srv/pillar", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Unable to move %s/pillar to /srv/pillar: %d", p.config.TempConfigDir, err)
}
}
ui.Message("Running highstate")
@@ -171,3 +237,17 @@ func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communi
return nil
}
func uploadMinionConfig(comm packer.Communicator, dst string, src string) error {
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("Error opening minion config: %s", err)
}
defer f.Close()
if err = comm.Upload(dst, f); err != nil {
return fmt.Errorf("Error uploading minion config: %s", err)
}
return nil
}
@@ -2,6 +2,7 @@ package saltmasterless
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
@@ -46,6 +47,29 @@ func TestProvisionerPrepare_InvalidKey(t *testing.T) {
}
}
func TestProvisionerPrepare_MinionConfig(t *testing.T) {
var p Provisioner
config := testConfig()
config["minion_config"] = "/i/dont/exist/i/think"
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
tf, err := ioutil.TempFile("", "minion")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["minion_config"] = tf.Name()
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerPrepare_LocalStateTree(t *testing.T) {
var p Provisioner
config := testConfig()
@@ -62,3 +86,20 @@ func TestProvisionerPrepare_LocalStateTree(t *testing.T) {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerPrepare_LocalPillarRoots(t *testing.T) {
var p Provisioner
config := testConfig()
config["local_pillar_roots"] = "/i/dont/exist/i/think"
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["local_pillar_roots"] = os.TempDir()
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
+47 -28
View File
@@ -236,15 +236,6 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
}
defer f.Close()
log.Printf("Uploading %s => %s", path, p.config.RemotePath)
err = comm.Upload(p.config.RemotePath, f)
if err != nil {
return fmt.Errorf("Error uploading shell script: %s", err)
}
// Close the original file since we copied it
f.Close()
// Flatten the environment variables
flattendVars := strings.Join(envVars, " ")
@@ -257,29 +248,31 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return fmt.Errorf("Error processing command: %s", err)
}
cmd := &packer.RemoteCmd{Command: command}
startTimeout := time.After(p.config.startRetryTimeout)
log.Printf("Executing command: %s", cmd.Command)
for {
if err := cmd.StartWithUi(comm, ui); err == nil {
break
}
// Create an error and log it
err = fmt.Errorf("Error executing command: %s", err)
log.Printf(err.Error())
// Check if we timed out, otherwise we retry. It is safe to
// retry since the only error case above is if the command
// failed to START.
select {
case <-startTimeout:
// Upload the file and run the command. Do this in the context of
// a single retryable function so that we don't end up with
// the case that the upload succeeded, a restart is initiated,
// and then the command is executed but the file doesn't exist
// any longer.
var cmd *packer.RemoteCmd
err = p.retryable(func() error {
if _, err := f.Seek(0, 0); err != nil {
return err
default:
time.Sleep(2 * time.Second)
}
if err := comm.Upload(p.config.RemotePath, f); err != nil {
return fmt.Errorf("Error uploading script: %s", err)
}
cmd = &packer.RemoteCmd{Command: command}
return cmd.StartWithUi(comm, ui)
})
if err != nil {
return err
}
// Close the original file since we copied it
f.Close()
if cmd.ExitStatus != 0 {
return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
}
@@ -287,3 +280,29 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return nil
}
// retryable will retry the given function over and over until a
// non-error is returned.
func (p *Provisioner) retryable(f func() error) error {
startTimeout := time.After(p.config.startRetryTimeout)
for {
var err error
if err = f(); err == nil {
return nil
}
// Create an error and log it
err = fmt.Errorf("Retryable error: %s", err)
log.Printf(err.Error())
// Check if we timed out, otherwise we retry. It is safe to
// retry since the only error case above is if the command
// failed to START.
select {
case <-startTimeout:
return err
default:
time.Sleep(2 * time.Second)
}
}
}
+11
View File
@@ -20,9 +20,19 @@ cd $DIR
GIT_COMMIT=$(git rev-parse HEAD)
GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
# If we're building a race-enabled build, then set that up.
if [ ! -z $PACKER_RACE ]; then
echo -e "${OK_COLOR}--> Building with race detection enabled${NO_COLOR}"
PACKER_RACE="-race"
fi
echo -e "${OK_COLOR}--> Installing dependencies to speed up builds...${NO_COLOR}"
go get ./...
# Compile the main Packer app
echo -e "${OK_COLOR}--> Compiling Packer${NO_COLOR}"
go build \
${PACKER_RACE} \
-ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \
-v \
-o bin/packer .
@@ -32,6 +42,7 @@ for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
PLUGIN_NAME=$(basename ${PLUGIN})
echo -e "${OK_COLOR}--> Compiling Plugin: ${PLUGIN_NAME}${NO_COLOR}"
go build \
${PACKER_RACE} \
-ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \
-v \
-o bin/packer-${PLUGIN_NAME} ${PLUGIN}
+1 -8
View File
@@ -60,15 +60,11 @@ waitAll() {
trap "kill 0" SIGINT SIGTERM EXIT
# Build our root project
xc &
xc
# Build all the plugins
for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
PLUGIN_NAME=$(basename ${PLUGIN})
(
pushd ${PLUGIN}
xc
popd
find ./pkg \
-type f \
-name ${PLUGIN_NAME} \
@@ -77,11 +73,8 @@ for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
-type f \
-name ${PLUGIN_NAME}.exe \
-execdir mv ${PLUGIN_NAME}.exe packer-${PLUGIN_NAME}.exe ';'
) &
done
waitAll
# Zip all the packages
mkdir -p ./pkg/${VERSIONDIR}/dist
for PLATFORM in $(find ./pkg/${VERSIONDIR} -mindepth 1 -maxdepth 1 -type d); do
+4 -1
View File
@@ -20,13 +20,16 @@ func setupSignalHandlers(env packer.Environment) {
<-ch
log.Println("First interrupt. Ignoring to allow plugins to clean up.")
env.Ui().Error("Interrupt signal received. Cleaning up...")
// Second interrupt. Go down hard.
<-ch
log.Println("Second interrupt. Exiting now.")
env.Ui().Error("Interrupt signal received twice. Forcefully exiting now.")
// Force kill all the plugins
// Force kill all the plugins, but mark that we're killing them
// first so that we don't get panics everywhere.
plugin.CleanupClients()
os.Exit(1)
}()
+3 -1
View File
@@ -45,7 +45,9 @@ set :images_dir, 'images'
# Use the RedCarpet Markdown engine
set :markdown_engine, :redcarpet
set :markdown, :fenced_code_blocks => true
set :markdown,
:fenced_code_blocks => true,
:with_toc_data => true
# Build-specific configuration
configure :build do
@@ -89,6 +89,10 @@ Optional:
associate with the AMI. By default no product codes are associated with
the AMI.
* `ami_regions` (array of string) - A list of regions to copy the AMI to.
Tags and attributes are copied along with the AMI. AMI copying takes time
depending on the size of the AMI, but will generally take many minutes.
* `ami_users` (array of string) - A list of account IDs that have access
to launch the resulting AMI(s). By default no additional users other than the user
creating the AMI has permissions to launch it.
@@ -117,6 +121,8 @@ Optional:
template where the `.Device` variable is replaced with the name of the
device where the volume is attached.
* `tags` (object of key/value strings) - Tags applied to the AMI.
* `unmount_command` (string) - Just like `mount_command`, except this is
the command to unmount devices.
@@ -75,6 +75,10 @@ Optional:
associate with the AMI. By default no product codes are associated with
the AMI.
* `ami_regions` (array of string) - A list of regions to copy the AMI to.
Tags and attributes are copied along with the AMI. AMI copying takes time
depending on the size of the AMI, but will generally take many minutes.
* `ami_users` (array of string) - A list of account IDs that have access
to launch the resulting AMI(s). By default no additional users other than the user
creating the AMI has permissions to launch it.
@@ -91,6 +91,10 @@ Optional:
associate with the AMI. By default no product codes are associated with
the AMI.
* `ami_regions` (array of string) - A list of regions to copy the AMI to.
Tags and attributes are copied along with the AMI. AMI copying takes time
depending on the size of the AMI, but will generally take many minutes.
* `ami_users` (array of string) - A list of account IDs that have access
to launch the resulting AMI(s). By default no additional users other than the user
creating the AMI has permissions to launch it.
@@ -0,0 +1,77 @@
---
layout: "docs"
---
# OpenStack Builder
Type: `openstack`
The `openstack` builder is able to create new images for use with
[OpenStack](http://www.openstack.org). The builder takes a source
image, runs any provisioning necessary on the image after launching it,
then creates a new reusable image. This reusable image can then be
used as the foundation of new servers that are launched within OpenStack.
The builder will create temporary keypairs that provide temporary access to
the server while the image is being created. This simplifies configuration
quite a bit.
The builder does _not_ manage images. Once it creates an image, it is up to
you to use it or delete it.
## Configuration Reference
There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
Required:
* `flavor` (string) - The ID or full URL for the desired flavor for the
server to be created.
* `image_name` (string) - The name of the resulting image.
* `password` (string) - The password used to connect to the OpenStack service.
If not specified, Packer will attempt to read this from the
`SDK_PASSWORD` environment variable.
* `provider` (string) - The provider used to connect to the OpenStack service.
If not specified, Packer will attempt to read this from the
`SDK_PROVIDER` environment variable.
* `source_image` (string) - The ID or full URL to the base image to use.
This is the image that will be used to launch a new server and provision it.
* `username` (string) - The username used to connect to the OpenStack service.
If not specified, Packer will attempt to read this from the
`SDK_USERNAME` environment variable.
Optional:
* `ssh_port` (int) - The port that SSH will be available on. Defaults to port
22.
* `ssh_timeout` (string) - The time to wait for SSH to become available
before timing out. The format of this value is a duration such as "5s"
or "5m". The default SSH timeout is "1m".
* `ssh_username` (string) - The username to use in order to communicate
over SSH to the running droplet. The default is "root".
## Basic Example
Here is a basic example. This is a working example to build a
Ubuntu 12.04 LTS (Precise Pangolin) on Rackspace OpenStack cloud offering.
<pre class="prettyprint">
{
"type": "openstack",
"username": "",
"password": "",
"provider": "",
"ssh_username": "root",
"image_name": "Test image",
"source_image": "23b564c9-c3e6-49f9-bc68-86c7a9ab5018",
"flavor": "2"
}
</pre>

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