Compare commits

..

224 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
Mitchell Hashimoto 6ef73b88be builder/amazon: fix crash case if SIGINT while waiting for instance 2013-08-17 09:45:23 -06:00
Mitchell Hashimoto 775555944e Update CHANGELOG 2013-08-16 11:02:25 -07:00
Mitchell Hashimoto 9b6e4ac92c Merge pull request #302 from mitchellh/do-encode-params
builder/digitalocean: better handling of url params in api requests
2013-08-16 11:01:40 -07:00
Jack Pearkes 885ea52e56 builder/digitalocean: better handling of url params in api requests 2013-08-16 19:47:22 +02:00
Mitchell Hashimoto 0a950a7cdc website: document handling reboots 2013-08-16 10:05:03 -07:00
Mitchell Hashimoto c0f39e5dca website: doc iso_urls 2013-08-16 09:58:00 -07:00
Mitchell Hashimoto de80940027 builder/vmware: interrupts work during VNC type on VMware 2013-08-16 09:07:27 -07:00
Mitchell Hashimoto fd0e642195 builder/amazon: modifying more than one AMI attribute type works 2013-08-15 20:28:35 -07:00
Mitchell Hashimoto 154adee43c common: detect drive letter with windows file URLs [GH-284] 2013-08-15 20:16:05 -07:00
Mitchell Hashimoto 40fccde1b0 update CHANGELOG for go-fs fix that fixes [GH-285] 2013-08-15 20:07:15 -07:00
Mitchell Hashimoto a1a7ff20c2 *: switch to packer ConfigTemplate 2013-08-15 19:17:23 -07:00
Mitchell Hashimoto f19dcf15e1 common: Remove Template, in packer now 2013-08-15 19:11:42 -07:00
Mitchell Hashimoto 53bd115e3e packer: ConfigTemplate, move from common 2013-08-15 19:11:27 -07:00
Mitchell Hashimoto 3b4bf9f206 Allow failures on tip for Travis 2013-08-15 18:02:41 -07:00
Mitchell Hashimoto fc61f8cb1d packer: shuffle some tests around to where they make more sense 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto a3407e5e3d post-processor/vagrant: process user variabels [GH-295] 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto deaec36040 post-processor/vagrant: use common.PackerConfig 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto 015933b29f builder/vmware: support iso_urls for multiple URLs [GH-110] 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto a6d4e326a7 builder/virtualbox: iso_urls for multiple ISO urls [GH-110] 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto 51fb48c5cd builder/vmware: switch to common download step 2013-08-15 17:46:41 -07:00
Mitchell Hashimoto bda45c1b53 builder/virtualbox: remove unused function 2013-08-15 17:46:40 -07:00
Mitchell Hashimoto 4188e07fc2 builder/virtualbox: switch download guest additions to use new common 2013-08-15 17:46:40 -07:00
Mitchell Hashimoto 2946d14edf common: Add new StepDownload to DRY up downloads 2013-08-15 17:46:40 -07:00
Mark Peek 78e891c2b1 Update CHANGELOG: amazon block device mappings 2013-08-15 14:32:41 -07:00
Mark Peek 507135ec84 website: document the amazon block device mappings 2013-08-15 14:32:04 -07:00
Mark Peek 40d6c46ed3 builder/amazon: add block device mappings [GH-90] 2013-08-15 14:05:08 -07:00
Mitchell Hashimoto 165bac93d0 Update CHANGELOG 2013-08-14 22:35:46 -04:00
Mitchell Hashimoto 3242498928 Merge pull request #294 from phobos182/master
builder/amazon/instance: was missing sending IAM profile data
2013-08-14 19:34:43 -07:00
Jeremy Carroll c193048b3b Was missing sending IAM profile data to instance store builders. 2013-08-14 19:03:41 -07:00
Mitchell Hashimoto 9f84f2c363 common: return -1 download percent if download hasn't started [GH-288] 2013-08-14 11:15:47 -04:00
Mitchell Hashimoto f0592e8c70 Merge pull request #290 from pierreozoux/patch-1
website: typo in the link
2013-08-14 08:11:15 -07:00
Pierre Ozoux 24d60b61c7 typo in the link 2013-08-14 15:04:02 +01:00
Mitchell Hashimoto 91af560d26 builder/virtualbox: Don't validate guest additions URL so early because it requires template 2013-08-14 09:59:09 -04:00
Mitchell Hashimoto 989a9a8d63 add issues URL in crash detect output 2013-08-13 23:59:59 -04:00
Mitchell Hashimoto 0dfaacc09d Enable panicwrap and put crash logs in crash.log 2013-08-13 23:55:05 -04:00
Mitchell Hashimoto 80e37446e9 builder/virtualbox: guest-additions_url can use Version var [GH-272] 2013-08-13 22:11:15 -04:00
Mitchell Hashimoto 5f11d86279 notifications for travis 2013-08-13 21:52:13 -04:00
Mitchell Hashimoto 7f4132537a Add travis.yml 2013-08-13 21:42:39 -04:00
Mitchell Hashimoto cfca3d8f66 add make deps 2013-08-13 21:42:08 -04:00
Mitchell Hashimoto 6fb454ef0c command/inspect: template-build should be template-builder 2013-08-13 21:35:01 -04:00
Mitchell Hashimoto 4e854f6103 website: document inspect machine-readable types 2013-08-13 21:34:44 -04:00
Mitchell Hashimoto 69902bb0f0 website: document the inspect command 2013-08-13 21:29:13 -04:00
Mitchell Hashimoto 08f724e6b7 Update CHANGELOG, 0.3.2 will be built with Go 1.1.2 2013-08-13 16:51:48 -07:00
Mitchell Hashimoto c2ccd19b15 packer: Parallelize synopsis lookup to speed up help output
Using `time` to calculate the average of 100 iterations on my machine,
`packer` went from 130ms on average to 70ms.

