Compare commits

..

218 Commits

Author SHA1 Message Date
Mitchell Hashimoto 9907933bf8 v0.3.2 2013-08-18 09:47:14 -06: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
Mitchell Hashimoto 0eb9e06637 v0.3.0 2013-08-12 13:57:04 -07:00
Mitchell Hashimoto e00f114520 builder/amazon/common: correct logic in deleting secutiry group 2013-08-12 13:43:52 -07:00
Mitchell Hashimoto c09c1108ca builder/amazon/common: retry deleting security group [GH-278] 2013-08-12 13:34:55 -07:00
Mitchell Hashimoto 34556713dc website: document user data stuff 2013-08-12 12:12:44 -07:00
Mitchell Hashimoto 4d7b0c2dbc builder/amazon/all: support user data file 2013-08-12 11:53:50 -07:00
Mitchell Hashimoto c1f8e5c3b3 builder/amazon/common: support user_data_file 2013-08-12 11:52:43 -07:00
Mitchell Hashimoto 97b4c7d22b Update CHANGELOG 2013-08-12 11:40:59 -07:00
Mitchell Hashimoto 4e300259c7 Merge pull request #253 from phobos182/master
builder/amazon/common: added passing UserData to goamz.
2013-08-12 11:36:15 -07:00
Mitchell Hashimoto db903af566 website: document machine-readable types 2013-08-12 11:33:57 -07:00
Mitchell Hashimoto 3c9e504475 command/build: use the real artifact string in machine readable 2013-08-12 10:31:36 -07:00
Mitchell Hashimoto d88ff24343 packer/rpc: ui.Machine works over RPC properly 2013-08-12 10:25:56 -07:00
Mitchell Hashimoto f7bd403151 command/build: machine-readable artifacts 2013-08-12 10:09:45 -07:00
Mitchell Hashimoto 1bf2bade7d packer: machine readable output for available commands 2013-08-12 09:22:01 -07:00
Mitchell Hashimoto d4d58e3779 Fix packer test 2013-08-12 09:19:24 -07:00
Mitchell Hashimoto 4f58b20479 provisioner/salt-masterless: nice error even if bad exit status
/cc @rgarcia
2013-08-12 09:19:11 -07:00
Mitchell Hashimoto 758a53b146 scripts: full SHA1 in build, not short 2013-08-12 09:17:03 -07:00
Mitchell Hashimoto 336495d34c Update CHANGELOG 2013-08-12 00:02:25 -07:00
Mitchell Hashimoto 78d03c58e5 Merge branch 'machine-readable'
This adds the -machine-readable flag to Packer which turns all output
into machine-readable format. This is documented within the website
source.
2013-08-12 00:01:18 -07:00
Mitchell Hashimoto 43b23c5221 packer: version machine-readable output 2013-08-11 23:56:47 -07:00
Mitchell Hashimoto 226e6f5859 website: document machine-readable 2013-08-11 23:56:39 -07:00
Mitchell Hashimoto bebed45a80 packer: -machine-readable not double hyphen 2013-08-11 23:26:24 -07:00
Mitchell Hashimoto f4a9d9d3b7 packer: document machine-readable 2013-08-11 23:24:57 -07:00
Mitchell Hashimoto eed6ece45b packer: replace \r with literal on Ui for MR 2013-08-11 23:19:13 -07:00
Mitchell Hashimoto 04c8592077 packer: replace new lines with literal \n 2013-08-11 23:18:14 -07:00
Mitchell Hashimoto f3bb028ec3 packer: output a newline after each MR line 2013-08-11 23:14:42 -07:00
Mitchell Hashimoto f46bf47579 parse --machine-readable and set it 2013-08-11 23:12:20 -07:00
Mitchell Hashimoto ca54df1a76 packer: TargettedUi properly targets machine-readable data 2013-08-11 19:08:08 -07:00
Mitchell Hashimoto b54a5c0074 packer: Add MachineReadableUi 2013-08-11 19:05:07 -07:00
Mitchell Hashimoto cd62aa5003 packer: BasicUi doesn't do machine-readable, just log 2013-08-11 18:38:24 -07:00
Mitchell Hashimoto 10b0f920bc packer: renamed PrefixedUi to TargettedUi 2013-08-11 18:31:28 -07:00
Mitchell Hashimoto f3f4ab91c7 packer: Rename ReaderWriterUi to BasicUi 2013-08-11 18:20:27 -07:00
Mitchell Hashimoto c8bc2bc1ba packer: Add Machine func to Ui 2013-08-11 18:16:00 -07:00
Mitchell Hashimoto 41370a215e Update CHANGELOG 2013-08-11 16:18:40 -07:00
Mitchell Hashimoto f2355a577a fmt 2013-08-11 16:17:59 -07:00
Mitchell Hashimoto 6a7450974d Merge pull request #269 from rach/master
provisioner/salt-masterless: `local_state_tree` should not be mandatory
2013-08-11 16:17:37 -07:00
Mitchell Hashimoto 810bea7fcb communicator/ssh: docs 2013-08-11 16:15:10 -07:00
Mitchell Hashimoto d5ab581260 Merge pull request #273 from rgarcia/password-key
communicator/ssh: ability to use a password-protected key
2013-08-11 16:13:53 -07:00
Rafael Garcia ae35cd51e2 communicator/ssh: ability to use a password-protected key 2013-08-11 09:46:28 -07:00
Rach Belaid a4c555a5b7 #269 Make local_state_tree option not mandatory
Update and rename salt.html.markdown to salt-masterless.html.markdown
salt.html.markdown had a better history and formatting
And remove local_state_tree from required option from code and docs
2013-08-10 16:18:57 +01:00
Rach Belaid a542ff967b Delete duplicate doc for Salt masterless provisionner
there is a other doc file for this called salt.html.markdown which contain a better history
2013-08-10 15:24:04 +01:00
Mitchell Hashimoto 59eff6a222 website: add GA tracker 2013-08-09 18:12:11 -07:00
Mitchell Hashimoto b25f0ceabf provisioner/salt-masterless: error if any commands exit with non-zero
[GH-266]

/cc @rgarcia
2013-08-09 17:35:57 -07:00
Mitchell Hashimoto a0a6817f21 Update CHANGELOG 2013-08-09 17:33:22 -07:00
Mitchell Hashimoto 5decc186a6 builder/vmware: nitpick some styles
/cc @rasa - I changed up quite a bit here. I tried to reduce function
count if possible, renamed some functions, etc. Overall the
functionality was all spot on, but I felt the functions were too
specialized. Thanks!
2013-08-09 17:31:43 -07:00
Ross Smith II cfc7715495 fixed typo in vmware path on Windows XP, fixes #237/#238 2013-08-09 17:10:05 -07:00
Ross Smith II 60fc565dbb Add support for VMWare Workstation on Windows XP, fixes #237 2013-08-09 17:09:55 -07:00
Mitchell Hashimoto da4a7c3b70 provisioner/salt-masterless: don't delete states [GH-265] 2013-08-09 17:01:39 -07:00
Mitchell Hashimoto 2349706a68 packer/rpc: register map[string]string since we transfer that 2013-08-09 16:53:18 -07:00
Mitchell Hashimoto 777f595c8b common/json: add Unmarshal with method with syntax errors 2013-08-09 16:50:24 -07:00
Mitchell Hashimoto e7b1ba2f5e command/bulid: cosmetic, align help text 2013-08-09 16:46:38 -07:00
Mitchell Hashimoto 95926424f2 command/build,validate: accept -var-file 2013-08-09 16:46:06 -07:00
Mitchell Hashimoto 1e4e343333 common/command: add -var-file support for user vars 2013-08-09 16:45:29 -07:00
Mitchell Hashimoto de13474865 website: document user variables 2013-08-09 16:25:13 -07:00
Mitchell Hashimoto 3ca085c2c1 Update CHANGELOG 2013-08-09 15:57:09 -07:00
Mitchell Hashimoto 713791ba9a command/build,validate: pass user vars to Prepare 2013-08-09 15:57:09 -07:00
Mitchell Hashimoto bb1b3d8fe0 common/command: parse the "-var" flag 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 1a1b4ba715 common/command: rename BuildFilters to BuildOptions 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 41b3f9ebd9 command/build,validate: use new ParseTemplateFile methods 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto d0200b7be8 command/validate: accept -only/-except and use new common stuff 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto a0033fba35 common/command: introduce this package for common cmmand stuff 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 180d5c8275 packer: add ParseTemplateFile since that is useful 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto ddfd9be8cc command/build,validate: compiles again with latest user var changes 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto bf457f437c provisioner/salt-masterless: convert to new template stuff + user vars 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 100458955f builder/*, provisioner/*: set user vars 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto ab5a1981d9 common: UserData => UserVars 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto eaf5e66f3a common: PackerConfig understands user vars 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 3e8b3b9327 packer: properly override/send user variables to prepare 2013-08-09 15:57:08 -07:00
Mitchell Hashimoto 7408558a55 packer, packer/rpc: Update Build interface to allow variable overrides 2013-08-09 15:57:07 -07:00
Mitchell Hashimoto d87f878a44 packer: Template understands variables, puts it into a Build 2013-08-09 15:57:07 -07:00
Mitchell Hashimoto 7836d60c61 Merge pull request #259 from phobos182/debug
builder/amazon/common: Log error messages from goamz on state change errors
2013-08-09 11:58:24 -07:00
Jeremy Carroll c94287bbde Log error messages from goamz. 2013-08-09 11:55:34 -07:00
Mitchell Hashimoto f909bc29ed website: update docs for image attributes
/cc @jmassara - easy stuff :)
2013-08-08 22:57:22 -07:00
Mitchell Hashimoto d736c4b180 Update CHANGELOG 2013-08-08 22:53:33 -07:00
Mitchell Hashimoto afe8dda7a9 builder/amazon/chroot: can modify AMI attributes 2013-08-08 22:52:39 -07:00
Mitchell Hashimoto 558f919e4f builder/amazon/common: rename StepModifyAttributes to
StepModifyAMIAttributes
2013-08-08 22:51:48 -07:00
Mitchell Hashimoto d7c9658c1c builder/amazon/common: AMIConfig common config
/cc @jmassara - I pulled out the AMI stuff into a common config
struct and also added in the new template stuff (that didn't exist
when you made the pull, so not your fault! :))
2013-08-08 22:50:37 -07:00
Mitchell Hashimoto 2a0b3cfdc2 builder/amazon/common: on modify AMI attributes, skip if no work
/cc @jmassara
2013-08-08 22:46:50 -07:00
Mitchell Hashimoto 1f53e047a3 builder/amazon/common: use new API for goamz to set AMI attributes
/cc @jmassara
2013-08-08 22:43:29 -07:00
James Massara b0394c6d30 builder/amazon/all: Added support for setting attributes on the AMI 2013-08-08 22:35:21 -07:00
Mitchell Hashimoto d4d7730473 Merge branch 'ignored_files' of https://github.com/priteau/packer into priteau-ignored_files
Conflicts:
	website/source/docs/builders/amazon-instance.html.markdown
2013-08-08 17:30:35 -07:00
Mitchell Hashimoto 9dc98a6a2a Update CHANGELOG 2013-08-08 17:26:46 -07:00
Mitchell Hashimoto ffc73a2ffa command/fix: replace CreateTime with new timestamp 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto db5b236ad6 command/fix: log fixer to run 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto bdd49619ae command/fix: use strings instead of Fixer for list 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto 23803621e9 provisioner/shell: tests passing and compiling 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto 9520c6e6ed builder/amazon: more efficient use of templates 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto c6e36108b3 README: move CreateTime to timestmap 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto a9df9e91c6 website: get rid of CreateTime for timestamp 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto f9ffb1fc54 website: document the new createtime stuff 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto 16ccfe3824 website: new config template docs 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto 1b01e82d8d provisioner/shell: convert to latest template stuff 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto f66c145ee8 provisioner/file: use the template processing stuff 2013-08-08 17:25:26 -07:00
Mitchell Hashimoto ad9575a041 builder/vmware: error checking on upload path template 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto d71621a599 builder/vmware: use new template stuff 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 8fad60b1b4 builder/virtualbox: use new template processing stuff 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto fde7910e85 builder/amazon: remove unnecessary validation 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 66818ef89f builder/digitalocean: use new template processing 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 1268373105 builder/amazon/instance: switch to new template stuff 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto a9b951e1fe builder/amazon/ebs: get rid of unnecessary template 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 28e72c7f7b builder/amazon/chroot: switch to new template stuff 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto a669b4dbd0 builder/amazon/ebs: process templates for config 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 8e7aa41bdf builder/amazon/common: RunConfig to template processing 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto 93ac76e9c7 builder/amazon/common: access config uses template processing 2013-08-08 17:25:25 -07:00
Mitchell Hashimoto ff27abc06a common: clone template during validation to avoid unnecessary children 2013-08-08 17:25:24 -07:00
Mitchell Hashimoto 04a378c39a common: validation and generating unique template names 2013-08-08 17:25:24 -07:00
Mitchell Hashimoto ecd598ab64 common: support user data 2013-08-08 17:25:24 -07:00
Mitchell Hashimoto 915f9b73be common: functions for template processing 2013-08-08 17:25:24 -07:00
Pierre Riteau cb756912ad Warn that ec2-bundle-vol may ignore some important files 2013-08-08 19:00:19 +01:00
Mark Peek 253d32c274 website: add myself to the community page 2013-08-08 10:03:09 -07:00
Mitchell Hashimoto 0b471adc7d Merge pull request #255 from johnbellone/patch-1
Update packer.go to cleanup plugin clients on bad CLI exit.
2013-08-08 09:31:36 -07:00
John Bellone 13ca445e13 Update packer.go to cleanup plugin clients on exit.
Since defer does not fire when exiting the application with os.Exit we need to be sure that we cleanup all of the clients on failures from the CLI and creating a new environment object.
2013-08-08 12:16:28 -04:00
Mark Peek 87e61b643c builder/amazon/all: address feedback for ami tag refactoring change
Use explicit name for Tag field and note tags added in the ui.
2013-08-07 19:37:41 -07:00
Mitchell Hashimoto 6a6aa146ac up version for dev 2013-08-07 15:47:43 -07:00
Mitchell Hashimoto 1e61157c5c v0.2.3 2013-08-07 15:32:38 -07:00
Mitchell Hashimoto 7c4bd4f2d6 website: nitpick alternative installation methods 2013-08-07 15:32:09 -07:00
Jeremy Carroll a81fc2d837 Convert string to byte array 2013-08-07 15:20:49 -07:00
Mitchell Hashimoto 9c6cc829fa Merge pull request #244 from henrikhodne/hh-homebrew-docs-website
website: add documentation on installing through Homebrew
2013-08-07 15:01:57 -07:00
Jeremy Carroll 9eda3fa327 Testing adding user-data support to Packer 2013-08-07 14:33:20 -07:00
Mitchell Hashimoto 53b0223e20 Merge pull request #249 from rgarcia/config-decoding
provisioner/shell,file: use common for config decoding
2013-08-07 06:35:59 -07:00
Mark Peek 15e80316fe Update CHANGELOG for Amazon AMI tag support [GH-233] 2013-08-06 15:31:56 -07:00
Rafael Garcia 485d5b86d0 provisioner/file: state of the art config decoding 2013-08-06 15:30:49 -07:00
Rafael Garcia b0d5bff98f provisioner/shell: state of the art config decoding 2013-08-06 15:30:36 -07:00
Mark Peek 8c51316897 website: cleanup Amazon AMI tag documentation 2013-08-06 15:16:39 -07:00
Mark Peek 3bcfeafa15 builder/amazon/all: refactor ami tags [GH-233]
Refactor the EBS ami tag into a common step and add support
for instance-store ami tags.
/cc @jmassara
2013-08-06 15:05:03 -07:00
Mark Peek 3d461195e3 Merge pull request #233 from jmassara/tags
builder/amazon/ebs: Added tagging support for amazon/ebs AMIs
2013-08-06 15:01:32 -07:00
James Massara e6a1fc448a Updated tags configuration example 2013-08-06 07:41:00 -07:00
James Massara d1aefb38bb Simplied Tags configuration 2013-08-06 07:33:00 -07:00
Mitchell Hashimoto e0a66fbe30 Merge pull request #245 from extraordinaire/patch-1
builder/amazon/common: fix check for vpcid
2013-08-05 08:39:23 -07:00
Andrei Serdeliuc 80db9efef5 fix check for vpcid
check for VpcId was incorrectly checking if VPC is empty, instead of not empty.
2013-08-05 16:19:05 +01:00
James Massara 8853e7a17e Moved Tags configuration out of common and into ebs. 2013-08-03 20:09:14 -07:00
James Massara ad38604390 removed Tags definition from RunConfig 2013-08-03 17:53:53 -07:00
Henrik Hodne 64f4ee380d website: document Homebrew installation process [GH-227] 2013-08-03 16:44:30 -07:00
Mitchell Hashimoto 3bf88e2dd7 builder/amazon/common: sleep between API calls for SSHAddr 2013-08-03 16:38:21 -07:00
Mitchell Hashimoto c8508ade17 builder/amazon/common: be more gentle on AWS API while getting SSHAddr 2013-08-03 16:37:34 -07:00
Mitchell Hashimoto 4e8db89403 builder/amazon/all: use new SSHAddress method [GH-243] 2013-08-03 16:24:49 -07:00
Mitchell Hashimoto 1b8551d843 builder/amazon/common: refresh instance while connecting to SSH [GH-243] 2013-08-03 16:23:32 -07:00
Mitchell Hashimoto 46ec8f758b website: document {{.Vars}} in execute_command example [GH-241] 2013-08-03 13:40:31 -07:00
Mitchell Hashimoto 46e49b745e common: replace windows file URL backslash with forward slash
/cc @jasonberanek - Just adding this as well because I see this being
common as well.
2013-08-03 13:38:27 -07:00
Mitchell Hashimoto 4b29fab843 Update CHANGELOG 2013-08-03 13:35:19 -07:00
Mitchell Hashimoto 82e21622ef common: style 2013-08-03 13:34:48 -07:00
Mitchell Hashimoto e4dda36a1d Merge pull request #240 from jasonberanek/windows-paths-fix
common: Fix URL and Relative Windows file path support #235/#239
2013-08-03 13:32:51 -07:00
Jason A. Beranek 1216fc1cbb common/config,download: gofmt fixes 2013-08-02 16:06:06 -05:00
Jason A. Beranek 2b4735e825 common/config,download: fix URL and relative Windows local file path issues [GH-235] [GH-239] 2013-08-02 15:59:19 -05:00
James Massara 422e05d8b9 Updated with tags info 2013-08-01 21:28:25 -07:00
James Massara 4c273e33c5 removed amiId temp variable 2013-08-01 18:35:43 -07:00
James Massara b24fcbc800 Adds support for adding tags to the AMI 2013-08-01 16:38:22 -07:00
James Massara 852c10264b Adds support for adding tags to the AMI 2013-08-01 16:31:07 -07:00
Mitchell Hashimoto ef59ee41a8 rename builder/common to common since it is generally useful 2013-08-01 12:11:54 -07:00
Mitchell Hashimoto da7febbfb9 Update version for dev 2013-08-01 10:40:18 -07:00
148 changed files with 4681 additions and 1167 deletions
+22
View File
@@ -0,0 +1,22 @@
language: go
go:
- 1.1
- tip
install: make deps
script: make test
notifications:
flowdock:
secure: fZrcf9rlh2IrQrlch1sHkn3YI7SKvjGnAl/zyV5D6NROe1Bbr6d3QRMuCXWWdhJHzjKmXk5rIzbqJhUc0PNF7YjxGNKSzqWMQ56KcvN1k8DzlqxpqkcA3Jbs6fXCWo2fssRtZ7hj/wOP1f5n6cc7kzHDt9dgaYJ6nO2fqNPJiTc=
irc:
channels:
- "chat.freenode.net#packer-tool"
on_success: change
on_failure: always
matrix:
allow_failures:
- go: tip
+116
View File
@@ -1,3 +1,119 @@
## 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:
* All `{{.CreateTime}}` variables within templates (such as for AMI names)
are now replaced with `{{timestamp}}`. Run `packer fix` to fix your
templates.
FEATURES:
* **User Variables** allow you to specify variables within your templates
that can be replaced using the command-line, files, or environmental
variables. This dramatically improves the portability of packer templates.
See the documentation for more information.
* **Machine-readable output** can be enabled by passing the
`-machine-readable` flag to _any_ Packer command.
* All strings in a template are now processed for variables/functions,
so things like `{{timestamp}}` can be used everywhere. More features will
be added in the future.
* The `amazon` builders (all of them) can now have attributes of their
resulting AMIs modified, such as access permissions and product codes.
IMPROVEMENTS:
* builder/amazon/all: User data can be passed to start the instances. [GH-253]
* provisioner/salt-masterless: `local_state_tree` is no longer required,
allowing you to use shell provisioner (or others) to bring this down.
[GH-269]
BUG FIXES:
* builder/amazon/ebs,instance: Retry deleing security group a few times.
[GH-278]
* builder/vmware: Workstation works on Windows XP now. [GH-238]
* builder/vmware: Look for files on Windows in multiple locations
using multiple environmental variables. [GH-263]
* provisioner/salt-masterless: states aren't deleted after the run
anymore. [GH-265]
* provisioner/salt-masterless: error if any commands exit with a non-zero
exit status. [GH-266]
## 0.2.3 (August 7, 2013)
IMPROVEMENTS:
* builder/amazon/all: Added Amazon AMI tag support [GH-233]
BUG FIXES:
* core: Absolute/relative filepaths on Windows now work for iso_url
and other settings. [GH-240]
* builder/amazon/all: instance info is refreshed while waiting for SSH,
allowing Packer to see updated IP/DNS info. [GH-243]
## 0.2.2 (August 1, 2013)
FEATURES:
+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
+1 -1
View File
@@ -43,7 +43,7 @@ own.
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-example {{.CreateTime}}"
"ami_name": "packer-example {{timestamp}}"
}]
}
```
+53 -16
View File
@@ -10,11 +10,10 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"runtime"
"text/template"
)
// The unique ID for this builder
@@ -25,8 +24,8 @@ const BuilderId = "mitchellh.amazon.chroot"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
AMIName string `mapstructure:"ami_name"`
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
@@ -34,6 +33,8 @@ type Config struct {
MountPath string `mapstructure:"mount_path"`
SourceAmi string `mapstructure:"source_ami"`
UnmountCommand string `mapstructure:"unmount_command"`
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -47,6 +48,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Defaults
if b.config.ChrootMounts == nil {
b.config.ChrootMounts = make([][]string, 0)
@@ -84,31 +91,56 @@ func (b *Builder) Prepare(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
for _, mounts := range b.config.ChrootMounts {
for i, mounts := range b.config.ChrootMounts {
if len(mounts) != 3 {
errs = packer.MultiErrorAppend(
errs, errors.New("Each chroot_mounts entry should be three elements."))
break
}
for j, entry := range mounts {
b.config.ChrootMounts[i][j], err = b.config.tpl.Process(entry, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing chroot_mounts[%d][%d]: %s",
i, j, err))
}
}
}
for i, file := range b.config.CopyFiles {
var err error
b.config.CopyFiles[i], err = b.config.tpl.Process(file, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing copy_files[%d]: %s",
i, err))
}
}
if b.config.SourceAmi == "" {
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
}
templates := map[string]*string{
"device_path": &b.config.DevicePath,
"mount_command": &b.config.MountCommand,
"source_ami": &b.config.SourceAmi,
"unmount_command": &b.config.UnmountCommand,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -157,6 +189,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepEarlyCleanup{},
&StepSnapshot{},
&StepRegisterAMI{},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
}
// Run!
+8 -6
View File
@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"text/template"
)
type mountPathData struct {
@@ -30,14 +29,17 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction
ui := state["ui"].(packer.Ui)
device := state["device"].(string)
mountPathRaw := new(bytes.Buffer)
t := template.Must(template.New("mountPath").Parse(config.MountPath))
t.Execute(mountPathRaw, &mountPathData{
mountPath, err := config.tpl.Process(config.MountPath, &mountPathData{
Device: filepath.Base(device),
})
var err error
mountPath := mountPathRaw.String()
if err != nil {
err := fmt.Errorf("Error preparing mount directory: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
mountPath, err = filepath.Abs(mountPath)
if err != nil {
err := fmt.Errorf("Error preparing mount directory: %s", err)
+1 -19
View File
@@ -1,21 +1,13 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
// StepRegisterAMI creates the AMI.
type StepRegisterAMI struct{}
@@ -26,16 +18,6 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
snapshotId := state["snapshot_id"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))
for i, device := range image.BlockDevices {
@@ -48,7 +30,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
}
registerOpts := &ec2.RegisterImage{
Name: amiName,
Name: config.AMIName,
Architecture: image.Architecture,
KernelId: image.KernelId,
RamdiskId: image.RamdiskId,
+31 -2
View File
@@ -3,6 +3,7 @@ package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/packer/packer"
"strings"
"unicode"
)
@@ -36,12 +37,40 @@ func (c *AccessConfig) Region() (aws.Region, error) {
return aws.Regions[region], nil
}
func (c *AccessConfig) Prepare() []error {
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{
"access_key": &c.AccessKey,
"secret_key": &c.SecretKey,
"region": &c.RawRegion,
}
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.RawRegion != "" {
if _, ok := aws.Regions[c.RawRegion]; !ok {
return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)}
errs = append(errs, fmt.Errorf("Unknown region: %s", c.RawRegion))
}
}
if len(errs) > 0 {
return errs
}
return nil
}
+3 -3
View File
@@ -11,17 +11,17 @@ func testAccessConfig() *AccessConfig {
func TestAccessConfigPrepare_Region(t *testing.T) {
c := testAccessConfig()
c.RawRegion = ""
if err := c.Prepare(); err != nil {
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.RawRegion = "us-east-12"
if err := c.Prepare(); err == nil {
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
c.RawRegion = "us-east-1"
if err := c.Prepare(); err != nil {
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
+67
View File
@@ -0,0 +1,67 @@
package common
import (
"fmt"
"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"`
}
func (c *AMIConfig) 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{
"ami_name": &c.AMIName,
"ami_description": &c.AMIDescription,
}
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))
}
}
sliceTemplates := map[string][]string{
"ami_users": c.AMIUsers,
"ami_groups": c.AMIGroups,
"ami_product_codes": c.AMIProductCodes,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = t.Process(elem, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
if c.AMIName == "" {
errs = append(errs, fmt.Errorf("ami_name must be specified"))
}
if len(errs) > 0 {
return errs
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
package common
import (
"testing"
)
func testAMIConfig() *AMIConfig {
return &AMIConfig{
AMIName: "foo",
}
}
func TestAMIConfigPrepare_Region(t *testing.T) {
c := testAMIConfig()
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.AMIName = ""
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
}
+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")
}
+20 -2
View File
@@ -9,18 +9,34 @@ import (
"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 EC2 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, err error)
// StateChangeConf is the configuration struct used for `WaitForState`.
type StateChangeConf struct {
Conn *ec2.EC2
Pending []string
Refresh func() (interface{}, string, error)
Refresh StateRefreshFunc
StepState map[string]interface{}
Target string
}
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{}, string, error) {
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
log.Printf("Error on InstanceStateRefresh: %s", err)
return nil, "", err
}
@@ -29,6 +45,8 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{
}
}
// 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)
+41 -1
View File
@@ -3,6 +3,8 @@ package common
import (
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
"os"
"time"
)
@@ -12,6 +14,8 @@ type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"`
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
InstanceType string `mapstructure:"instance_type"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
@@ -23,7 +27,15 @@ type RunConfig struct {
sshTimeout time.Duration
}
func (c *RunConfig) Prepare() []error {
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.SSHPort == 0 {
c.SSHPort = 22
@@ -48,6 +60,34 @@ func (c *RunConfig) Prepare() []error {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {
if _, err := os.Stat(c.UserDataFile); err != nil {
errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
}
}
templates := map[string]*string{
"iam_instance_profile": &c.IamInstanceProfile,
"instance_type": &c.InstanceType,
"ssh_timeout": &c.RawSSHTimeout,
"security_group_id": &c.SecurityGroupId,
"ssh_username": &c.SSHUsername,
"source_ami": &c.SourceAmi,
"subnet_id": &c.SubnetId,
"vpc_id": &c.VpcId,
}
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))
+47 -8
View File
@@ -1,6 +1,7 @@
package common
import (
"io/ioutil"
"os"
"testing"
)
@@ -24,7 +25,7 @@ func testConfig() *RunConfig {
func TestRunConfigPrepare(t *testing.T) {
c := testConfig()
err := c.Prepare()
err := c.Prepare(nil)
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
@@ -33,7 +34,7 @@ func TestRunConfigPrepare(t *testing.T) {
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
@@ -41,7 +42,7 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) {
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
@@ -49,7 +50,7 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
if err := c.Prepare(); len(err) != 0 {
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
@@ -58,7 +59,7 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
}
c.SSHPort = 44
if err := c.Prepare(); len(err) != 0 {
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
@@ -70,12 +71,12 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(); len(err) != 0 {
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
@@ -83,7 +84,45 @@ func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
if err := c.Prepare(); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_UserData(t *testing.T) {
c := testConfig()
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
c.UserData = "foo"
c.UserDataFile = tf.Name()
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_UserDataFile(t *testing.T) {
c := testConfig()
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.UserDataFile = "idontexistidontthink"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
c.UserDataFile = tf.Name()
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
+27 -10
View File
@@ -6,23 +6,40 @@ import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
"time"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
func SSHAddress(e *ec2.EC2, port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
var host string
instance := state["instance"].(*ec2.Instance)
if instance.DNSName != "" {
host = instance.DNSName
} else if instance.VpcId == "" {
host = instance.PrivateIpAddress
} else {
return "", errors.New("couldn't determine IP address for instance")
for j := 0; j < 2; j++ {
var host string
i := state["instance"].(*ec2.Instance)
if i.DNSName != "" {
host = i.DNSName
} else if i.VpcId != "" {
host = i.PrivateIpAddress
}
if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil
}
r, err := e.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return "", err
}
if len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
return "", fmt.Errorf("instance not found: %s", i.InstanceId)
}
state["instance"] = &r.Reservations[0].Instances[0]
time.Sleep(1 * time.Second)
}
return fmt.Sprintf("%s:%d", host, port), nil
return "", errors.New("couldn't determine IP address for instance")
}
}
+43
View File
@@ -0,0 +1,43 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepCreateTags struct {
Tags map[string]string
}
func (s *StepCreateTags) 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.Tags) > 0 {
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
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 := ec2conn.CreateTags([]string{ami}, ec2Tags)
if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepCreateTags) Cleanup(state map[string]interface{}) {
// No cleanup...
}
@@ -0,0 +1,79 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepModifyAMIAttributes struct {
Users []string
Groups []string
ProductCodes []string
Description string
}
func (s *StepModifyAMIAttributes) 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]
// Determine if there is any work to do.
valid := false
valid = valid || s.Description != ""
valid = valid || (s.Users != nil && len(s.Users) > 0)
valid = valid || (s.Groups != nil && len(s.Groups) > 0)
valid = valid || (s.ProductCodes != nil && len(s.ProductCodes) > 0)
if !valid {
return multistep.ActionContinue
}
// 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...")
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
}
func (s *StepModifyAMIAttributes) Cleanup(state map[string]interface{}) {
// No cleanup...
}
@@ -5,15 +5,19 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
)
type StepRunSourceInstance struct {
ExpectedRootDevice string
InstanceType string
UserData string
UserDataFile string
SourceAMI string
IamInstanceProfile string
SubnetId string
BlockDevices BlockDevices
instance *ec2.Instance
}
@@ -24,15 +28,28 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
securityGroupId := state["securityGroupId"].(string)
ui := state["ui"].(packer.Ui)
userData := s.UserData
if s.UserDataFile != "" {
contents, err := ioutil.ReadFile(s.UserDataFile)
if err != nil {
state["error"] = fmt.Errorf("Problem reading user data file: %s", err)
return multistep.ActionHalt
}
userData = string(contents)
}
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
UserData: []byte(userData),
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
}
ui.Say("Launching a source AWS instance...")
@@ -75,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
@@ -83,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
+13 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type StepSecurityGroup struct {
@@ -83,9 +84,19 @@ func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary security group...")
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
if err != nil {
var err error
for i := 0; i < 5; i++ {
_, err = ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
if err == nil {
break
}
log.Printf("Error deleting security group: %s", err)
time.Sleep(5 * time.Second)
}
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
}
+45 -17
View File
@@ -6,15 +6,13 @@
package ebs
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
)
// The unique ID for this builder
@@ -23,10 +21,14 @@ const BuilderId = "mitchellh.amazonebs"
type config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
// Tags for the AMI
Tags map[string]string
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -40,23 +42,40 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Accumulate any errors
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
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("Failed parsing ami_name: %s", err))
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
}
@@ -96,18 +115,27 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "ebs",
InstanceType: b.config.InstanceType,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
SourceAMI: b.config.SourceAmi,
IamInstanceProfile: b.config.IamInstanceProfile,
SubnetId: b.config.SubnetId,
BlockDevices: b.config.BlockDevices,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
&awscommon.StepCreateTags{Tags: b.config.Tags},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
}
// Run!
+4 -21
View File
@@ -1,44 +1,27 @@
package ebs
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type stepCreateAMI struct{}
type amiNameData struct {
CreateTime string
}
func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ec2conn := state["ec2"].(*ec2.EC2)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
// Create the image
ui.Say(fmt.Sprintf("Creating the AMI: %s", amiName))
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
createOpts := &ec2.CreateImage{
InstanceId: instance.InstanceId,
Name: amiName,
InstanceId: instance.InstanceId,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
}
createResp, err := ec2conn.CreateImage(createOpts)
+60 -18
View File
@@ -8,12 +8,11 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
"text/template"
)
// The unique ID for this builder
@@ -24,18 +23,22 @@ const BuilderId = "mitchellh.amazon.instance"
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"`
AMIName string `mapstructure:"ami_name"`
BundleDestination string `mapstructure:"bundle_destination"`
BundlePrefix string `mapstructure:"bundle_prefix"`
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
Tags map[string]string
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -49,12 +52,18 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
if b.config.BundleDestination == "" {
b.config.BundleDestination = "/tmp"
}
if b.config.BundlePrefix == "" {
b.config.BundlePrefix = "image-{{.CreateTime}}"
b.config.BundlePrefix = "image-{{timestamp}}"
}
if b.config.BundleUploadCommand == "" {
@@ -86,8 +95,41 @@ func (b *Builder) Prepare(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
validates := map[string]*string{
"bundle_upload_command": &b.config.BundleUploadCommand,
"bundle_vol_command": &b.config.BundleVolCommand,
}
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))
}
}
templates := map[string]*string{
"account_id": &b.config.AccountId,
"ami_name": &b.config.AMIName,
"bundle_destination": &b.config.BundleDestination,
"bundle_prefix": &b.config.BundlePrefix,
"s3_bucket": &b.config.S3Bucket,
"x509_cert_path": &b.config.X509CertPath,
"x509_key_path": &b.config.X509KeyPath,
"x509_upload_path": &b.config.X509UploadPath,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if b.config.AccountId == "" {
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
@@ -95,17 +137,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
}
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if b.config.S3Bucket == "" {
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
}
@@ -163,11 +194,15 @@ 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(b.config.SSHPort),
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
@@ -176,6 +211,13 @@ 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,
},
}
// Run!
+1 -1
View File
@@ -115,7 +115,7 @@ func TestBuilderPrepare_BundlePrefix(t *testing.T) {
t.Fatalf("err: %s", err)
}
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
if b.config.BundlePrefix == "" {
t.Fatalf("bad: %s", b.config.BundlePrefix)
}
}
+11 -22
View File
@@ -1,14 +1,10 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type bundleCmdData struct {
@@ -21,10 +17,6 @@ type bundleCmdData struct {
PrivatePath string
}
type bundlePrefixData struct {
CreateTime string
}
type StepBundleVolume struct{}
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
@@ -55,29 +47,26 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
}
// Bundle the volume
var bundlePrefix bytes.Buffer
prefixTData := bundlePrefixData{
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
t.Execute(&bundlePrefix, prefixTData)
var bundleCmd bytes.Buffer
tData := bundleCmdData{
var err error
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
AccountId: config.AccountId,
Architecture: instance.Architecture,
CertPath: x509RemoteCertPath,
Destination: config.BundleDestination,
KeyPath: x509RemoteKeyPath,
Prefix: bundlePrefix.String(),
Prefix: config.BundlePrefix,
PrivatePath: config.X509UploadPath,
})
if err != nil {
err := fmt.Errorf("Error processing bundle volume command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
t.Execute(&bundleCmd, tData)
ui.Say("Bundling the volume...")
cmd = new(packer.RemoteCmd)
cmd.Command = bundleCmd.String()
cmd.Command = config.BundleVolCommand
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
ui.Error(state["error"].(error).Error())
@@ -93,7 +82,7 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
}
// Store the manifest path
manifestName := bundlePrefix.String() + ".manifest.xml"
manifestName := config.BundlePrefix + ".manifest.xml"
state["manifest_name"] = manifestName
state["manifest_path"] = fmt.Sprintf(
"%s/%s", config.BundleDestination, manifestName)
+2 -19
View File
@@ -1,21 +1,13 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
@@ -24,20 +16,11 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
manifestPath := state["remote_manifest_path"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: amiName,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
@@ -1,11 +1,9 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"text/template"
)
type uploadCmdData struct {
@@ -25,19 +23,23 @@ func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepActio
manifestPath := state["manifest_path"].(string)
ui := state["ui"].(packer.Ui)
var uploadCmd bytes.Buffer
tData := uploadCmdData{
var err error
config.BundleUploadCommand, err = config.tpl.Process(config.BundleUploadCommand, uploadCmdData{
AccessKey: config.AccessKey,
BucketName: config.S3Bucket,
BundleDirectory: config.BundleDestination,
ManifestPath: manifestPath,
SecretKey: config.SecretKey,
})
if err != nil {
err := fmt.Errorf("Error processing bundle upload command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
t.Execute(&uploadCmd, tData)
ui.Say("Uploading the bundle...")
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
ui.Error(state["error"].(error).Error())
-10
View File
@@ -1,10 +0,0 @@
package common
// PackerConfig is a struct that contains the configuration keys that
// are sent by packer, properly tagged already so mapstructure can load
// them. Embed this structure into your configuration class to get it.
type PackerConfig struct {
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
}
+25 -17
View File
@@ -53,10 +53,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 +71,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 +98,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 +106,7 @@ 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
}
@@ -112,7 +114,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 +125,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 +141,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 +149,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 +169,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{}
+34 -29
View File
@@ -4,26 +4,19 @@
package digitalocean
import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strconv"
"text/template"
"time"
)
// The unique id for the builder
const BuilderId = "pearkes.digitalocean"
type snapshotNameData struct {
CreateTime string
}
// Configuration tells the builder the credentials
// to use while communicating with DO and describes the image
// you are creating
@@ -36,11 +29,10 @@ type config struct {
SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"`
SnapshotName string
SnapshotName string `mapstructure:"snapshot_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSnapshotName string `mapstructure:"snapshot_name"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawEventDelay string `mapstructure:"event_delay"`
RawStateTimeout string `mapstructure:"state_timeout"`
@@ -50,6 +42,8 @@ type config struct {
sshTimeout time.Duration
eventDelay time.Duration
stateTimeout time.Duration
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -63,6 +57,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
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)
@@ -92,6 +92,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.ImageID = 284203
}
if b.config.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.SnapshotName = "packer-{{timestamp}}"
}
if b.config.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
@@ -103,11 +108,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.SSHPort = 22
}
if b.config.RawSnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
}
if b.config.RawSSHTimeout == "" {
// Default to 1 minute timeouts
b.config.RawSSHTimeout = "1m"
@@ -125,6 +125,25 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.RawStateTimeout = "6m"
}
templates := map[string]*string{
"client_id": &b.config.ClientID,
"api_key": &b.config.APIKey,
"snapshot_name": &b.config.SnapshotName,
"ssh_username": &b.config.SSHUsername,
"ssh_timeout": &b.config.RawSSHTimeout,
"event_delay": &b.config.RawEventDelay,
"state_timeout": &b.config.RawStateTimeout,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = packer.MultiErrorAppend(
@@ -157,20 +176,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
b.config.stateTimeout = stateTimeout
// Parse the name of the snapshot
snapNameBuf := new(bytes.Buffer)
tData := snapshotNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
} else {
t.Execute(snapNameBuf, tData)
b.config.SnapshotName = snapNameBuf.String()
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
+3 -3
View File
@@ -332,8 +332,8 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
t.Errorf("invalid: %d", b.config.RawSnapshotName)
if b.config.SnapshotName == "" {
t.Errorf("invalid: %s", b.config.SnapshotName)
}
// Test set
@@ -345,7 +345,7 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
}
// Test set with template
config["snapshot_name"] = "{{.CreateTime}}"
config["snapshot_name"] = "{{timestamp}}"
b = Builder{}
err = b.Prepare(config)
if err != nil {
+109 -22
View File
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
@@ -18,7 +18,6 @@ const BuilderId = "mitchellh.virtualbox"
type Builder struct {
config config
driver Driver
runner multistep.Runner
}
@@ -38,7 +37,7 @@ 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"`
@@ -51,12 +50,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 *packer.ConfigTemplate
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -65,6 +66,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
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)
@@ -124,6 +131,72 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
// Errors
templates := map[string]*string{
"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.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,
"boot_wait": &b.config.RawBootWait,
"shutdown_timeout": &b.config.RawShutdownTimeout,
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
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,
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
}
}
for i, file := range b.config.FloppyFiles {
var err error
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing floppy_files[%d]: %s",
i, err))
}
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
@@ -148,14 +221,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))
}
}
@@ -163,14 +243,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(
@@ -215,10 +287,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
b.driver, err = b.newDriver()
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
for i, args := range b.config.VBoxManage {
for j, arg := range args {
if err := b.config.tpl.Validate(arg); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing vboxmanage[%d][%d]: %s", i, j, err))
}
}
}
if errs != nil && len(errs.Errors) > 0 {
@@ -229,9 +304,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,
@@ -262,7 +349,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
+58
View File
@@ -84,6 +84,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 +211,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 +232,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 +241,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)
@@ -269,6 +273,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 +297,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 +309,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 +319,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 +386,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 +394,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 +414,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 +428,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 +436,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,6 +445,7 @@ 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)
@@ -403,12 +457,14 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
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 +488,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 +496,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)
@@ -2,18 +2,15 @@ package virtualbox
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"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/builder/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{}) {}
+8 -6
View File
@@ -1,13 +1,11 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"strings"
"text/template"
"time"
"unicode"
"unicode/utf8"
@@ -49,11 +47,15 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
ui.Say("Typing the boot command...")
for _, command := range config.BootCommand {
var buf bytes.Buffer
t := template.Must(template.New("boot").Parse(command))
t.Execute(&buf, tplData)
command, err := config.tpl.Process(command, tplData)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
for _, code := range scancodes(buf.String()) {
for _, code := range scancodes(command) {
if code == "wait" {
time.Sleep(1 * time.Second)
continue
@@ -1,12 +1,10 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
"text/template"
)
type guestAdditionsPathTemplate struct {
@@ -39,12 +37,16 @@ func (s *stepUploadGuestAdditions) Run(state map[string]interface{}) multistep.S
Version: version,
}
var processedPath bytes.Buffer
t := template.Must(template.New("path").Parse(config.GuestAdditionsPath))
t.Execute(&processedPath, tplData)
config.GuestAdditionsPath, err = config.tpl.Process(config.GuestAdditionsPath, tplData)
if err != nil {
err := fmt.Errorf("Error preparing guest additions path: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Uploading VirtualBox guest additions ISO...")
if err := comm.Upload(processedPath.String(), f); err != nil {
if err := comm.Upload(config.GuestAdditionsPath, f); err != nil {
state["error"] = fmt.Errorf("Error uploading guest additions: %s", err)
return multistep.ActionHalt
}
+8 -6
View File
@@ -1,12 +1,10 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strings"
"text/template"
)
type commandTemplate struct {
@@ -40,10 +38,14 @@ func (s *stepVBoxManage) Run(state map[string]interface{}) multistep.StepAction
copy(command, originalCommand)
for i, arg := range command {
var buf bytes.Buffer
t := template.Must(template.New("arg").Parse(arg))
t.Execute(&buf, tplData)
command[i] = buf.String()
var err error
command[i], err = config.tpl.Process(arg, tplData)
if err != nil {
err := fmt.Errorf("Error preparing vboxmanage command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
ui.Message(fmt.Sprintf("Executing: %s", strings.Join(command, " ")))
+111 -16
View File
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
@@ -19,7 +19,6 @@ const BuilderId = "mitchellh.vmware"
type Builder struct {
config config
driver Driver
runner multistep.Runner
}
@@ -32,7 +31,7 @@ type config struct {
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"`
@@ -52,12 +51,14 @@ type config struct {
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 *packer.ConfigTemplate
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -66,6 +67,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
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)
@@ -121,6 +128,81 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.ToolsUploadPath = "{{ .Flavor }}.iso"
}
// Errors
templates := map[string]*string{
"disk_name": &b.config.DiskName,
"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.RawSingleISOUrl,
"output_directory": &b.config.OutputDir,
"shutdown_command": &b.config.ShutdownCommand,
"ssh_password": &b.config.SSHPassword,
"ssh_username": &b.config.SSHUser,
"tools_upload_flavor": &b.config.ToolsUploadFlavor,
"vm_name": &b.config.VMName,
"boot_wait": &b.config.RawBootWait,
"shutdown_timeout": &b.config.RawShutdownTimeout,
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
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,
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
}
}
for i, file := range b.config.FloppyFiles {
var err error
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing floppy_files[%d]: %s",
i, err))
}
}
newVMXData := make(map[string]string)
for k, v := range b.config.VMXData {
k, err = b.config.tpl.Process(k, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing VMX data 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 VMX data value '%s': %s", v, err))
continue
}
newVMXData[k] = v
}
b.config.VMXData = newVMXData
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
@@ -145,14 +227,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))
}
}
@@ -207,12 +296,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
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
}
@@ -221,12 +304,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,
@@ -254,7 +349,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
+56
View File
@@ -52,6 +52,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 +72,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 +96,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 +108,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 +206,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 +215,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 +237,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 +304,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 +312,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,6 +332,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)
@@ -300,6 +350,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 +398,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 +422,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,6 +430,7 @@ 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)
@@ -397,6 +451,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 +460,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)
+4
View File
@@ -135,3 +135,7 @@ func (d *Workstation9Driver) ToolsIsoPath(flavor string) string {
func (d *Workstation9Driver) DhcpLeasesPath(device string) string {
return workstationDhcpLeasesPath(device)
}
func (d *Workstation9Driver) VmnetnatConfPath() string {
return workstationVmnetnatConfPath()
}
@@ -41,3 +41,7 @@ func workstationDhcpLeasesPath(device string) string {
func workstationToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func workstationVmnetnatConfPath() string {
return ""
}
+115 -30
View File
@@ -23,12 +23,7 @@ func workstationFindVdiskManager() (string, error) {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmware-vdiskmanager.exe"), nil
return findFile("vmware-vdiskmanager.exe", workstationProgramFilePaths()), nil
}
func workstationFindVMware() (string, error) {
@@ -37,12 +32,7 @@ func workstationFindVMware() (string, error) {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmware.exe"), nil
return findFile("vmware.exe", workstationProgramFilePaths()), nil
}
func workstationFindVmrun() (string, error) {
@@ -51,30 +41,26 @@ func workstationFindVmrun() (string, error) {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmrun.exe"), nil
return findFile("vmrun.exe", workstationProgramFilePaths()), nil
}
func workstationToolsIsoPath(flavor string) string {
path, err := workstationVMwareRoot()
if err != nil {
return ""
}
return filepath.Join(path, flavor+".iso")
return findFile(flavor+".iso", workstationProgramFilePaths())
}
func workstationDhcpLeasesPath(device string) string {
programData := os.Getenv("ProgramData")
if programData == "" {
return ""
path, err := workstationDhcpLeasesPathRegistry()
if err != nil {
log.Printf("Error finding leases in registry: %s", err)
} else if _, err := os.Stat(path); err == nil {
return path
}
return filepath.Join(programData, "/VMware/vmnetdhcp.leases")
return findFile("vmnetdhcp.leases", workstationDataFilePaths())
}
func workstationVmnetnatConfPath() string {
return findFile("vmnetnat.conf", workstationDataFilePaths())
}
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
@@ -126,6 +112,105 @@ func workstationVMwareRoot() (s string, err error) {
return
}
s = strings.Replace(s, "\\", "/", -1)
return
return normalizePath(s), nil
}
// This reads the VMware DHCP leases path from the Windows registry.
func workstationDhcpLeasesPathRegistry() (s string, err error) {
key := "SYSTEM\\CurrentControlSet\\services\\VMnetDHCP\\Parameters"
subkey := "LeaseFile"
s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey)
if err != nil {
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
return
}
return normalizePath(s), nil
}
func normalizePath(path string) string {
path = strings.Replace(path, "\\", "/", -1)
path = strings.Replace(path, "//", "/", -1)
path = strings.TrimRight(path, "/")
return path
}
func findFile(file string, paths []string) string {
for _, path := range paths {
path = filepath.Join(path, file)
path = normalizePath(path)
log.Printf("Searching for file '%s'", path)
if _, err := os.Stat(path); err == nil {
log.Printf("Found file '%s'", path)
return path
}
}
log.Printf("File not found: '%s'", file)
return ""
}
// workstationProgramFilesPaths returns a list of paths that are eligible
// to contain program files we may want just as vmware.exe.
func workstationProgramFilePaths() []string {
path, err := workstationVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
}
paths := make([]string, 0, 5)
if os.Getenv("VMWARE_HOME") != "" {
paths = append(paths, os.Getenv("VMWARE_HOME"))
}
if path != "" {
paths = append(paths, path)
}
if os.Getenv("ProgramFiles(x86)") != "" {
paths = append(paths,
filepath.Join(os.Getenv("ProgramFiles(x86)"), "/VMware/VMware Workstation"))
}
if os.Getenv("ProgramFiles") != "" {
paths = append(paths,
filepath.Join(os.Getenv("ProgramFiles"), "/VMware/VMware Workstation"))
}
return paths
}
// 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 := workstationDhcpLeasesPathRegistry()
if err != nil {
log.Printf("Error getting DHCP leases path: %s", err)
}
if leasesPath != "" {
leasesPath = filepath.Dir(leasesPath)
}
paths := make([]string, 0, 5)
if os.Getenv("VMWARE_DATA") != "" {
paths = append(paths, os.Getenv("VMWARE_DATA"))
}
if leasesPath != "" {
paths = append(paths, leasesPath)
}
if os.Getenv("ProgramData") != "" {
paths = append(paths,
filepath.Join(os.Getenv("ProgramData"), "/VMware"))
}
if os.Getenv("ALLUSERSPROFILE") != "" {
paths = append(paths,
filepath.Join(os.Getenv("ALLUSERSPROFILE"), "/Application Data/VMware"))
}
return paths
}
+7 -8
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
@@ -17,15 +16,15 @@ import (
type VMnetNatConfIPFinder struct{}
func (*VMnetNatConfIPFinder) HostIP() (string, error) {
programData := os.Getenv("ProgramData")
if programData == "" {
return "", errors.New("ProgramData directory not found.")
driver := &Workstation9Driver{}
vmnetnat := driver.VmnetnatConfPath()
if vmnetnat == "" {
return "", errors.New("Could not find NAT vmnet conf file")
}
programData = strings.Replace(programData, "\\", "/", -1)
vmnetnat := filepath.Join(programData, "/VMware/vmnetnat.conf")
if _, err := os.Stat(vmnetnat); err != nil {
return "", fmt.Errorf("Error with vmnetnat.conf: %s", err)
return "", fmt.Errorf("Could not find NAT vmnet conf file: %s", vmnetnat)
}
f, err := os.Open(vmnetnat)
@@ -62,5 +61,5 @@ func (*VMnetNatConfIPFinder) HostIP() (string, error) {
}
}
return "", errors.New("host IP not found in NAT config")
return "", errors.New("host IP not found in " + vmnetnat)
}
-89
View File
@@ -1,89 +0,0 @@
package vmware
import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/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{}) {}
+14 -6
View File
@@ -1,7 +1,6 @@
package vmware
import (
"bytes"
"fmt"
"github.com/mitchellh/go-vnc"
"github.com/mitchellh/multistep"
@@ -10,7 +9,6 @@ import (
"net"
"runtime"
"strings"
"text/template"
"time"
"unicode"
"unicode/utf8"
@@ -90,11 +88,21 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
ui.Say("Typing the boot command over VNC...")
for _, command := range config.BootCommand {
var buf bytes.Buffer
t := template.Must(template.New("boot").Parse(command))
t.Execute(&buf, tplData)
command, err := config.tpl.Process(command, tplData)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
vncSendString(c, buf.String())
// 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)
}
return multistep.ActionContinue
+8 -6
View File
@@ -1,12 +1,10 @@
package vmware
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
"text/template"
)
type toolsUploadPathTemplate struct {
@@ -34,11 +32,15 @@ func (*stepUploadTools) Run(state map[string]interface{}) multistep.StepAction {
defer f.Close()
tplData := &toolsUploadPathTemplate{Flavor: config.ToolsUploadFlavor}
var processedPath bytes.Buffer
t := template.Must(template.New("path").Parse(config.ToolsUploadPath))
t.Execute(&processedPath, tplData)
config.ToolsUploadPath, err = config.tpl.Process(config.ToolsUploadPath, tplData)
if err != nil {
err := fmt.Errorf("Error preparing upload path: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := comm.Upload(processedPath.String(), f); err != nil {
if err := comm.Upload(config.ToolsUploadPath, f); err != nil {
state["error"] = fmt.Errorf("Error uploading VMware Tools: %s", err)
return multistep.ActionHalt
}
+49 -59
View File
@@ -4,11 +4,12 @@ import (
"bytes"
"flag"
"fmt"
cmdcommon "github.com/mitchellh/packer/common/command"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"os/signal"
"strconv"
"strings"
"sync"
)
@@ -22,15 +23,13 @@ func (Command) Help() string {
func (c Command) Run(env packer.Environment, args []string) int {
var cfgDebug bool
var cfgForce bool
var cfgExcept []string
var cfgOnly []string
buildOptions := new(cmdcommon.BuildOptions)
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
cmdFlags.BoolVar(&cfgDebug, "debug", false, "debug mode for builds")
cmdFlags.BoolVar(&cfgForce, "force", false, "force a build if artifacts exist")
cmdFlags.Var((*stringSliceValue)(&cfgExcept), "except", "build all builds except these")
cmdFlags.Var((*stringSliceValue)(&cfgOnly), "only", "only build the given builds by name")
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
@@ -41,23 +40,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 1
}
if len(cfgOnly) > 0 && len(cfgExcept) > 0 {
env.Ui().Error("Only one of '-except' or '-only' may be specified.\n")
if err := buildOptions.Validate(); err != nil {
env.Ui().Error(err.Error())
env.Ui().Error("")
env.Ui().Error(c.Help())
return 1
}
userVars, err := buildOptions.AllUserVars()
if err != nil {
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
env.Ui().Error("")
env.Ui().Error(c.Help())
return 1
}
// Read the file into a byte array so that we can parse the template
log.Printf("Reading template: %s", args[0])
tplData, err := ioutil.ReadFile(args[0])
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
return 1
}
// Parse the template into a machine-usable format
log.Println("Parsing template...")
tpl, err := packer.ParseTemplate(tplData)
tpl, err := packer.ParseTemplateFile(args[0])
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
@@ -72,47 +72,10 @@ func (c Command) Run(env packer.Environment, args []string) int {
}
// Go through each builder and compile the builds that we care about
buildNames := tpl.BuildNames()
builds := make([]packer.Build, 0, len(buildNames))
for _, buildName := range buildNames {
if len(cfgExcept) > 0 {
found := false
for _, only := range cfgExcept {
if buildName == only {
found = true
break
}
}
if found {
log.Printf("Skipping build '%s' because specified by -except.", buildName)
continue
}
}
if len(cfgOnly) > 0 {
found := false
for _, only := range cfgOnly {
if buildName == only {
found = true
break
}
}
if !found {
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
continue
}
}
log.Printf("Creating build: %s", buildName)
build, err := tpl.Build(buildName, components)
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to create build '%s': \n\n%s", buildName, err))
return 1
}
builds = append(builds, build)
builds, err := buildOptions.Builds(tpl, components)
if err != nil {
env.Ui().Error(err.Error())
return 1
}
if cfgDebug {
@@ -150,7 +113,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
log.Printf("Preparing build: %s", b.Name())
b.SetDebug(cfgDebug)
b.SetForce(cfgForce)
err := b.Prepare()
err := b.Prepare(userVars)
if err != nil {
env.Ui().Error(err.Error())
return 1
@@ -233,7 +196,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
if len(artifacts) > 0 {
env.Ui().Say("\n==> Builds finished. The artifacts of successful builds are:")
for name, buildArtifacts := range artifacts {
for _, artifact := range buildArtifacts {
// Create a UI for the machine readable stuff to be targetted
ui := &packer.TargettedUi{
Target: name,
Ui: env.Ui(),
}
// Machine-readable helpful
ui.Machine("artifact-count", strconv.FormatInt(int64(len(buildArtifacts)), 10))
for i, artifact := range buildArtifacts {
var message bytes.Buffer
fmt.Fprintf(&message, "--> %s: ", name)
@@ -243,6 +215,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
fmt.Fprint(&message, "<nothing>")
}
iStr := strconv.FormatInt(int64(i), 10)
if artifact != nil {
ui.Machine("artifact", iStr, "builder-id", artifact.BuilderId())
ui.Machine("artifact", iStr, "id", artifact.Id())
ui.Machine("artifact", iStr, "string", artifact.String())
files := artifact.Files()
ui.Machine("artifact",
iStr,
"files-count", strconv.FormatInt(int64(len(files)), 10))
for fi, file := range files {
fiStr := strconv.FormatInt(int64(fi), 10)
ui.Machine("artifact", iStr, "file", fiStr, file)
}
} else {
ui.Machine("artifact", iStr, "nil")
}
env.Ui().Say(message.String())
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func testEnvironment() packer.Environment {
config := packer.DefaultEnvironmentConfig()
config.Ui = &packer.ReaderWriterUi{
config.Ui = &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
+3
View File
@@ -10,6 +10,9 @@ Options:
-debug Debug mode enabled for builds
-force Force a build to continue if artifacts exist, deletes existing artifacts
-machine-readable Machine-readable output
-except=foo,bar,baz Build all builds other than these
-only=foo,bar,baz Only build the given builds by name
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`
-14
View File
@@ -1,14 +0,0 @@
package build
import "strings"
type stringSliceValue []string
func (s *stringSliceValue) String() string {
return strings.Join(*s, ",")
}
func (s *stringSliceValue) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
+13 -2
View File
@@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
)
@@ -49,10 +50,20 @@ func (c Command) Run(env packer.Environment, args []string) int {
tplF.Close()
// Run the template through the various fixers
fixers := []Fixer{Fixers["iso-md5"]}
fixers := []string{
"iso-md5",
"createtime",
}
input := templateData
for _, fixer := range fixers {
for _, name := range fixers {
var err error
fixer, ok := Fixers[name]
if !ok {
panic("fixer not found: " + name)
}
log.Printf("Running fixer: %s", name)
input, err = fixer.Fix(input)
if err != nil {
env.Ui().Error(fmt.Sprintf("Error fixing: %s", err))
+2 -1
View File
@@ -13,6 +13,7 @@ var Fixers map[string]Fixer
func init() {
Fixers = map[string]Fixer{
"iso-md5": new(FixerISOMD5),
"iso-md5": new(FixerISOMD5),
"createtime": new(FixerCreateTime),
}
}
+51
View File
@@ -0,0 +1,51 @@
package fix
import (
"github.com/mitchellh/mapstructure"
"regexp"
)
// FixerCreateTime is a Fixer that replaces the ".CreateTime" template
// calls with "{{timestamp}"
type FixerCreateTime struct{}
func (FixerCreateTime) Fix(input map[string]interface{}) (map[string]interface{}, error) {
// Our template type we'll use for this fixer only
type template struct {
Builders []map[string]interface{}
}
// Decode the input into our structure, if we can
var tpl template
if err := mapstructure.Decode(input, &tpl); err != nil {
return nil, err
}
badKeys := []string{
"ami_name",
"bundle_prefix",
"snapshot_name",
}
re := regexp.MustCompile(`{{\s*\.CreateTime\s*}}`)
// Go through each builder and replace CreateTime if we can
for _, builder := range tpl.Builders {
for _, key := range badKeys {
raw, ok := builder[key]
if !ok {
continue
}
v, ok := raw.(string)
if !ok {
continue
}
builder[key] = re.ReplaceAllString(v, "{{timestamp}}")
}
}
input["builders"] = tpl.Builders
return input, nil
}
+45
View File
@@ -0,0 +1,45 @@
package fix
import (
"reflect"
"testing"
)
func TestFixerCreateTime_Impl(t *testing.T) {
var raw interface{}
raw = new(FixerCreateTime)
if _, ok := raw.(Fixer); !ok {
t.Fatalf("must be a Fixer")
}
}
func TestFixerCreateTime_Fix(t *testing.T) {
var f FixerCreateTime
input := map[string]interface{}{
"builders": []interface{}{
map[string]string{
"type": "foo",
"ami_name": "{{.CreateTime}} foo",
},
},
}
expected := map[string]interface{}{
"builders": []map[string]interface{}{
map[string]interface{}{
"type": "foo",
"ami_name": "{{timestamp}} foo",
},
},
}
output, err := f.Fix(input)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(output, expected) {
t.Fatalf("unexpected: %#v\nexpected: %#v\n", output, expected)
}
}
+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 -19
View File
@@ -3,8 +3,8 @@ package validate
import (
"flag"
"fmt"
cmdcommon "github.com/mitchellh/packer/common/command"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"strings"
)
@@ -17,10 +17,12 @@ func (Command) Help() string {
func (c Command) Run(env packer.Environment, args []string) int {
var cfgSyntaxOnly bool
buildOptions := new(cmdcommon.BuildOptions)
cmdFlags := flag.NewFlagSet("validate", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
@@ -31,17 +33,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 1
}
// Read the file into a byte array so that we can parse the template
log.Printf("Reading template: %s", args[0])
tplData, err := ioutil.ReadFile(args[0])
if err := buildOptions.Validate(); err != nil {
env.Ui().Error(err.Error())
env.Ui().Error("")
env.Ui().Error(c.Help())
return 1
}
userVars, err := buildOptions.AllUserVars()
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
env.Ui().Error("")
env.Ui().Error(c.Help())
return 1
}
// Parse the template into a machine-usable format
log.Println("Parsing template...")
tpl, err := packer.ParseTemplate(tplData)
log.Printf("Reading template: %s", args[0])
tpl, err := packer.ParseTemplateFile(args[0])
if err != nil {
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
@@ -63,23 +72,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
}
// Otherwise, get all the builds
buildNames := tpl.BuildNames()
builds := make([]packer.Build, 0, len(buildNames))
for _, buildName := range buildNames {
log.Printf("Creating build from template for: %s", buildName)
build, err := tpl.Build(buildName, components)
if err != nil {
errs = append(errs, fmt.Errorf("Build '%s': %s", buildName, err))
continue
}
builds = append(builds, build)
builds, err := buildOptions.Builds(tpl, components)
if err != nil {
env.Ui().Error(err.Error())
return 1
}
// Check the configuration of all builds
for _, b := range builds {
log.Printf("Preparing build: %s", b.Name())
err := b.Prepare()
err := b.Prepare(userVars)
if err != nil {
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
}
+5 -1
View File
@@ -12,5 +12,9 @@ Usage: packer validate [options] TEMPLATE
Options:
-syntax-only Only check syntax. Do not verify config of the template.
-syntax-only Only check syntax. Do not verify config of the template.
-except=foo,bar,baz Validate all builds other than these
-only=foo,bar,baz Validate only these builds
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`
+39
View File
@@ -0,0 +1,39 @@
package command
import (
"flag"
"fmt"
"strings"
)
// BuildOptionFlags sets the proper command line flags needed for
// build options.
func BuildOptionFlags(fs *flag.FlagSet, f *BuildOptions) {
fs.Var((*SliceValue)(&f.Except), "except", "build all builds except these")
fs.Var((*SliceValue)(&f.Only), "only", "only build the given builds by name")
fs.Var((*userVarValue)(&f.UserVars), "var", "specify a user variable")
fs.Var((*AppendSliceValue)(&f.UserVarFiles), "var-file", "file with user variables")
}
// userVarValue is a flag.Value that parses out user variables in
// the form of 'key=value' and sets it on this map.
type userVarValue map[string]string
func (v *userVarValue) String() string {
return ""
}
func (v *userVarValue) Set(raw string) error {
idx := strings.Index(raw, "=")
if idx == -1 {
return fmt.Errorf("No '=' value in arg: %s", raw)
}
if *v == nil {
*v = make(map[string]string)
}
key, value := raw[0:idx], raw[idx+1:]
(*v)[key] = value
return nil
}
+104
View File
@@ -0,0 +1,104 @@
package command
import (
"flag"
"reflect"
"testing"
)
func TestBuildOptionFlags(t *testing.T) {
opts := new(BuildOptions)
fs := flag.NewFlagSet("test", flag.ContinueOnError)
BuildOptionFlags(fs, opts)
args := []string{
"-except=foo,bar,baz",
"-only=a,b",
"-var=foo=bar",
"-var", "bar=baz",
"-var=foo=bang",
"-var-file=foo",
"-var-file=bar",
}
err := fs.Parse(args)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"foo", "bar", "baz"}
if !reflect.DeepEqual(opts.Except, expected) {
t.Fatalf("bad: %#v", opts.Except)
}
expected = []string{"a", "b"}
if !reflect.DeepEqual(opts.Only, expected) {
t.Fatalf("bad: %#v", opts.Only)
}
if len(opts.UserVars) != 2 {
t.Fatalf("bad: %#v", opts.UserVars)
}
if opts.UserVars["foo"] != "bang" {
t.Fatalf("bad: %#v", opts.UserVars)
}
if opts.UserVars["bar"] != "baz" {
t.Fatalf("bad: %#v", opts.UserVars)
}
expected = []string{"foo", "bar"}
if !reflect.DeepEqual(opts.UserVarFiles, expected) {
t.Fatalf("bad: %#v", opts.UserVarFiles)
}
}
func TestUserVarValue_implements(t *testing.T) {
var raw interface{}
raw = new(userVarValue)
if _, ok := raw.(flag.Value); !ok {
t.Fatalf("userVarValue should be a Value")
}
}
func TestUserVarValueSet(t *testing.T) {
sv := new(userVarValue)
err := sv.Set("key=value")
if err != nil {
t.Fatalf("err: %s", err)
}
vars := map[string]string(*sv)
if vars["key"] != "value" {
t.Fatalf("Bad: %#v", vars)
}
// Empty value
err = sv.Set("key=")
if err != nil {
t.Fatalf("err: %s", err)
}
vars = map[string]string(*sv)
if vars["key"] != "" {
t.Fatalf("Bad: %#v", vars)
}
// Equal in value
err = sv.Set("key=foo=bar")
if err != nil {
t.Fatalf("err: %s", err)
}
vars = map[string]string(*sv)
if vars["key"] != "foo=bar" {
t.Fatalf("Bad: %#v", vars)
}
// No equal
err = sv.Set("key")
if err == nil {
t.Fatal("should have error")
}
}
+34
View File
@@ -0,0 +1,34 @@
package command
import "strings"
// AppendSliceValue implements the flag.Value interface and allows multiple
// calls to the same variable to append a list.
type AppendSliceValue []string
func (s *AppendSliceValue) String() string {
return strings.Join(*s, ",")
}
func (s *AppendSliceValue) Set(value string) error {
if *s == nil {
*s = make([]string, 0, 1)
}
*s = append(*s, value)
return nil
}
// SliceValue implements the flag.Value interface and allows a list of
// strings to be given on the command line and properly parsed into a slice
// of strings internally.
type SliceValue []string
func (s *SliceValue) String() string {
return strings.Join(*s, ",")
}
func (s *SliceValue) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package command
import (
"flag"
"reflect"
"testing"
)
func TestAppendSliceValue_implements(t *testing.T) {
var raw interface{}
raw = new(AppendSliceValue)
if _, ok := raw.(flag.Value); !ok {
t.Fatalf("AppendSliceValue should be a Value")
}
}
func TestAppendSliceValueSet(t *testing.T) {
sv := new(AppendSliceValue)
err := sv.Set("foo")
if err != nil {
t.Fatalf("err: %s", err)
}
err = sv.Set("bar")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"foo", "bar"}
if !reflect.DeepEqual([]string(*sv), expected) {
t.Fatalf("Bad: %#v", sv)
}
}
func TestSliceValue_implements(t *testing.T) {
var raw interface{}
raw = new(SliceValue)
if _, ok := raw.(flag.Value); !ok {
t.Fatalf("SliceValue should be a Value")
}
}
func TestSliceValueSet(t *testing.T) {
sv := new(SliceValue)
err := sv.Set("foo,bar,baz")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"foo", "bar", "baz"}
if !reflect.DeepEqual([]string(*sv), expected) {
t.Fatalf("Bad: %#v", sv)
}
}
+125
View File
@@ -0,0 +1,125 @@
package command
import (
"errors"
"fmt"
jsonutil "github.com/mitchellh/packer/common/json"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
)
// BuildOptions is a set of options related to builds that can be set
// from the command line.
type BuildOptions struct {
UserVarFiles []string
UserVars map[string]string
Except []string
Only []string
}
// Validate validates the options
func (f *BuildOptions) Validate() error {
if len(f.Except) > 0 && len(f.Only) > 0 {
return errors.New("Only one of '-except' or '-only' may be specified.")
}
if len(f.UserVarFiles) > 0 {
for _, path := range f.UserVarFiles {
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("Cannot access: %s", path)
}
}
}
return nil
}
// AllUserVars returns the user variables, compiled from both the
// file paths and the vars on the command line.
func (f *BuildOptions) AllUserVars() (map[string]string, error) {
all := make(map[string]string)
// Copy in the variables from the files
for _, path := range f.UserVarFiles {
fileVars, err := readFileVars(path)
if err != nil {
return nil, err
}
for k, v := range fileVars {
all[k] = v
}
}
// Copy in the command-line vars
for k, v := range f.UserVars {
all[k] = v
}
return all, nil
}
// Builds returns the builds out of the given template that pass the
// configured options.
func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([]packer.Build, error) {
buildNames := t.BuildNames()
builds := make([]packer.Build, 0, len(buildNames))
for _, buildName := range buildNames {
if len(f.Except) > 0 {
found := false
for _, except := range f.Except {
if buildName == except {
found = true
break
}
}
if found {
log.Printf("Skipping build '%s' because specified by -except.", buildName)
continue
}
}
if len(f.Only) > 0 {
found := false
for _, only := range f.Only {
if buildName == only {
found = true
break
}
}
if !found {
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
continue
}
}
log.Printf("Creating build: %s", buildName)
build, err := t.Build(buildName, cf)
if err != nil {
return nil, fmt.Errorf("Failed to create build '%s': \n\n%s", buildName, err)
}
builds = append(builds, build)
}
return builds, nil
}
func readFileVars(path string) (map[string]string, error) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
vars := make(map[string]string)
err = jsonutil.Unmarshal(bytes, &vars)
if err != nil {
return nil, err
}
return vars, nil
}
+53
View File
@@ -0,0 +1,53 @@
package command
import (
"testing"
)
func TestBuildOptionsValidate(t *testing.T) {
bf := new(BuildOptions)
err := bf.Validate()
if err != nil {
t.Fatalf("err: %s", err)
}
// Both set
bf.Except = make([]string, 1)
bf.Only = make([]string, 1)
err = bf.Validate()
if err == nil {
t.Fatal("should error")
}
// One set
bf.Except = make([]string, 1)
bf.Only = make([]string, 0)
err = bf.Validate()
if err != nil {
t.Fatalf("err: %s", err)
}
bf.Except = make([]string, 0)
bf.Only = make([]string, 1)
err = bf.Validate()
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestBuildOptionsValidate_userVarFiles(t *testing.T) {
bf := new(BuildOptions)
err := bf.Validate()
if err != nil {
t.Fatalf("err: %s", err)
}
// Non-existent file
bf.UserVarFiles = []string{"ireallyshouldntexistanywhere"}
err = bf.Validate()
if err == nil {
t.Fatal("should error")
}
}
@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
)
@@ -63,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
@@ -73,6 +83,17 @@ func DownloadableURL(original string) (string, error) {
}
if url.Scheme == "file" {
// For Windows absolute file paths, remove leading / prior to processing
// since net/url turns "C:/" into "/C:/"
if runtime.GOOS == "windows" && url.Path[0] == '/' {
url.Path = url.Path[1:len(url.Path)]
// Also replace all backslashes with forwardslashes since Windows
// users are likely to do this but the URL should actually only
// contain forward slashes.
url.Path = strings.Replace(url.Path, `\`, `/`, -1)
}
if _, err := os.Stat(url.Path); err != nil {
return "", err
}
@@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"os"
"runtime"
)
// DownloadConfig is the configuration given to instantiate a new
@@ -107,6 +108,11 @@ func (d *DownloadClient) Get() (string, error) {
var finalPath string
if url.Scheme == "file" && !d.config.CopyFile {
finalPath = url.Path
// Remove forward slash on absolute Windows file URLs before processing
if runtime.GOOS == "windows" && finalPath[0] == '/' {
finalPath = finalPath[1:len(finalPath)]
}
} else {
finalPath = d.config.TargetPath
@@ -142,12 +148,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
+40
View File
@@ -0,0 +1,40 @@
package json
import (
"bytes"
"encoding/json"
"fmt"
)
// Unmarshal is wrapper around json.Unmarshal that returns user-friendly
// errors when there are syntax errors.
func Unmarshal(data []byte, i interface{}) error {
err := json.Unmarshal(data, i)
if err != nil {
syntaxErr, ok := err.(*json.SyntaxError)
if !ok {
return err
}
// We have a syntax error. Extract out the line number and friends.
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
newline := []byte{'\x0a'}
// Calculate the start/end position of the line where the error is
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
end := len(data)
if idx := bytes.Index(data[start:], newline); idx >= 0 {
end = start + idx
}
// Count the line number we're on plus the offset in the line
line := bytes.Count(data[:start], newline) + 1
pos := int(syntaxErr.Offset) - start - 1
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
line, pos, syntaxErr, data[start:end])
return err
}
return nil
}
+1
View File
@@ -0,0 +1 @@
package json
+12
View File
@@ -0,0 +1,12 @@
package common
// PackerConfig is a struct that contains the configuration keys that
// are sent by packer, properly tagged already so mapstructure can load
// them. Embed this structure into your configuration class to get it.
type PackerConfig struct {
PackerBuildName string `mapstructure:"packer_build_name"`
PackerBuilderType string `mapstructure:"packer_builder_type"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables"`
}
+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")
}
}
+11 -2
View File
@@ -205,9 +205,14 @@ func (c *comm) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *comm) newSession() (*ssh.Session, error) {
func (c *comm) newSession() (session *ssh.Session, err error) {
log.Println("opening new ssh session")
session, err := c.client.NewSession()
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 {
@@ -225,6 +230,10 @@ func (c *comm) reconnect() (err error) {
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 {
+17
View File
@@ -10,6 +10,9 @@ import (
"io"
)
// SimpleKeychain makes it easy to use private keys in order to connect
// via SSH, since the interface exposed by Go isn't the easiest to use
// right away.
type SimpleKeychain struct {
keys []interface{}
}
@@ -26,6 +29,20 @@ func (k *SimpleKeychain) AddPEMKey(key string) (err error) {
return
}
// AddPEMKeyPassword adds a PEM encoded private key that is protected by
// a password to the keychain.
func (k *SimpleKeychain) AddPEMKeyPassword(key string, password string) (err error) {
block, _ := pem.Decode([]byte(key))
bytes, _ := x509.DecryptPEMBlock(block, []byte(password))
rsakey, err := x509.ParsePKCS1PrivateKey(bytes)
if err != nil {
return
}
k.keys = append(k.keys, rsakey)
return
}
// Key method for ssh.ClientKeyring interface
func (k *SimpleKeychain) Key(i int) (interface{}, error) {
if i < 0 || i >= len(k.keys) {
+1
View File
@@ -30,6 +30,7 @@ const defaultConfig = `
"commands": {
"build": "packer-command-build",
"fix": "packer-command-fix",
"inspect": "packer-command-inspect",
"validate": "packer-command-validate"
},
+109 -28
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,19 +102,24 @@ 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)
cache := &packer.FileCache{CacheDir: cacheDir}
// Determine if we're in machine-readable mode by mucking around with
// the arguments...
args, machineReadable := extractMachineReadable(os.Args[1:])
defer plugin.CleanupClients()
// Create the environment configuration
envConfig := packer.DefaultEnvironmentConfig()
envConfig.Cache = cache
envConfig.Commands = config.CommandNames()
@@ -86,23 +128,44 @@ func main() {
envConfig.Components.Hook = config.LoadHook
envConfig.Components.PostProcessor = config.LoadPostProcessor
envConfig.Components.Provisioner = config.LoadProvisioner
if machineReadable {
envConfig.Ui = &packer.MachineReadableUi{
Writer: os.Stdout,
}
}
env, err := packer.NewEnvironment(envConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
os.Exit(1)
return 1
}
setupSignalHandlers(env)
exitCode, err := env.Cli(os.Args[1:])
exitCode, err := env.Cli(args)
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
os.Exit(1)
return 1
}
plugin.CleanupClients()
os.Exit(exitCode)
return exitCode
}
// extractMachineReadable checks the args for the machine readable
// flag and returns whether or not it is on. It modifies the args
// to remove this flag.
func extractMachineReadable(args []string) ([]string, bool) {
for i, arg := range args {
if arg == "-machine-readable" {
// We found it. Slice it out.
result := make([]string, len(args)-1)
copy(result, args[:i])
copy(result[i:], args[i+1:])
return result, true
}
}
return args, false
}
func loadConfig() (*config, error) {
@@ -149,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
}
+46 -16
View File
@@ -23,6 +23,10 @@ const (
// This is the key in configurations that is set to "true" when Packer
// force build is enabled.
ForceConfigKey = "packer_force"
// This key contains a map[string]string of the user variables for
// template processing.
UserVariablesConfigKey = "packer_user_variables"
)
// A Build represents a single job within Packer that is responsible for
@@ -35,7 +39,7 @@ type Build interface {
// Prepare configures the various components of this build and reports
// any errors in doing so (such as syntax errors, validation errors, etc.)
Prepare() error
Prepare(v map[string]string) error
// Run runs the actual builder, returning an artifact implementation
// of what is built. If anything goes wrong, an error is returned.
@@ -73,6 +77,7 @@ type coreBuild struct {
hooks map[string][]Hook
postProcessors [][]coreBuildPostProcessor
provisioners []coreBuildProvisioner
variables map[string]string
debug bool
force bool
@@ -102,8 +107,9 @@ func (b *coreBuild) Name() string {
}
// Prepare prepares the build by doing some initialization for the builder
// and any hooks. This _must_ be called prior to Run.
func (b *coreBuild) Prepare() (err error) {
// and any hooks. This _must_ be called prior to Run. The parameter is the
// overrides for the variables within the template (if any).
func (b *coreBuild) Prepare(userVars map[string]string) (err error) {
b.l.Lock()
defer b.l.Unlock()
@@ -113,11 +119,37 @@ func (b *coreBuild) Prepare() (err error) {
b.prepareCalled = true
// Compile the variables
variables := make(map[string]string)
for k, v := range b.variables {
variables[k] = v
}
if userVars != nil {
errs := make([]error, 0)
for k, v := range userVars {
if _, ok := variables[k]; !ok {
errs = append(
errs, fmt.Errorf("Unknown user variable: %s", k))
continue
}
variables[k] = v
}
if len(errs) > 0 {
return &MultiError{
Errors: errs,
}
}
}
packerConfig := map[string]interface{}{
BuildNameConfigKey: b.name,
BuilderTypeConfigKey: b.builderType,
DebugConfigKey: b.debug,
ForceConfigKey: b.force,
BuildNameConfigKey: b.name,
BuilderTypeConfigKey: b.builderType,
DebugConfigKey: b.debug,
ForceConfigKey: b.force,
UserVariablesConfigKey: variables,
}
// Prepare the builder
@@ -181,11 +213,10 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
hook := &DispatchHook{hooks}
artifacts := make([]Artifact, 0, 1)
// The builder just has a normal Ui, but prefixed
builderUi := &PrefixedUi{
fmt.Sprintf("==> %s", b.Name()),
fmt.Sprintf(" %s", b.Name()),
originalUi,
// The builder just has a normal Ui, but targetted
builderUi := &TargettedUi{
Target: b.Name(),
Ui: originalUi,
}
log.Printf("Running builder: %s", b.builderType)
@@ -208,10 +239,9 @@ PostProcessorRunSeqLoop:
for _, ppSeq := range b.postProcessors {
priorArtifact := builderArtifact
for i, corePP := range ppSeq {
ppUi := &PrefixedUi{
fmt.Sprintf("==> %s (%s)", b.Name(), corePP.processorType),
fmt.Sprintf(" %s (%s)", b.Name(), corePP.processorType),
originalUi,
ppUi := &TargettedUi{
Target: fmt.Sprintf("%s (%s)", b.Name(), corePP.processorType),
Ui: originalUi,
}
builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType))
+81 -23
View File
@@ -23,6 +23,7 @@ func testBuild() *coreBuild {
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
},
},
variables: make(map[string]string),
}
}
@@ -30,6 +31,15 @@ func testBuilder() *TestBuilder {
return &TestBuilder{}
}
func testDefaultPackerConfig() map[string]interface{} {
return map[string]interface{}{
BuildNameConfigKey: "test",
BuilderTypeConfigKey: "foo",
DebugConfigKey: false,
ForceConfigKey: false,
UserVariablesConfigKey: make(map[string]string),
}
}
func TestBuild_Name(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
@@ -39,18 +49,12 @@ func TestBuild_Name(t *testing.T) {
func TestBuild_Prepare(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
packerConfig := map[string]interface{}{
BuildNameConfigKey: "test",
BuilderTypeConfigKey: "foo",
DebugConfigKey: false,
ForceConfigKey: false,
}
packerConfig := testDefaultPackerConfig()
build := testBuild()
builder := build.builder.(*TestBuilder)
build.Prepare()
build.Prepare(nil)
assert.True(builder.prepareCalled, "prepare should be called")
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
@@ -67,7 +71,7 @@ func TestBuild_Prepare(t *testing.T) {
func TestBuild_Prepare_Twice(t *testing.T) {
build := testBuild()
if err := build.Prepare(); err != nil {
if err := build.Prepare(nil); err != nil {
t.Fatalf("bad error: %s", err)
}
@@ -82,24 +86,20 @@ func TestBuild_Prepare_Twice(t *testing.T) {
}
}()
build.Prepare()
build.Prepare(nil)
}
func TestBuild_Prepare_Debug(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
packerConfig := map[string]interface{}{
BuildNameConfigKey: "test",
BuilderTypeConfigKey: "foo",
DebugConfigKey: true,
ForceConfigKey: false,
}
packerConfig := testDefaultPackerConfig()
packerConfig[DebugConfigKey] = true
build := testBuild()
builder := build.builder.(*TestBuilder)
build.SetDebug(true)
build.Prepare()
build.Prepare(nil)
assert.True(builder.prepareCalled, "prepare should be called")
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
@@ -109,6 +109,64 @@ func TestBuild_Prepare_Debug(t *testing.T) {
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
}
func TestBuildPrepare_variables_default(t *testing.T) {
packerConfig := testDefaultPackerConfig()
packerConfig[UserVariablesConfigKey] = map[string]string{
"foo": "bar",
}
build := testBuild()
build.variables["foo"] = "bar"
builder := build.builder.(*TestBuilder)
err := build.Prepare(nil)
if err != nil {
t.Fatalf("err: %s", err)
}
if !builder.prepareCalled {
t.Fatal("prepare should be called")
}
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
}
}
func TestBuildPrepare_variables_nonexist(t *testing.T) {
build := testBuild()
build.variables["foo"] = "bar"
err := build.Prepare(map[string]string{"bar": "baz"})
if err == nil {
t.Fatal("should have had error")
}
}
func TestBuildPrepare_variables_override(t *testing.T) {
packerConfig := testDefaultPackerConfig()
packerConfig[UserVariablesConfigKey] = map[string]string{
"foo": "baz",
}
build := testBuild()
build.variables["foo"] = "bar"
builder := build.builder.(*TestBuilder)
err := build.Prepare(map[string]string{"foo": "baz"})
if err != nil {
t.Fatalf("err: %s", err)
}
if !builder.prepareCalled {
t.Fatal("prepare should be called")
}
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
}
}
func TestBuild_Run(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
@@ -116,7 +174,7 @@ func TestBuild_Run(t *testing.T) {
ui := testUi()
build := testBuild()
build.Prepare()
build.Prepare(nil)
artifacts, err := build.Run(ui, cache)
assert.Nil(err, "should not error")
assert.Equal(len(artifacts), 2, "should have two artifacts")
@@ -152,7 +210,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
build := testBuild()
build.postProcessors = [][]coreBuildPostProcessor{}
build.Prepare()
build.Prepare(nil)
artifacts, err := build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
@@ -177,7 +235,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
},
}
build.Prepare()
build.Prepare(nil)
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
@@ -205,7 +263,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
},
}
build.Prepare()
build.Prepare(nil)
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
@@ -235,7 +293,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
},
}
build.Prepare()
build.Prepare(nil)
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
@@ -262,7 +320,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
},
}
build.Prepare()
build.Prepare(nil)
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
+26 -2
View File
@@ -72,6 +72,14 @@ 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.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 +116,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
}
@@ -156,3 +164,19 @@ func (r *RemoteCmd) Wait() {
<-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
}
+1 -1
View File
@@ -47,7 +47,7 @@ func TestRemoteCmd_StartWithUi(t *testing.T) {
Stdout: rcOutput,
}
testUi := &ReaderWriterUi{
testUi := &BasicUi{
Reader: new(bytes.Buffer),
Writer: uiOutput,
}
+81
View File
@@ -0,0 +1,81 @@
package packer
import (
"bytes"
"fmt"
"strconv"
"text/template"
"time"
)
// 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 ConfigTemplate struct {
UserVars map[string]string
root *template.Template
i int
}
// NewConfigTemplate creates a new configuration template processor.
func NewConfigTemplate() (*ConfigTemplate, error) {
result := &ConfigTemplate{
UserVars: make(map[string]string),
}
result.root = template.New("configTemplateRoot")
result.root.Funcs(template.FuncMap{
"timestamp": templateTimestamp,
"user": result.templateUser,
})
return result, nil
}
// Process processes a single string, compiling and executing the template.
func (t *ConfigTemplate) Process(s string, data interface{}) (string, error) {
tpl, err := t.root.New(t.nextTemplateName()).Parse(s)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
if err := tpl.Execute(buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// Validate the template.
func (t *ConfigTemplate) Validate(s string) error {
root, err := t.root.Clone()
if err != nil {
return err
}
_, err = root.New("template").Parse(s)
return err
}
func (t *ConfigTemplate) nextTemplateName() string {
name := fmt.Sprintf("tpl%d", t.i)
t.i++
return name
}
// User is the function exposed as "user" within the templates and
// looks up user variables.
func (t *ConfigTemplate) templateUser(n string) (string, error) {
result, ok := t.UserVars[n]
if !ok {
return "", fmt.Errorf("uknown user var: %s", n)
}
return result, nil
}
func templateTimestamp() string {
return strconv.FormatInt(time.Now().UTC().Unix(), 10)
}
+67
View File
@@ -0,0 +1,67 @@
package packer
import (
"math"
"strconv"
"testing"
"time"
)
func TestConfigTemplateProcess_timestamp(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
result, err := tpl.Process(`{{timestamp}}`, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
val, err := strconv.ParseInt(result, 10, 64)
if err != nil {
t.Fatalf("err: %s", err)
}
currentTime := time.Now().UTC().Unix()
if math.Abs(float64(currentTime-val)) > 10 {
t.Fatalf("val: %d (current: %d)", val, currentTime)
}
}
func TestConfigTemplateProcess_user(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
tpl.UserVars["foo"] = "bar"
result, err := tpl.Process(`{{user "foo"}}`, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
if result != "bar" {
t.Fatalf("bad: %s", result)
}
}
func TestConfigTemplateValidate(t *testing.T) {
tpl, err := NewConfigTemplate()
if err != nil {
t.Fatalf("err: %s", err)
}
// Valid
err = tpl.Validate(`{{user "foo"}}`)
if err != nil {
t.Fatalf("err: %s", err)
}
// Invalid
err = tpl.Validate(`{{idontexist}}`)
if err == nil {
t.Fatal("should have error")
}
}
+69 -19
View File
@@ -8,6 +8,7 @@ import (
"os"
"sort"
"strings"
"sync"
)
// The function type used to lookup Builder implementations.
@@ -68,12 +69,18 @@ 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 {
config := &EnvironmentConfig{}
config.Commands = make([]string, 0)
config.Ui = &ReaderWriterUi{
config.Ui = &BasicUi{
Reader: os.Stdin,
Writer: os.Stdout,
}
@@ -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,39 +273,82 @@ 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
}
// Pad the key with spaces so that they're all the same width
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
for _, message := range entries {
if message != "" {
e.ui.Say(message)
}
}
}()
// 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.")
}
// Returns the UI for the environment. The UI is the interface that should
+16 -5
View File
@@ -17,9 +17,20 @@ 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 = &ReaderWriterUi{
config.Ui = &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
@@ -45,8 +56,8 @@ func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
config := DefaultEnvironmentConfig()
assert.NotNil(config.Ui, "default UI should not be nil")
rwUi, ok := config.Ui.(*ReaderWriterUi)
assert.True(ok, "default UI should be ReaderWriterUi")
rwUi, ok := config.Ui.(*BasicUi)
assert.True(ok, "default UI should be BasicUi")
assert.Equal(rwUi.Writer, os.Stdout, "default UI should go to stdout")
assert.Equal(rwUi.Reader, os.Stdin, "default UI should read from stdin")
}
@@ -175,7 +186,7 @@ func TestEnvironment_DefaultCli_Help(t *testing.T) {
// A little lambda to help us test the output actually contains help
testOutput := func() {
buffer := defaultEnv.Ui().(*ReaderWriterUi).Writer.(*bytes.Buffer)
buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer)
output := buffer.String()
buffer.Reset()
assert.True(strings.Contains(output, "usage: packer"), "should print help")
@@ -341,7 +352,7 @@ func TestEnvironmentProvisioner_Error(t *testing.T) {
func TestEnvironment_SettingUi(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
ui := &ReaderWriterUi{
ui := &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
+4 -4
View File
@@ -30,8 +30,8 @@ func (b *build) Name() (result string) {
return
}
func (b *build) Prepare() (err error) {
if cerr := b.client.Call("Build.Prepare", new(interface{}), &err); cerr != nil {
func (b *build) Prepare(v map[string]string) (err error) {
if cerr := b.client.Call("Build.Prepare", v, &err); cerr != nil {
return cerr
}
@@ -86,8 +86,8 @@ func (b *BuildServer) Name(args *interface{}, reply *string) error {
return nil
}
func (b *BuildServer) Prepare(args interface{}, reply *error) error {
*reply = b.build.Prepare()
func (b *BuildServer) Prepare(v map[string]string, reply *error) error {
*reply = b.build.Prepare(v)
return nil
}
+11 -2
View File
@@ -13,6 +13,7 @@ var testBuildArtifact = &testArtifact{}
type testBuild struct {
nameCalled bool
prepareCalled bool
prepareVars map[string]string
runCalled bool
runCache packer.Cache
runUi packer.Ui
@@ -28,8 +29,9 @@ func (b *testBuild) Name() string {
return "name"
}
func (b *testBuild) Prepare() error {
func (b *testBuild) Prepare(v map[string]string) error {
b.prepareCalled = true
b.prepareVars = v
return nil
}
@@ -78,8 +80,15 @@ func TestBuildRPC(t *testing.T) {
assert.True(b.nameCalled, "name should be called")
// Test Prepare
bClient.Prepare()
bClient.Prepare(map[string]string{"foo": "bar"})
assert.True(b.prepareCalled, "prepare should be called")
if len(b.prepareVars) != 1 {
t.Fatalf("bad vars: %#v", b.prepareVars)
}
if b.prepareVars["foo"] != "bar" {
t.Fatalf("bad vars: %#v", b.prepareVars)
}
// Test Run
cache := new(testCache)
+1
View File
@@ -4,6 +4,7 @@ import "encoding/gob"
func init() {
gob.Register(new(map[string]interface{}))
gob.Register(new(map[string]string))
gob.Register(make([]interface{}, 0))
gob.Register(new(BasicError))
}
+24
View File
@@ -17,6 +17,12 @@ type UiServer struct {
ui packer.Ui
}
// The arguments sent to Ui.Machine
type UiMachineArgs struct {
Category string
Args []string
}
func (u *Ui) Ask(query string) (result string, err error) {
err = u.client.Call("Ui.Ask", query, &result)
return
@@ -28,6 +34,17 @@ func (u *Ui) Error(message string) {
}
}
func (u *Ui) Machine(t string, args ...string) {
rpcArgs := &UiMachineArgs{
Category: t,
Args: args,
}
if err := u.client.Call("Ui.Machine", rpcArgs, new(interface{})); err != nil {
panic(err)
}
}
func (u *Ui) Message(message string) {
if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil {
panic(err)
@@ -52,6 +69,13 @@ func (u *UiServer) Error(message *string, reply *interface{}) error {
return nil
}
func (u *UiServer) Machine(args *UiMachineArgs, reply *interface{}) error {
u.ui.Machine(args.Category, args.Args...)
*reply = nil
return nil
}
func (u *UiServer) Message(message *string, reply *interface{}) error {
u.ui.Message(*message)
*reply = nil
+24
View File
@@ -3,6 +3,7 @@ package rpc
import (
"cgl.tideland.biz/asserts"
"net/rpc"
"reflect"
"testing"
)
@@ -11,6 +12,9 @@ type testUi struct {
askQuery string
errorCalled bool
errorMessage string
machineCalled bool
machineType string
machineArgs []string
messageCalled bool
messageMessage string
sayCalled bool
@@ -28,6 +32,12 @@ func (u *testUi) Error(message string) {
u.errorMessage = message
}
func (u *testUi) Machine(t string, args ...string) {
u.machineCalled = true
u.machineType = t
u.machineArgs = args
}
func (u *testUi) Message(message string) {
u.messageCalled = true
u.messageMessage = message
@@ -72,4 +82,18 @@ func TestUiRPC(t *testing.T) {
uiClient.Say("message")
assert.Equal(ui.sayMessage, "message", "message should be correct")
uiClient.Machine("foo", "bar", "baz")
if !ui.machineCalled {
t.Fatal("machine should be called")
}
if ui.machineType != "foo" {
t.Fatalf("bad type: %#v", ui.machineType)
}
expected := []string{"bar", "baz"}
if !reflect.DeepEqual(ui.machineArgs, expected) {
t.Fatalf("bad: %#v", ui.machineArgs)
}
}
+62 -48
View File
@@ -2,9 +2,12 @@ package packer
import (
"bytes"
"encoding/json"
"fmt"
"github.com/mitchellh/mapstructure"
jsonutil "github.com/mitchellh/packer/common/json"
"io"
"io/ioutil"
"os"
"sort"
)
@@ -13,6 +16,7 @@ import (
// "interface{}" pointers since we actually don't know what their contents
// are until we read the "type" field.
type rawTemplate struct {
Variables map[string]string
Builders []map[string]interface{}
Hooks map[string][]string
Provisioners []map[string]interface{}
@@ -22,40 +26,41 @@ type rawTemplate struct {
// The Template struct represents a parsed template, parsed into the most
// completed form it can be without additional processing by the caller.
type Template struct {
Builders map[string]rawBuilderConfig
Variables map[string]string
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 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
@@ -65,31 +70,8 @@ type rawProvisionerConfig struct {
// way.
func ParseTemplate(data []byte) (t *Template, err error) {
var rawTplInterface interface{}
err = json.Unmarshal(data, &rawTplInterface)
err = jsonutil.Unmarshal(data, &rawTplInterface)
if err != nil {
syntaxErr, ok := err.(*json.SyntaxError)
if !ok {
return
}
// We have a syntax error. Extract out the line number and friends.
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
newline := []byte{'\x0a'}
// Calculate the start/end position of the line where the error is
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
end := len(data)
if idx := bytes.Index(data[start:], newline); idx >= 0 {
end = start + idx
}
// Count the line number we're on plus the offset in the line
line := bytes.Count(data[:start], newline) + 1
pos := int(syntaxErr.Offset) - start - 1
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
line, pos, syntaxErr, data[start:end])
return
}
@@ -123,14 +105,20 @@ func ParseTemplate(data []byte) (t *Template, err error) {
}
t = &Template{}
t.Builders = make(map[string]rawBuilderConfig)
t.Variables = make(map[string]string)
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 {
t.Variables[k] = v
}
// 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 {
@@ -165,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
}
@@ -180,7 +168,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
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]
@@ -201,7 +189,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
continue
}
config.rawConfig = pp
config.RawConfig = pp
}
}
@@ -230,7 +218,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
// actively reject them as invalid configuration.
delete(v, "override")
raw.rawConfig = v
raw.RawConfig = v
}
if len(t.Builders) == 0 {
@@ -247,6 +235,31 @@ func ParseTemplate(data []byte) (t *Template, err error) {
return
}
// ParseTemplateFile takes the given template file and parses it into
// a single template.
func ParseTemplateFile(path string) (*Template, error) {
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) {
switch v := rawV.(type) {
case string:
@@ -369,7 +382,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,
}
}
@@ -392,7 +405,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 {
@@ -407,11 +420,12 @@ 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,
provisioners: provisioners,
variables: t.Variables,
}
return

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