Previously, the load time would scale linearly about 30ms (on my
machine) on average per new command added. Now that is much much
smaller.
2013-08-13 10:16:25 -07:00
Mitchell Hashimoto 1fc4703b97 packer: get rid of unused code 2013-08-13 09:56:37 -07:00
Mitchell Hashimoto 5c26225b86 Update CHANGELOG 2013-08-13 09:54:13 -07:00
Mitchell Hashimoto 9b01b38a87 command/inspect: change logging to be %#v for better values 2013-08-13 09:53:30 -07:00
Mitchell Hashimoto 9775c00881 packer: ParseTemplateFile understands "-" to mean stdin 2013-08-13 09:52:32 -07:00
Mitchell Hashimoto 70282fedee command/inspect: help text shows machine-readable flag 2013-08-13 09:38:54 -07:00
Mitchell Hashimoto 6eceacf11f command/inspect: Inspect a templates contents
This command prints out the components of a template, and most
importantly respects the machine-readable flag so that you can
programmatically inspect a template's contents without manually parsing
the JSON.
2013-08-13 09:36:40 -07:00
Mitchell Hashimoto e22eb3aa10 packer: export template RawConfig 2013-08-13 09:11:49 -07:00
Mitchell Hashimoto 02987f6c97 packer: Export the raw template config structs 2013-08-13 09:10:49 -07:00
Mitchell Hashimoto c163d53437 packer: unknown commands show better on UI 2013-08-13 09:07:08 -07:00
Mitchell Hashimoto eb5559817b Update CHANGELOG 2013-08-13 08:56:25 -07:00
Mitchell Hashimoto 2789162fec builder/virtualbox: instantiate driver in Run method 2013-08-13 08:55:33 -07:00
Mitchell Hashimoto 6090b6ecdf builder/vmware: create driver in Run method 2013-08-13 08:54:12 -07:00
Mitchell Hashimoto 2bec873de5 Up version for dev 2013-08-12 17:23:14 -07:00
Mitchell Hashimoto 0a39650332 provisioner/salt-masterless: switch replace to that prior replace works
/cc @markpeek
2013-08-12 17:10:50 -07:00
Mitchell Hashimoto 1fd5937839 v0.3.1 2013-08-12 17:05:54 -07:00
Mitchell Hashimoto 95435e484c Update CHANGELOG 2013-08-12 17:00:42 -07:00
Mitchell Hashimoto bc097abc72 packer: Don't output up to \r with remote command, lost anyways 2013-08-12 16:55:17 -07:00
Mitchell Hashimoto 856f27bc9b provisioner/salt-masterless: replace windows slash with linux [GH-276] 2013-08-12 16:26:46 -07:00
Mitchell Hashimoto 43c6b9e6d1 builder/vmware: find files if they exist [GH-279] 2013-08-12 16:13:06 -07:00
Mitchell Hashimoto 9a2dbd54bf provisioner/shell: start_retry_timeout for reboot handling [GH-260] 2013-08-12 15:49:55 -07:00
Mitchell Hashimoto 1ec2de97a6 communicator/ssh: fix panic when client is nil on reconnect 2013-08-12 15:22:31 -07:00
Mitchell Hashimoto 7d9c7530be packer: replace stdout/stderr after StartWithUi
This just kind of shields the error with StartWithUi if we retry
2013-08-12 15:17:17 -07:00
Mitchell Hashimoto 049dc5bff5 Up version for dev 2013-08-12 14:16:55 -07:00
Mitchell Hashimoto 724e2b3c52 builder/vmware: fix windows builds 2013-08-12 14:01:50 -07: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
142 changed files with 5310 additions and 1065 deletions
+24
View File
@@ -0,0 +1,24 @@
language: go
go:
- 1.1
- tip
install: make deps
script:
- go test ./...
- go test -race ./...
notifications:
flowdock:
secure: fZrcf9rlh2IrQrlch1sHkn3YI7SKvjGnAl/zyV5D6NROe1Bbr6d3QRMuCXWWdhJHzjKmXk5rIzbqJhUc0PNF7YjxGNKSzqWMQ56KcvN1k8DzlqxpqkcA3Jbs6fXCWo2fssRtZ7hj/wOP1f5n6cc7kzHDt9dgaYJ6nO2fqNPJiTc=
irc:
channels:
- "chat.freenode.net#packer-tool"
on_success: change
on_failure: always
matrix:
allow_failures:
- go: tip
+144
View File
@@ -1,3 +1,147 @@
## 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:
* New command: `packer inspect`. This command tells you the components of
a template. It respects the `-machine-readable` flag as well so you can
parse out components of a template.
* Packer will detect its own crashes (always a bug) and save a "crash.log"
file.
* builder/virtualbox: You may now specify multiple URLs for an ISO
using "iso_url" in a template. The URLs will be tried in order.
* builder/vmware: You may now specify multiple URLs for an ISO
using "iso_url" in a template. The URLs will be tried in order.
IMPROVEMENTS:
* core: built with Go 1.1.2
* core: packer help output now loads much faster.
* builder/virtualbox: guest_additions_url can now use the `Version`
variable to get the VirtualBox version. [GH-272]
* builder/virtualbox: Do not check for VirtualBox as part of template
validation; only check at execution.
* builder/vmware: Do not check for VMware as part of template validation;
only check at execution.
* command/build: A path of "-" will read the template from stdin.
* builder/amazon: add block device mappings [GH-90]
BUG FIXES:
* windows: file URLs are easier to get right as Packer
has better parsing and error handling for Windows file paths. [GH-284]
* builder/amazon/all: Modifying more than one AMI attribute type no longer
crashes.
* builder/amazon-instance: send IAM instance profile data. [GH-294]
* builder/digitalocean: API request parameters are properly URL
encoded. [GH-281]
* builder/virtualbox: dowload progress won't be shown until download
actually starts. [GH-288]
* builder/virtualbox: floppy files names of 13 characters are now properly
written to the FAT12 filesystem. [GH-285]
* builder/vmware: dowload progress won't be shown until download
actually starts. [GH-288]
* builder/vmware: interrupt works while typing commands over VNC.
* builder/virtualbox: floppy files names of 13 characters are now properly
written to the FAT12 filesystem. [GH-285]
* post-processor/vagrant: Process user variables. [GH-295]
## 0.3.1 (August 12, 2013)
IMPROVEMENTS:
* provisioner/shell: New setting `start_retry_timeout` which is the timeout
for the provisioner to attempt to _start_ the remote process. This allows
the shell provisioner to work properly with reboots. [GH-260]
BUG FIXES:
* core: Remote command output containing '\r' now looks much better
within the Packer output.
* builder/vmware: Fix issue with finding driver files. [GH-279]
* provisioner/salt-masterless: Uploads work properly from Windows. [GH-276]
## 0.3.0 (August 12, 2013)
BACKWARDS INCOMPATIBILITIES:
+8 -6
View File
@@ -3,22 +3,24 @@ OK_COLOR=\033[32;01m
ERROR_COLOR=\033[31;01m
WARN_COLOR=\033[33;01m
all:
all: deps
@mkdir -p bin/
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
@go get -d -v ./...
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./scripts/build.sh
deps:
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
@go get -d -v ./...
@go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
format:
go fmt ./...
test:
test: deps
@echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
@go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d
go test ./...
.PHONY: all format test
.PHONY: all deps format test
+9 -2
View File
@@ -34,7 +34,7 @@ type Config struct {
SourceAmi string `mapstructure:"source_ami"`
UnmountCommand string `mapstructure:"unmount_command"`
tpl *common.Template
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -48,7 +48,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -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)
+3 -3
View File
@@ -3,7 +3,7 @@ package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"strings"
"unicode"
)
@@ -37,10 +37,10 @@ func (c *AccessConfig) Region() (aws.Region, error) {
return aws.Regions[region], nil
}
func (c *AccessConfig) Prepare(t *common.Template) []error {
func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
+58 -8
View File
@@ -2,22 +2,25 @@ package common
import (
"fmt"
"github.com/mitchellh/packer/common"
"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 *common.Template) []error {
func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
@@ -42,6 +45,7 @@ func (c *AMIConfig) Prepare(t *common.Template) []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 *common.Template) []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)
}
}
+46
View File
@@ -0,0 +1,46 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
)
// BlockDevice
type BlockDevice struct {
DeviceName string `mapstructure:"device_name"`
VirtualName string `mapstructure:"virtual_name"`
SnapshotId string `mapstructure:"snapshot_id"`
VolumeType string `mapstructure:"volume_type"`
VolumeSize int64 `mapstructure:"volume_size"`
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
IOPS int64 `mapstructure:"iops"`
}
type BlockDevices struct {
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings,squash"`
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings,squash"`
}
func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
var blockDevices []ec2.BlockDeviceMapping
for _, blockDevice := range b {
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: blockDevice.DeviceName,
VirtualName: blockDevice.VirtualName,
SnapshotId: blockDevice.SnapshotId,
VolumeType: blockDevice.VolumeType,
VolumeSize: blockDevice.VolumeSize,
DeleteOnTermination: blockDevice.DeleteOnTermination,
IOPS: blockDevice.IOPS,
})
}
return blockDevices
}
func (b *BlockDevices) BuildAMIDevices() []ec2.BlockDeviceMapping {
return buildBlockDevices(b.AMIMappings)
}
func (b *BlockDevices) BuildLaunchDevices() []ec2.BlockDeviceMapping {
return buildBlockDevices(b.LaunchMappings)
}
@@ -0,0 +1,41 @@
package common
import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/goamz/ec2"
"testing"
)
func TestBlockDevice(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
ec2Mapping := []ec2.BlockDeviceMapping{
ec2.BlockDeviceMapping{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
}
blockDevice := BlockDevice{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
}
blockDevices := BlockDevices{
AMIMappings: []BlockDevice{blockDevice},
LaunchMappings: []BlockDevice{blockDevice},
}
assert.Equal(ec2Mapping, blockDevices.BuildAMIDevices(), "should match output")
assert.Equal(ec2Mapping, blockDevices.BuildLaunchDevices(), "should match output")
}
+3 -3
View File
@@ -3,7 +3,7 @@ package common
import (
"errors"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"time"
)
@@ -27,10 +27,10 @@ type RunConfig struct {
sshTimeout time.Duration
}
func (c *RunConfig) Prepare(t *common.Template) []error {
func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
@@ -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...
}
@@ -31,20 +31,44 @@ func (s *StepModifyAMIAttributes) Run(state map[string]interface{}) multistep.St
return multistep.ActionContinue
}
options := &ec2.ModifyImageAttribute{
Description: s.Description,
AddUsers: s.Users,
AddGroups: s.Groups,
ProductCodes: s.ProductCodes,
// Construct the modify image attribute requests we're going to make.
// We need to make each separately since the EC2 API only allows changing
// one type at a kind currently.
options := make(map[string]*ec2.ModifyImageAttribute)
if s.Description != "" {
options["description"] = &ec2.ModifyImageAttribute{
Description: s.Description,
}
}
if len(s.Groups) > 0 {
options["groups"] = &ec2.ModifyImageAttribute{
AddGroups: s.Groups,
}
}
if len(s.Users) > 0 {
options["users"] = &ec2.ModifyImageAttribute{
AddUsers: s.Users,
}
}
if len(s.ProductCodes) > 0 {
options["product codes"] = &ec2.ModifyImageAttribute{
ProductCodes: s.ProductCodes,
}
}
ui.Say("Modifying AMI attributes...")
_, err := ec2conn.ModifyImageAttribute(ami, options)
if err != nil {
err := fmt.Errorf("Error modify AMI attributes: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
for name, opts := range options {
ui.Message(fmt.Sprintf("Modifying: %s", name))
_, err := ec2conn.ModifyImageAttribute(ami, opts)
if err != nil {
err := fmt.Errorf("Error modify AMI attributes: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
@@ -17,6 +17,7 @@ type StepRunSourceInstance struct {
SourceAMI string
IamInstanceProfile string
SubnetId string
BlockDevices BlockDevices
instance *ec2.Instance
}
@@ -48,6 +49,7 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
}
ui.Say("Launching a source AWS instance...")
@@ -90,7 +92,6 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
StepState: state,
}
latestInstance, err := WaitForState(&stateChange)
s.instance = latestInstance.(*ec2.Instance)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
state["error"] = err
@@ -98,6 +99,7 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
return multistep.ActionHalt
}
s.instance = latestInstance.(*ec2.Instance)
state["instance"] = s.instance
return multistep.ActionContinue
+11 -29
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"
@@ -22,12 +21,10 @@ type config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Tags for the AMI
Tags map[string]string
tpl *common.Template
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -41,7 +38,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -53,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
}
@@ -119,6 +94,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SourceAMI: b.config.SourceAmi,
IamInstanceProfile: b.config.IamInstanceProfile,
SubnetId: b.config.SubnetId,
BlockDevices: b.config.BlockDevices,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
@@ -128,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!
+3 -2
View File
@@ -19,8 +19,9 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
// Create the image
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
createOpts := &ec2.CreateImage{
InstanceId: instance.InstanceId,
Name: config.AMIName,
InstanceId: instance.InstanceId,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
}
createResp, err := ec2conn.CreateImage(createOpts)
+12 -4
View File
@@ -24,6 +24,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
AccountId string `mapstructure:"account_id"`
@@ -32,12 +33,11 @@ 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"`
tpl *common.Template
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -51,7 +51,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -193,10 +193,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
IamInstanceProfile: b.config.IamInstanceProfile,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
BlockDevices: b.config.BlockDevices,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
@@ -208,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)
@@ -20,6 +20,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
+39 -18
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,
@@ -53,10 +57,9 @@ func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
// Creates an SSH Key and returns it's id
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
// Escape the public key
pub = url.QueryEscape(pub)
params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub)
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
body, err := NewRequest(d, "ssh_keys/new", params)
if err != nil {
@@ -72,15 +75,18 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
// Destroys an SSH key
func (d DigitalOceanClient) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequest(d, path, "")
_, err := NewRequest(d, path, url.Values{})
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) {
params := fmt.Sprintf(
"name=%v&image_id=%v&size_id=%v&region_id=%v&ssh_key_ids=%v",
name, image, size, region, keyId)
params := url.Values{}
params.Set("name", name)
params.Set("size_id", fmt.Sprintf("%v", size))
params.Set("image_id", fmt.Sprintf("%v", image))
params.Set("region_id", fmt.Sprintf("%v", region))
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
body, err := NewRequest(d, "droplets/new", params)
if err != nil {
@@ -96,7 +102,7 @@ func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, re
// Destroys a droplet
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequest(d, path, "")
_, err := NewRequest(d, path, url.Values{})
return err
}
@@ -104,7 +110,16 @@ func (d DigitalOceanClient) DestroyDroplet(id uint) error {
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequest(d, path, "")
_, err := NewRequest(d, path, url.Values{})
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
}
@@ -112,7 +127,9 @@ func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
// 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)
params := fmt.Sprintf("name=%v", name)
params := url.Values{}
params.Set("name", name)
_, err := NewRequest(d, path, params)
@@ -121,7 +138,7 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
// Returns all available images.
func (d DigitalOceanClient) Images() ([]Image, error) {
resp, err := NewRequest(d, "images", "")
resp, err := NewRequest(d, "images", url.Values{})
if err != nil {
return nil, err
}
@@ -137,7 +154,7 @@ func (d DigitalOceanClient) Images() ([]Image, error) {
// Destroys an image by its ID.
func (d DigitalOceanClient) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequest(d, path, "")
_, err := NewRequest(d, path, url.Values{})
return err
}
@@ -145,7 +162,7 @@ func (d DigitalOceanClient) DestroyImage(id uint) error {
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequest(d, path, "")
body, err := NewRequest(d, path, url.Values{})
if err != nil {
return "", "", err
}
@@ -165,10 +182,14 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) {
func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
client := d.client
url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s",
DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey)
// Add the authentication parameters
params.Set("client_id", d.ClientID)
params.Set("api_key", d.APIKey)
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
var decodedResponse map[string]interface{}
+3 -2
View File
@@ -43,7 +43,7 @@ type config struct {
eventDelay time.Duration
stateTimeout time.Duration
tpl *common.Template
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -57,7 +57,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -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)
}
+74 -28
View File
@@ -18,7 +18,6 @@ const BuilderId = "mitchellh.virtualbox"
type Builder struct {
config config
driver Driver
runner multistep.Runner
}
@@ -28,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"`
@@ -38,11 +38,12 @@ type config struct {
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrl string `mapstructure:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls"`
OutputDir string `mapstructure:"output_directory"`
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"`
@@ -51,13 +52,14 @@ type config struct {
VMName string `mapstructure:"vm_name"`
RawBootWait string `mapstructure:"boot_wait"`
RawSingleISOUrl string `mapstructure:"iso_url"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
tpl *common.Template
tpl *packer.ConfigTemplate
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -66,7 +68,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -131,22 +133,25 @@ 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_path": &b.config.GuestAdditionsPath,
"guest_additions_url": &b.config.GuestAdditionsURL,
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
"guest_os_type": &b.config.GuestOSType,
"http_directory": &b.config.HTTPDir,
"iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType,
"iso_url": &b.config.ISOUrl,
"iso_url": &b.config.RawSingleISOUrl,
"output_directory": &b.config.OutputDir,
"shutdown_command": &b.config.ShutdownCommand,
"ssh_password": &b.config.SSHPassword,
"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,
@@ -161,6 +166,27 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
for i, url := range b.config.ISOUrls {
var err error
b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
}
}
validates := map[string]*string{
"guest_additions_path": &b.config.GuestAdditionsPath,
"guest_additions_url": &b.config.GuestAdditionsURL,
}
for n, ptr := range validates {
if err := b.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
for i, command := range b.config.BootCommand {
if err := b.config.tpl.Validate(command); err != nil {
errs = packer.MultiErrorAppend(errs,
@@ -178,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"))
@@ -202,14 +233,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if b.config.ISOUrl == "" {
if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
errs, errors.New("One of iso_url or iso_urls must be specified."))
} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
} else if b.config.RawSingleISOUrl != "" {
b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
}
for i, url := range b.config.ISOUrls {
b.config.ISOUrls[i], err = common.DownloadableURL(url)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url: %s", err))
errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
}
}
@@ -217,14 +255,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256)
}
if b.config.GuestAdditionsURL != "" {
b.config.GuestAdditionsURL, err = common.DownloadableURL(b.config.GuestAdditionsURL)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url: %s", err))
}
}
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = packer.MultiErrorAppend(
@@ -253,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"))
@@ -278,12 +318,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
b.driver, err = b.newDriver()
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -292,9 +326,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
// Create the driver that we'll use to communicate with VirtualBox
driver, err := b.newDriver()
if err != nil {
return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err)
}
steps := []multistep.Step{
new(stepDownloadGuestAdditions),
new(stepDownloadISO),
&common.StepDownload{
Checksum: b.config.ISOChecksum,
ChecksumType: b.config.ISOChecksumType,
Description: "ISO",
ResultKey: "iso_path",
Url: b.config.ISOUrls,
},
new(stepPrepareOutputDir),
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
@@ -325,7 +371,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state := make(map[string]interface{})
state["cache"] = cache
state["config"] = &b.config
state["driver"] = b.driver
state["driver"] = driver
state["hook"] = hook
state["ui"] = ui
+169
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) {
@@ -84,6 +118,7 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
// Test with a good one
config["boot_wait"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -210,6 +245,7 @@ func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) {
}
config["guest_additions_url"] = "http://www.packer.io"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
@@ -230,6 +266,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
// Bad
config["http_port_min"] = -500
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -238,6 +275,35 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
// Good
config["http_port_min"] = 500
config["http_port_max"] = 1000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
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)
@@ -269,6 +335,7 @@ func TestBuilderPrepare_ISOChecksum(t *testing.T) {
// Test good
config["iso_checksum"] = "FOo"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -292,6 +359,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
// Test good
config["iso_checksum_type"] = "mD5"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -303,6 +371,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
// Test unknown
config["iso_checksum_type"] = "fake"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -312,18 +381,59 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
func TestBuilderPrepare_ISOUrl(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "iso_url")
delete(config, "iso_urls")
// Test both epty
config["iso_url"] = ""
b = Builder{}
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test iso_url set
config["iso_url"] = "http://www.packer.io"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
expected := []string{"http://www.packer.io"}
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
t.Fatalf("bad: %#v", b.config.ISOUrls)
}
// Test both set
config["iso_url"] = "http://www.packer.io"
config["iso_urls"] = []string{"http://www.packer.io"}
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test just iso_urls set
delete(config, "iso_url")
config["iso_urls"] = []string{
"http://www.packer.io",
"http://www.hashicorp.com",
}
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
expected = []string{
"http://www.packer.io",
"http://www.hashicorp.com",
}
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
t.Fatalf("bad: %#v", b.config.ISOUrls)
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
@@ -338,6 +448,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
defer os.RemoveAll(dir)
config["output_directory"] = dir
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -345,6 +456,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -364,6 +476,7 @@ func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
// Test with a good one
config["shutdown_timeout"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -377,6 +490,7 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
// Bad
config["ssh_host_port_min"] = 1000
config["ssh_host_port_max"] = 500
b = Builder{}
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -384,6 +498,7 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
// Bad
config["ssh_host_port_min"] = -500
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -392,23 +507,75 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
// Good
config["ssh_host_port_min"] = 500
config["ssh_host_port_max"] = 1000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
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()
config["ssh_username"] = ""
b = Builder{}
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["ssh_username"] = "exists"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -432,6 +599,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
// Test with a bad value
config["ssh_wait_timeout"] = "this is not good"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -439,6 +607,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
// Test with a good one
config["ssh_wait_timeout"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
+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
}
@@ -2,8 +2,6 @@ package virtualbox
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
@@ -13,7 +11,6 @@ import (
"log"
"os"
"strings"
"time"
)
var additionsVersionMap = map[string]string{
@@ -21,6 +18,10 @@ var additionsVersionMap = map[string]string{
"4.1.23": "4.1.22",
}
type guestAdditionsUrlTemplate struct {
Version string
}
// This step uploads a file containing the VirtualBox version, which
// can be useful for various provisioning reasons.
//
@@ -30,7 +31,6 @@ type stepDownloadGuestAdditions struct{}
func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
var action multistep.StepAction
cache := state["cache"].(packer.Cache)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
config := state["config"].(*config)
@@ -61,87 +61,56 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep
}
}
checksumBytes, err := hex.DecodeString(checksum)
if err != nil {
state["error"] = fmt.Errorf("Couldn't decode checksum into bytes: %s", checksum)
return multistep.ActionHalt
}
// Use the provided source (URL or file path) or generate it
url := config.GuestAdditionsURL
if url == "" {
if url != "" {
tplData := &guestAdditionsUrlTemplate{
Version: version,
}
url, err = config.tpl.Process(url, tplData)
if err != nil {
err := fmt.Errorf("Error preparing guest additions url: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
} else {
url = fmt.Sprintf(
"http://download.virtualbox.org/virtualbox/%s/%s",
version,
additionsName)
}
log.Printf("Guest additions URL: %s", url)
log.Printf("Acquiring lock to download the guest additions ISO.")
cachePath := cache.Lock(url)
defer cache.Unlock(url)
downloadConfig := &common.DownloadConfig{
Url: url,
TargetPath: cachePath,
Hash: sha256.New(),
Checksum: checksumBytes,
url, err = common.DownloadableURL(url)
if err != nil {
err := fmt.Errorf("Error preparing guest additions url: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
download := common.NewDownloadClient(downloadConfig)
ui.Say("Downloading VirtualBox guest additions. Progress will be shown periodically.")
state["guest_additions_path"], action = s.progressDownload(download, state)
return action
log.Printf("Guest additions URL: %s", url)
downStep := &common.StepDownload{
Checksum: checksum,
ChecksumType: "sha256",
Description: "Guest additions",
ResultKey: "guest_additions_path",
Url: []string{url},
}
return downStep.Run(state)
}
func (s *stepDownloadGuestAdditions) Cleanup(state map[string]interface{}) {}
func (s *stepDownloadGuestAdditions) progressDownload(c *common.DownloadClient, state map[string]interface{}) (string, multistep.StepAction) {
ui := state["ui"].(packer.Ui)
var result string
downloadCompleteCh := make(chan error, 1)
// Start a goroutine to actually do the download...
go func() {
var err error
result, err = c.Get()
downloadCompleteCh <- err
}()
progressTicker := time.NewTicker(5 * time.Second)
defer progressTicker.Stop()
// A loop that handles showing progress as well as timing out and handling
// interrupts and all that.
DownloadWaitLoop:
for {
select {
case err := <-downloadCompleteCh:
if err != nil {
state["error"] = fmt.Errorf("Error downloading: %s", err)
return "", multistep.ActionHalt
}
break DownloadWaitLoop
case <-progressTicker.C:
ui.Message(fmt.Sprintf("Download progress: %d%%", c.PercentProgress()))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return "", multistep.ActionHalt
}
}
}
return result, multistep.ActionContinue
}
func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]interface{}, additionsVersion string, additionsName string) (string, multistep.StepAction) {
// First things first, we get the list of checksums for the files available
// for this version.
checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", additionsVersion)
checksumsUrl := fmt.Sprintf(
"http://download.virtualbox.org/virtualbox/%s/SHA256SUMS",
additionsVersion)
checksumsFile, err := ioutil.TempFile("", "packer")
if err != nil {
@@ -151,25 +120,23 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]in
return "", multistep.ActionHalt
}
defer os.Remove(checksumsFile.Name())
checksumsFile.Close()
downloadConfig := &common.DownloadConfig{
Url: checksumsUrl,
TargetPath: checksumsFile.Name(),
Hash: nil,
downStep := &common.StepDownload{
Description: "Guest additions checksums",
ResultKey: "guest_additions_checksums_path",
TargetPath: checksumsFile.Name(),
Url: []string{checksumsUrl},
}
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
download := common.NewDownloadClient(downloadConfig)
checksumsPath, action := s.progressDownload(download, state)
if action != multistep.ActionContinue {
action := downStep.Run(state)
if action == multistep.ActionHalt {
return "", action
}
// Next, we find the checksum for the file we're looking to download.
// It is an error if the checksum cannot be found.
checksumsF, err := os.Open(checksumsPath)
checksumsF, err := os.Open(state["guest_additions_checksums_path"].(string))
if err != nil {
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
return "", multistep.ActionHalt
@@ -199,7 +166,8 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]in
}
if checksum == "" {
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
state["error"] = fmt.Errorf(
"The checksum for the file '%s' could not be found.", additionsName)
return "", multistep.ActionHalt
}
-87
View File
@@ -1,87 +0,0 @@
package virtualbox
import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// This step downloads the ISO specified.
//
// Uses:
// cache packer.Cache
// config *config
// ui packer.Ui
//
// Produces:
// iso_path string
type stepDownloadISO struct{}
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
checksum, err := hex.DecodeString(config.ISOChecksum)
if err != nil {
state["error"] = fmt.Errorf("Error parsing checksum: %s", err)
return multistep.ActionHalt
}
log.Printf("Acquiring lock to download the ISO.")
cachePath := cache.Lock(config.ISOUrl)
defer cache.Unlock(config.ISOUrl)
downloadConfig := &common.DownloadConfig{
Url: config.ISOUrl,
TargetPath: cachePath,
CopyFile: false,
Hash: common.HashForType(config.ISOChecksumType),
Checksum: checksum,
}
download := common.NewDownloadClient(downloadConfig)
downloadCompleteCh := make(chan error, 1)
go func() {
ui.Say("Copying or downloading ISO. Progress will be reported periodically.")
cachePath, err = download.Get()
downloadCompleteCh <- err
}()
progressTicker := time.NewTicker(5 * time.Second)
defer progressTicker.Stop()
DownloadWaitLoop:
for {
select {
case err := <-downloadCompleteCh:
if err != nil {
err := fmt.Errorf("Error downloading ISO: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
break DownloadWaitLoop
case <-progressTicker.C:
ui.Message(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return multistep.ActionHalt
}
}
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
+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",
+85 -18
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"
@@ -19,7 +20,6 @@ const BuilderId = "mitchellh.vmware"
type Builder struct {
config config
driver Driver
runner multistep.Runner
}
@@ -28,11 +28,12 @@ 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"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrl string `mapstructure:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls"`
VMName string `mapstructure:"vm_name"`
OutputDir string `mapstructure:"output_directory"`
Headless bool `mapstructure:"headless"`
@@ -43,22 +44,26 @@ 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"`
RawBootWait string `mapstructure:"boot_wait"`
RawSingleISOUrl string `mapstructure:"iso_url"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
tpl *common.Template
tpl *packer.ConfigTemplate
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -67,7 +72,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
@@ -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)
}
@@ -135,7 +145,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
"http_directory": &b.config.HTTPDir,
"iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType,
"iso_url": &b.config.ISOUrl,
"iso_url": &b.config.RawSingleISOUrl,
"output_directory": &b.config.OutputDir,
"shutdown_command": &b.config.ShutdownCommand,
"ssh_password": &b.config.SSHPassword,
@@ -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 {
@@ -156,6 +167,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
for i, url := range b.config.ISOUrls {
var err error
b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
}
}
for i, command := range b.config.BootCommand {
if err := b.config.tpl.Validate(command); err != nil {
errs = packer.MultiErrorAppend(errs,
@@ -218,14 +238,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if b.config.ISOUrl == "" {
if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
errs, errors.New("One of iso_url or iso_urls must be specified."))
} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
} else if b.config.RawSingleISOUrl != "" {
b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
}
for i, url := range b.config.ISOUrls {
b.config.ISOUrls[i], err = common.DownloadableURL(url)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url: %s", err))
errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
}
}
@@ -237,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."))
@@ -275,17 +312,19 @@ 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"))
}
b.driver, err = NewDriver()
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VMware driver: %s", err))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -294,12 +333,24 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
// Initialize the driver that will handle our interaction with VMware
driver, err := NewDriver()
if err != nil {
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
}
// Seed the random number generator
rand.Seed(time.Now().UTC().UnixNano())
steps := []multistep.Step{
&stepPrepareTools{},
&stepDownloadISO{},
&common.StepDownload{
Checksum: b.config.ISOChecksum,
ChecksumType: b.config.ISOChecksumType,
Description: "ISO",
ResultKey: "iso_path",
Url: b.config.ISOUrls,
},
&stepPrepareOutputDir{},
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
@@ -314,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{},
@@ -327,7 +379,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state := make(map[string]interface{})
state["cache"] = cache
state["config"] = &b.config
state["driver"] = b.driver
state["driver"] = driver
state["hook"] = hook
state["ui"] = ui
@@ -384,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))
}
+185
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",
@@ -52,6 +82,7 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
// Test with a good one
config["boot_wait"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -71,6 +102,7 @@ func TestBuilderPrepare_ISOChecksum(t *testing.T) {
// Test good
config["iso_checksum"] = "FOo"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -94,6 +126,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
// Test good
config["iso_checksum_type"] = "mD5"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -105,6 +138,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
// Test unknown
config["iso_checksum_type"] = "fake"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -202,6 +236,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
// Bad
config["http_port_min"] = -500
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -210,6 +245,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
// Good
config["http_port_min"] = 500
config["http_port_max"] = 1000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -231,18 +267,59 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
func TestBuilderPrepare_ISOUrl(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "iso_url")
delete(config, "iso_urls")
// Test both epty
config["iso_url"] = ""
b = Builder{}
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test iso_url set
config["iso_url"] = "http://www.packer.io"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
expected := []string{"http://www.packer.io"}
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
t.Fatalf("bad: %#v", b.config.ISOUrls)
}
// Test both set
config["iso_url"] = "http://www.packer.io"
config["iso_urls"] = []string{"http://www.packer.io"}
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test just iso_urls set
delete(config, "iso_url")
config["iso_urls"] = []string{
"http://www.packer.io",
"http://www.hashicorp.com",
}
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
expected = []string{
"http://www.packer.io",
"http://www.hashicorp.com",
}
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
t.Fatalf("bad: %#v", b.config.ISOUrls)
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
@@ -257,6 +334,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
defer os.RemoveAll(dir)
config["output_directory"] = dir
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -264,6 +342,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -283,12 +362,62 @@ func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
// Test with a good one
config["shutdown_timeout"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
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()
@@ -300,6 +429,7 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
}
config["ssh_username"] = "exists"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -347,6 +477,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
// Test with a good one
config["ssh_wait_timeout"] = "5s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
@@ -370,6 +501,7 @@ func TestBuilderPrepare_ToolsUploadPath(t *testing.T) {
// Test with a bad value
config["tools_upload_path"] = "{{{nope}"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -377,12 +509,63 @@ func TestBuilderPrepare_ToolsUploadPath(t *testing.T) {
// Test with a good one
config["tools_upload_path"] = "hey"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
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()
@@ -397,6 +580,7 @@ func TestBuilderPrepare_VNCPort(t *testing.T) {
// Bad
config["vnc_port_min"] = -500
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
@@ -405,6 +589,7 @@ func TestBuilderPrepare_VNCPort(t *testing.T) {
// Good
config["vnc_port_min"] = 500
config["vnc_port_max"] = 1000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
+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
}
@@ -27,7 +27,7 @@ func workstationFindVdiskManager() (string, error) {
}
func workstationFindVMware() (string, error) {
path, _ := exec.LookPath("vmware.exe")
path, err := exec.LookPath("vmware.exe")
if err == nil {
return path, nil
}
@@ -36,7 +36,7 @@ func workstationFindVMware() (string, error) {
}
func workstationFindVmrun() (string, error) {
path, _ := exec.LookPath("vmrun.exe")
path, err := exec.LookPath("vmrun.exe")
if err == nil {
return path, nil
}
@@ -45,7 +45,7 @@ func workstationFindVmrun() (string, error) {
}
func workstationToolsIsoPath(flavor string) string {
return findFile(flavor+".iso", workstationProgramFilePaths()), nil
return findFile(flavor+".iso", workstationProgramFilePaths())
}
func workstationDhcpLeasesPath(device string) string {
@@ -56,11 +56,11 @@ func workstationDhcpLeasesPath(device string) string {
return path
}
return findFile("vmnetdhcp.leases", workstationDataFilePaths()), nil
return findFile("vmnetdhcp.leases", workstationDataFilePaths())
}
func workstationVmnetnatConfPath() string {
return findFile("vmnetnat.conf", workstationDataFilePaths()), nil
return findFile("vmnetnat.conf", workstationDataFilePaths())
}
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
@@ -141,7 +141,7 @@ func findFile(file string, paths []string) string {
path = normalizePath(path)
log.Printf("Searching for file '%s'", path)
if _, err := os.Stat(path); err != nil {
if _, err := os.Stat(path); err == nil {
log.Printf("Found file '%s'", path)
return path
}
@@ -184,7 +184,7 @@ func workstationProgramFilePaths() []string {
// workstationDataFilePaths returns a list of paths that are eligible
// to contain data files we may want such as vmnet NAT configuration files.
func workstationDataFilePaths() []string {
leasesPath, err := workstationVmnetDhcpLeasesPathFromRegistry()
leasesPath, err := workstationDhcpLeasesPathRegistry()
if err != nil {
log.Printf("Error getting DHCP leases path: %s", err)
}
@@ -198,7 +198,7 @@ func workstationDataFilePaths() []string {
paths = append(paths, os.Getenv("VMWARE_DATA"))
}
if path != "" {
if leasesPath != "" {
paths = append(paths, leasesPath)
}
+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 {
-89
View File
@@ -1,89 +0,0 @@
package vmware
import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// This step downloads the ISO specified.
//
// Uses:
// cache packer.Cache
// config *config
// ui packer.Ui
//
// Produces:
// iso_path string
type stepDownloadISO struct{}
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
checksum, err := hex.DecodeString(config.ISOChecksum)
if err != nil {
err := fmt.Errorf("Error parsing checksum: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Acquiring lock to download the ISO.")
cachePath := cache.Lock(config.ISOUrl)
defer cache.Unlock(config.ISOUrl)
downloadConfig := &common.DownloadConfig{
Url: config.ISOUrl,
TargetPath: cachePath,
CopyFile: false,
Hash: common.HashForType(config.ISOChecksumType),
Checksum: checksum,
}
download := common.NewDownloadClient(downloadConfig)
downloadCompleteCh := make(chan error, 1)
go func() {
ui.Say("Copying or downloading ISO. Progress will be reported periodically.")
cachePath, err = download.Get()
downloadCompleteCh <- err
}()
progressTicker := time.NewTicker(5 * time.Second)
defer progressTicker.Stop()
DownloadWaitLoop:
for {
select {
case err := <-downloadCompleteCh:
if err != nil {
err := fmt.Errorf("Error downloading ISO: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
break DownloadWaitLoop
case <-progressTicker.C:
ui.Say(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return multistep.ActionHalt
}
}
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
+6
View File
@@ -96,6 +96,12 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
return multistep.ActionHalt
}
// Check for interrupts between typing things so we can cancel
// since this isn't the fastest thing.
if _, ok := state[multistep.StateCancelled]; ok {
return multistep.ActionHalt
}
vncSendString(c, command)
}
+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())
}
}
+118
View File
@@ -0,0 +1,118 @@
package inspect
import (
"flag"
"fmt"
"github.com/mitchellh/packer/packer"
"log"
"sort"
"strings"
)
type Command struct{}
func (Command) Help() string {
return strings.TrimSpace(helpText)
}
func (c Command) Synopsis() string {
return "see components of a template"
}
func (c Command) Run(env packer.Environment, args []string) int {
flags := flag.NewFlagSet("inspect", flag.ContinueOnError)
flags.Usage = func() { env.Ui().Say(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
return 1
}
// Read the file into a byte array so that we can parse the template
log.Printf("Reading template: %#v", args[0])
tpl, err := packer.ParseTemplateFile(args[0])
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
}
// Convenience...
ui := env.Ui()
// Variables
ui.Say("Variables and their defaults:\n")
if len(tpl.Variables) == 0 {
ui.Say(" <No variables>")
} else {
keys := make([]string, 0, len(tpl.Variables))
max := 0
for k, _ := range tpl.Variables {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Variables[k]
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s = %s", k, padding, v)
ui.Machine("template-variable", k, v)
ui.Say(output)
}
}
ui.Say("")
// Builders
ui.Say("Builders:\n")
if len(tpl.Builders) == 0 {
ui.Say(" <No builders>")
} else {
keys := make([]string, 0, len(tpl.Builders))
max := 0
for k, _ := range tpl.Builders {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Builders[k]
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s", k, padding)
if v.Name != v.Type {
output = fmt.Sprintf("%s (%s)", output, v.Type)
}
ui.Machine("template-builder", k, v.Type)
ui.Say(output)
}
}
ui.Say("")
// Provisioners
ui.Say("Provisioners:\n")
if len(tpl.Provisioners) == 0 {
ui.Say(" <No provisioners>")
} else {
for _, v := range tpl.Provisioners {
ui.Machine("template-provisioner", v.Type)
ui.Say(fmt.Sprintf(" %s", v.Type))
}
}
return 0
}
+14
View File
@@ -0,0 +1,14 @@
package inspect
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}
+13
View File
@@ -0,0 +1,13 @@
package inspect
const helpText = `
Usage: packer inspect TEMPLATE
Inspects a template, parsing and outputting the components a template
defines. This does not validate the contents of a template (other than
basic syntax by necessity).
Options:
-machine-readable Machine-readable output
`
+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)
+9
View File
@@ -64,6 +64,15 @@ func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metada
// a completely valid URL. For example, the original URL might be "local/file.iso"
// which isn't a valid URL. DownloadableURL will return "file:///local/file.iso"
func DownloadableURL(original string) (string, error) {
if runtime.GOOS == "windows" {
// If the distance to the first ":" is just one character, assume
// we're dealing with a drive letter and thus a file path.
idx := strings.Index(original, ":")
if idx == 1 {
original = "file:///" + original
}
}
url, err := url.Parse(original)
if err != nil {
return "", err
+18 -4
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
}
@@ -148,12 +151,12 @@ func (d *DownloadClient) Get() (string, error) {
}
// PercentProgress returns the download progress as a percentage.
func (d *DownloadClient) PercentProgress() uint {
func (d *DownloadClient) PercentProgress() int {
if d.downloader == nil {
return 0
return -1
}
return uint((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
return int((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
}
// VerifyChecksum tests that the path matches the checksum for the
@@ -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...")
+138
View File
@@ -0,0 +1,138 @@
package common
import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// StepDownload downloads a remote file using the download client within
// this package. This step handles setting up the download configuration,
// progress reporting, interrupt handling, etc.
//
// Uses:
// cache packer.Cache
// ui packer.Ui
type StepDownload struct {
// The checksum and the type of the checksum for the download
Checksum string
ChecksumType string
// A short description of the type of download being done. Example:
// "ISO" or "Guest Additions"
Description string
// The name of the key where the final path of the ISO will be put
// into the state.
ResultKey string
// The path where the result should go, otherwise it goes to the
// cache directory.
TargetPath string
// A list of URLs to attempt to download this thing.
Url []string
}
func (s *StepDownload) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
ui := state["ui"].(packer.Ui)
var checksum []byte
if s.Checksum != "" {
var err error
checksum, err = hex.DecodeString(s.Checksum)
if err != nil {
state["error"] = fmt.Errorf("Error parsing checksum: %s", err)
return multistep.ActionHalt
}
}
ui.Say(fmt.Sprintf("Downloading or copying %s", s.Description))
var finalPath string
for _, url := range s.Url {
ui.Message(fmt.Sprintf("Downloading or copying: %s", url))
targetPath := s.TargetPath
if targetPath == "" {
log.Printf("Acquiring lock to download: %s", url)
targetPath = cache.Lock(url)
defer cache.Unlock(url)
}
config := &DownloadConfig{
Url: url,
TargetPath: targetPath,
CopyFile: false,
Hash: HashForType(s.ChecksumType),
Checksum: checksum,
}
path, err, retry := s.download(config, state)
if err != nil {
ui.Message(fmt.Sprintf("Error downloading: %s", err))
}
if !retry {
return multistep.ActionHalt
}
if err == nil {
finalPath = path
break
}
}
if finalPath == "" {
err := fmt.Errorf("%s download failed.", s.Description)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state[s.ResultKey] = finalPath
return multistep.ActionContinue
}
func (s *StepDownload) Cleanup(map[string]interface{}) {}
func (s *StepDownload) download(config *DownloadConfig, state map[string]interface{}) (string, error, bool) {
var path string
ui := state["ui"].(packer.Ui)
download := NewDownloadClient(config)
downloadCompleteCh := make(chan error, 1)
go func() {
var err error
path, err = download.Get()
downloadCompleteCh <- err
}()
progressTicker := time.NewTicker(5 * time.Second)
defer progressTicker.Stop()
for {
select {
case err := <-downloadCompleteCh:
if err != nil {
return "", err, true
}
return path, nil, true
case <-progressTicker.C:
progress := download.PercentProgress()
if progress >= 0 {
ui.Message(fmt.Sprintf("Download progress: %d%%", progress))
}
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return "", nil, false
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepDownload_Impl(t *testing.T) {
var raw interface{}
raw = new(StepDownload)
if _, ok := raw.(multistep.Step); !ok {
t.Fatalf("download should be a step")
}
}
+274 -89
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,23 +101,165 @@ 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 {
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash seperators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
return scpUploadFile(target_file, input, w, stdoutR)
}
return c.scpSession("scp -vt "+target_dir, scpFunc)
}
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)
}
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()
}
}
return c.scpSession("scp -rvt "+dst, scpFunc)
}
func (c *comm) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *comm) newSession() (session *ssh.Session, err error) {
log.Println("opening new ssh session")
if c.client == nil {
err = errors.New("client not available")
} else {
session, err = c.client.NewSession()
}
if err != nil {
log.Printf("ssh session open error: '%s', attempting reconnect", err)
if err := c.reconnect(); err != nil {
return nil, err
}
return c.client.NewSession()
}
return session, nil
}
func (c *comm) reconnect() (err error) {
if c.conn != nil {
c.conn.Close()
}
// Set the conn and client to nil since we'll recreate it
c.conn = nil
c.client = nil
log.Printf("reconnecting to TCP connection for SSH")
c.conn, err = c.config.Connection()
if err != nil {
log.Printf("reconnection error: %s", err)
return
}
log.Printf("handshaking with SSH")
c.client, err = ssh.Client(c.conn, c.config.SSHConfig)
if err != nil {
log.Printf("handshake error: %s", err)
}
return
}
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
w, err := session.StdinPipe()
stdinW, err := session.StdinPipe()
if err != nil {
return err
}
@@ -111,8 +267,8 @@ func (c *comm) Upload(path string, input io.Reader) error {
// 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()
if stdinW != nil {
stdinW.Close()
}
}()
@@ -127,56 +283,29 @@ func (c *comm) Upload(path string, input io.Reader) error {
stderr := new(bytes.Buffer)
session.Stderr = stderr
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash seperators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
log.Println("Starting remote scp process in sink mode")
if err = session.Start("scp -vt " + target_dir); err != nil {
log.Println("Starting remote scp process: %s", scpCommand)
if err := session.Start(scpCommand); err != nil {
return err
}
// 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
}
// 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 {
// 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("Upload complete, closing stdin pipe")
w.Close()
w = nil
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")
log.Println("Waiting for SSH session to complete.")
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
@@ -197,50 +326,9 @@ func (c *comm) Upload(path string, input io.Reader) error {
}
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
}
func (c *comm) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *comm) newSession() (*ssh.Session, error) {
log.Println("opening new ssh session")
session, err := c.client.NewSession()
if err != nil {
log.Printf("ssh session open error: '%s', attempting reconnect", err)
if err := c.reconnect(); err != nil {
return nil, err
}
return c.client.NewSession()
}
return session, nil
}
func (c *comm) reconnect() (err error) {
if c.conn != nil {
c.conn.Close()
}
log.Printf("reconnecting to TCP connection for SSH")
c.conn, err = c.config.Connection()
if err != nil {
log.Printf("reconnection error: %s", err)
return
}
log.Printf("handshaking with SSH")
c.client, err = ssh.Client(c.conn, c.config.SSHConfig)
if err != nil {
log.Printf("handshake error: %s", err)
}
return
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
@@ -262,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
+3
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"
},
@@ -30,6 +31,7 @@ const defaultConfig = `
"commands": {
"build": "packer-command-build",
"fix": "packer-command-fix",
"inspect": "packer-command-inspect",
"validate": "packer-command-validate"
},
@@ -38,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"
+81 -29
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/panicwrap"
"io"
"io/ioutil"
"log"
@@ -15,32 +16,68 @@ import (
)
func main() {
// Setup logging if PACKER_LOG is set.
// Log to PACKER_LOG_PATH if it is set, otherwise default to stderr.
var logOutput io.Writer = ioutil.Discard
if os.Getenv("PACKER_LOG") != "" {
logOutput = os.Stderr
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
var err error
logOutput, err = os.Create(logPath)
if err != nil {
fmt.Fprintf(
os.Stderr,
"Couldn't open '%s' for logging: %s",
logPath, err)
os.Exit(1)
}
}
}
log.SetOutput(logOutput)
// Call realMain instead of doing the work here so we can use
// `defer` statements within the function and have them work properly.
// (defers aren't called with os.Exit)
os.Exit(realMain())
}
// realMain is executed from main and returns the exit status to exit with.
func realMain() int {
// If there is no explicit number of Go threads to use, then set it
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
// Determine where logs should go in general (requested by the user)
logWriter, err := logOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
return 1
}
// We also always send logs to a temporary file that we use in case
// there is a panic. Otherwise, we delete it.
logTempFile, err := ioutil.TempFile("", "packer-log")
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1
}
defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Reset the log variables to minimize work in the subprocess
os.Setenv("PACKER_LOG", "")
os.Setenv("PACKER_LOG_FILE", "")
// Create the configuration for panicwrap and wrap our executable
wrapConfig := &panicwrap.WrapConfig{
Handler: panicHandler(logTempFile),
Writer: io.MultiWriter(logTempFile, logWriter),
}
exitStatus, err := panicwrap.Wrap(wrapConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
return 1
}
if exitStatus >= 0 {
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
return wrappedMain()
}
// wrappedMain is called only when we're wrapped by panicwrap and
// returns the exit status to exit with.
func wrappedMain() int {
log.SetOutput(os.Stderr)
log.Printf(
"Packer Version: %s %s %s",
packer.Version, packer.VersionPrerelease, packer.GitCommit)
@@ -52,7 +89,7 @@ func main() {
config, err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
os.Exit(1)
return 1
}
log.Printf("Packer config: %+v", config)
@@ -65,12 +102,12 @@ func main() {
cacheDir, err = filepath.Abs(cacheDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
os.Exit(1)
return 1
}
if err := os.MkdirAll(cacheDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
os.Exit(1)
return 1
}
log.Printf("Setting cache directory: %s", cacheDir)
@@ -100,8 +137,7 @@ func main() {
env, err := packer.NewEnvironment(envConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
plugin.CleanupClients()
os.Exit(1)
return 1
}
setupSignalHandlers(env)
@@ -109,12 +145,10 @@ func main() {
exitCode, err := env.Cli(args)
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
plugin.CleanupClients()
os.Exit(1)
return 1
}
plugin.CleanupClients()
os.Exit(exitCode)
return exitCode
}
// extractMachineReadable checks the args for the machine readable
@@ -178,3 +212,21 @@ func loadConfig() (*config, error) {
return &config, nil
}
// logOutput determines where we should send logs (if anywhere).
func logOutput() (logOutput io.Writer, err error) {
logOutput = ioutil.Discard
if os.Getenv("PACKER_LOG") != "" {
logOutput = os.Stderr
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
var err error
logOutput, err = os.Create(logPath)
if err != nil {
return nil, err
}
}
}
return
}
+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
}
+47 -8
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.
@@ -72,6 +84,17 @@ func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
defer stdout_w.Close()
defer stderr_w.Close()
// Retain the original stdout/stderr that we can replace back in.
originalStdout := r.Stdout
originalStderr := r.Stderr
defer func() {
r.Lock()
defer r.Unlock()
r.Stdout = originalStdout
r.Stderr = originalStderr
}()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
@@ -108,9 +131,9 @@ OutputLoop:
for {
select {
case output := <-stderrCh:
ui.Message(strings.TrimSpace(output))
ui.Message(r.cleanOutputLine(output))
case output := <-stdoutCh:
ui.Message(strings.TrimSpace(output))
ui.Message(r.cleanOutputLine(output))
case <-exitCh:
break OutputLoop
}
@@ -133,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{})
@@ -148,11 +171,27 @@ 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
}
// cleanOutputLine cleans up a line so that '\r' don't muck up the
// UI output when we're reading from a remote command.
func (r *RemoteCmd) cleanOutputLine(line string) string {
// Trim surrounding whitespace
line = strings.TrimSpace(line)
// Trim up to the first carriage return, since that text would be
// lost anyways.
idx := strings.LastIndex(line, "\r")
if idx > -1 {
line = line[idx+1:]
}
return line
}
+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())
}
}
@@ -1,4 +1,4 @@
package common
package packer
import (
"bytes"
@@ -8,19 +8,19 @@ import (
"time"
)
// Template processes string data as a text/template with some common
// ConfigTemplate processes string data as a text/template with some common
// elements and functions available. Plugin creators should process as
// many fields as possible through this.
type Template struct {
type ConfigTemplate struct {
UserVars map[string]string
root *template.Template
i int
}
// NewTemplate creates a new template processor.
func NewTemplate() (*Template, error) {
result := &Template{
// NewConfigTemplate creates a new configuration template processor.
func NewConfigTemplate() (*ConfigTemplate, error) {
result := &ConfigTemplate{
UserVars: make(map[string]string),
}
@@ -34,7 +34,7 @@ func NewTemplate() (*Template, error) {
}
// Process processes a single string, compiling and executing the template.
func (t *Template) Process(s string, data interface{}) (string, error) {
func (t *ConfigTemplate) Process(s string, data interface{}) (string, error) {
tpl, err := t.root.New(t.nextTemplateName()).Parse(s)
if err != nil {
return "", err
@@ -49,7 +49,7 @@ func (t *Template) Process(s string, data interface{}) (string, error) {
}
// Validate the template.
func (t *Template) Validate(s string) error {
func (t *ConfigTemplate) Validate(s string) error {
root, err := t.root.Clone()
if err != nil {
return err
@@ -59,7 +59,7 @@ func (t *Template) Validate(s string) error {
return err
}
func (t *Template) nextTemplateName() string {
func (t *ConfigTemplate) nextTemplateName() string {
name := fmt.Sprintf("tpl%d", t.i)
t.i++
return name
@@ -67,7 +67,7 @@ func (t *Template) nextTemplateName() string {
// User is the function exposed as "user" within the templates and
// looks up user variables.
func (t *Template) templateUser(n string) (string, error) {
func (t *ConfigTemplate) templateUser(n string) (string, error) {
result, ok := t.UserVars[n]
if !ok {
return "", fmt.Errorf("uknown user var: %s", n)
@@ -1,4 +1,4 @@
package common
package packer
import (
"math"
@@ -7,8 +7,8 @@ import (
"time"
)
func TestTemplateProcess_timestamp(t *testing.T) {
tpl, err := NewTemplate()
func TestConfigTemplateProcess_timestamp(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
@@ -29,8 +29,8 @@ func TestTemplateProcess_timestamp(t *testing.T) {
}
}
func TestTemplateProcess_user(t *testing.T) {
tpl, err := NewTemplate()
func TestConfigTemplateProcess_user(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
@@ -47,8 +47,8 @@ func TestTemplateProcess_user(t *testing.T) {
}
}
func TestTemplateValidate(t *testing.T) {
tpl, err := NewTemplate()
func TestConfigTemplateValidate(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
+65 -21
View File
@@ -8,6 +8,7 @@ import (
"os"
"sort"
"strings"
"sync"
)
// The function type used to lookup Builder implementations.
@@ -68,6 +69,12 @@ type EnvironmentConfig struct {
Ui Ui
}
type helpCommandEntry struct {
i int
key string
synopsis string
}
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
// be used to create a new enviroment with NewEnvironment with sane defaults.
func DefaultEnvironmentConfig() *EnvironmentConfig {
@@ -245,7 +252,7 @@ func (e *coreEnvironment) Cli(args []string) (result int, err error) {
// If we still don't have a command, show the help.
if command == nil {
log.Printf("Environment.CLI: command not found: %s\n", args[0])
e.ui.Error(fmt.Sprintf("Unknown command: %s\n", args[0]))
e.printHelp()
return 1, nil
}
@@ -266,42 +273,79 @@ func (e *coreEnvironment) Cli(args []string) (result int, err error) {
func (e *coreEnvironment) printHelp() {
// Created a sorted slice of the map keys and record the longest
// command name so we can better format the output later.
i := 0
maxKeyLen := 0
for _, command := range e.commands {
if len(command) > maxKeyLen {
maxKeyLen = len(command)
}
i++
}
// Sort the keys
sort.Strings(e.commands)
// Create the communication/sync mechanisms to get the synopsis' of
// the various commands. We do this in parallel since the overhead
// of the subprocess underneath is very expensive and this speeds things
// up an incredible amount.
var wg sync.WaitGroup
ch := make(chan *helpCommandEntry)
for i, key := range e.commands {
wg.Add(1)
// Get the synopsis in a goroutine since it may take awhile
// to subprocess out.
go func(i int, key string) {
defer wg.Done()
var synopsis string
command, err := e.components.Command(key)
if err != nil {
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
} else if command == nil {
return
} else {
synopsis = command.Synopsis()
}
// Pad the key with spaces so that they're all the same width
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
// Output the command and the synopsis
ch <- &helpCommandEntry{
i: i,
key: key,
synopsis: synopsis,
}
}(i, key)
}
e.ui.Say("usage: packer [--version] [--help] <command> [<args>]\n")
e.ui.Say("Available commands are:")
for _, key := range e.commands {
var synopsis string
command, err := e.components.Command(key)
if err != nil {
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
} else if command == nil {
continue
} else {
synopsis = command.Synopsis()
// Make a goroutine that just waits for all the synopsis gathering
// to complete, and then output it.
synopsisDone := make(chan struct{})
go func() {
defer close(synopsisDone)
entries := make([]string, len(e.commands))
for entry := range ch {
e.ui.Machine("command", entry.key, entry.synopsis)
message := fmt.Sprintf(" %s %s", entry.key, entry.synopsis)
entries[entry.i] = message
}
// Machine-readable output of the available command
e.ui.Machine("command", key, synopsis)
for _, message := range entries {
if message != "" {
e.ui.Say(message)
}
}
}()
// Pad the key with spaces so that they're all the same width
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
// Output the command and the synopsis
e.ui.Say(fmt.Sprintf(" %v %v", key, synopsis))
}
// Wait to complete getting the synopsis' then close the channel
wg.Wait()
close(ch)
<-synopsisDone
e.ui.Say("\nGlobally recognized options:")
e.ui.Say(" -machine-readable Machine-readable output format.")
+11
View File
@@ -17,6 +17,17 @@ func init() {
log.SetOutput(ioutil.Discard)
}
func testComponentFinder() *ComponentFinder {
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil }
return &ComponentFinder{
Builder: builderFactory,
PostProcessor: ppFactory,
Provisioner: provFactory,
}
}
func testEnvironment() Environment {
config := DefaultEnvironmentConfig()
config.Ui = &BasicUi{
-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)
}
}
+56 -28
View File
@@ -1,10 +1,13 @@
package packer
import (
"bytes"
"fmt"
"github.com/mitchellh/mapstructure"
jsonutil "github.com/mitchellh/packer/common/json"
"io"
"io/ioutil"
"os"
"sort"
)
@@ -24,40 +27,40 @@ type rawTemplate struct {
// completed form it can be without additional processing by the caller.
type Template struct {
Variables map[string]string
Builders map[string]rawBuilderConfig
Builders map[string]RawBuilderConfig
Hooks map[string][]string
PostProcessors [][]rawPostProcessorConfig
Provisioners []rawProvisionerConfig
PostProcessors [][]RawPostProcessorConfig
Provisioners []RawProvisionerConfig
}
// The rawBuilderConfig struct represents a raw, unprocessed builder
// The RawBuilderConfig struct represents a raw, unprocessed builder
// configuration. It contains the name of the builder as well as the
// raw configuration. If requested, this is used to compile into a full
// builder configuration at some point.
type rawBuilderConfig struct {
type RawBuilderConfig struct {
Name string
Type string
rawConfig interface{}
RawConfig interface{}
}
// rawPostProcessorConfig represents a raw, unprocessed post-processor
// RawPostProcessorConfig represents a raw, unprocessed post-processor
// configuration. It contains the type of the post processor as well as the
// raw configuration that is handed to the post-processor for it to process.
type rawPostProcessorConfig 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.
// RawProvisionerConfig represents a raw, unprocessed provisioner configuration.
// It contains the type of the provisioner as well as the raw configuration
// that is handed to the provisioner for it to process.
type rawProvisionerConfig struct {
type RawProvisionerConfig struct {
Type string
Override map[string]interface{}
rawConfig interface{}
RawConfig interface{}
}
// ParseTemplate takes a byte slice and parses a Template from it, returning
@@ -103,10 +106,10 @@ func ParseTemplate(data []byte) (t *Template, err error) {
t = &Template{}
t.Variables = make(map[string]string)
t.Builders = make(map[string]rawBuilderConfig)
t.Builders = make(map[string]RawBuilderConfig)
t.Hooks = rawTpl.Hooks
t.PostProcessors = make([][]rawPostProcessorConfig, len(rawTpl.PostProcessors))
t.Provisioners = make([]rawProvisionerConfig, len(rawTpl.Provisioners))
t.PostProcessors = make([][]RawPostProcessorConfig, len(rawTpl.PostProcessors))
t.Provisioners = make([]RawProvisionerConfig, len(rawTpl.Provisioners))
// Gather all the variables
for k, v := range rawTpl.Variables {
@@ -115,7 +118,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// Gather all the builders
for i, v := range rawTpl.Builders {
var raw rawBuilderConfig
var raw RawBuilderConfig
if err := mapstructure.Decode(v, &raw); err != nil {
if merr, ok := err.(*mapstructure.Error); ok {
for _, err := range merr.Errors {
@@ -150,7 +153,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// itself doesn't know about, and it will cause a validation error.
delete(v, "name")
raw.rawConfig = v
raw.RawConfig = v
t.Builders[raw.Name] = raw
}
@@ -159,13 +162,13 @@ 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
}
t.PostProcessors[i] = make([]rawPostProcessorConfig, len(rawPP))
t.PostProcessors[i] = make([]RawPostProcessorConfig, len(rawPP))
configs := t.PostProcessors[i]
for j, pp := range rawPP {
config := &configs[j]
@@ -186,7 +189,10 @@ func ParseTemplate(data []byte) (t *Template, err error) {
continue
}
config.rawConfig = pp
// Remove the input keep_input_artifact option
delete(pp, "keep_input_artifact")
config.RawConfig = pp
}
}
@@ -215,7 +221,15 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// actively reject them as invalid configuration.
delete(v, "override")
raw.rawConfig = v
// 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
}
if len(t.Builders) == 0 {
@@ -235,15 +249,29 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// ParseTemplateFile takes the given template file and parses it into
// a single template.
func ParseTemplateFile(path string) (*Template, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
var data []byte
if path == "-" {
// Read from stdin...
buf := new(bytes.Buffer)
_, err := io.Copy(buf, os.Stdin)
if err != nil {
return nil, err
}
data = buf.Bytes()
} else {
var err error
data, err = ioutil.ReadFile(path)
if err != nil {
return nil, err
}
}
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{}{
@@ -365,7 +393,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
current[i] = coreBuildPostProcessor{
processor: pp,
processorType: rawPP.Type,
config: rawPP.rawConfig,
config: rawPP.RawConfig,
keepInputArtifact: rawPP.KeepInputArtifact,
}
}
@@ -388,7 +416,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
}
configs := make([]interface{}, 1, 2)
configs[0] = rawProvisioner.rawConfig
configs[0] = rawProvisioner.RawConfig
if rawProvisioner.Override != nil {
if override, ok := rawProvisioner.Override[name]; ok {
@@ -403,7 +431,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
b = &coreBuild{
name: name,
builder: builder,
builderConfig: builderConfig.rawConfig,
builderConfig: builderConfig.RawConfig,
builderType: builderConfig.Type,
hooks: hooks,
postProcessors: postProcessors,

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