Compare commits

...

160 Commits

Author SHA1 Message Date
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
124 changed files with 3280 additions and 747 deletions
+69
View File
@@ -1,3 +1,72 @@
## 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:
+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 *common.Template
}
type Builder struct {
@@ -47,6 +48,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
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/common"
"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 *common.Template) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
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/common"
)
// 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 *common.Template) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
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")
}
}
+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/common"
"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 *common.Template) []error {
if t == nil {
var err error
t, err = common.NewTemplate()
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,55 @@
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
}
options := &ec2.ModifyImageAttribute{
Description: s.Description,
AddUsers: s.Users,
AddGroups: s.Groups,
ProductCodes: s.ProductCodes,
}
ui.Say("Modifying AMI attributes...")
_, err := ec2conn.ModifyImageAttribute(ami, options)
if err != nil {
err := fmt.Errorf("Error modify AMI attributes: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepModifyAMIAttributes) Cleanup(state map[string]interface{}) {
// No cleanup...
}
@@ -5,12 +5,15 @@ 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
@@ -24,10 +27,22 @@ 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}},
+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))
}
+43 -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,13 @@ const BuilderId = "mitchellh.amazonebs"
type config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `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 *common.Template
}
type Builder struct {
@@ -40,23 +41,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 = common.NewTemplate()
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 +114,26 @@ 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,
},
&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!
+2 -20
View File
@@ -1,44 +1,26 @@
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,
Name: config.AMIName,
}
createResp, err := ec2conn.CreateImage(createOpts)
+57 -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,21 @@ const BuilderId = "mitchellh.amazon.instance"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `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 *common.Template
}
type Builder struct {
@@ -49,12 +51,18 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
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 +94,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 +136,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 +193,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&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 +208,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)
+1 -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,10 @@ 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,
}
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"`
}
+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 *common.Template
}
type Builder struct {
@@ -63,6 +57,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
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 {
+64 -1
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"
@@ -57,6 +57,7 @@ type config struct {
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
tpl *common.Template
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -65,6 +66,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
@@ -124,6 +131,53 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
// Errors
templates := map[string]*string{
"guest_additions_path": &b.config.GuestAdditionsPath,
"guest_additions_url": &b.config.GuestAdditionsURL,
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
"guest_os_type": &b.config.GuestOSType,
"http_directory": &b.config.HTTPDir,
"iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType,
"iso_url": &b.config.ISOUrl,
"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, 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"))
@@ -215,6 +269,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %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))
}
}
}
b.driver, err = b.newDriver()
if err != nil {
errs = packer.MultiErrorAppend(
@@ -6,7 +6,7 @@ import (
"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"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
+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, " ")))
+74 -1
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"
@@ -58,6 +58,7 @@ type config struct {
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
tpl *common.Template
}
func (b *Builder) Prepare(raws ...interface{}) error {
@@ -66,6 +67,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
return err
}
b.config.tpl, err = common.NewTemplate()
if err != nil {
return err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
@@ -121,6 +128,72 @@ 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.ISOUrl,
"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, 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"))
+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)
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
+8 -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,15 @@ 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())
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)
}
}
+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"
)
@@ -73,6 +74,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
+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"`
}
+81
View File
@@ -0,0 +1,81 @@
package common
import (
"bytes"
"fmt"
"strconv"
"text/template"
"time"
)
// Template processes string data as a text/template with some common
// elements and functions available. Plugin creators should process as
// many fields as possible through this.
type Template struct {
UserVars map[string]string
root *template.Template
i int
}
// NewTemplate creates a new template processor.
func NewTemplate() (*Template, error) {
result := &Template{
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 *Template) 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 *Template) Validate(s string) error {
root, err := t.root.Clone()
if err != nil {
return err
}
_, err = root.New("template").Parse(s)
return err
}
func (t *Template) 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 *Template) 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 common
import (
"math"
"strconv"
"testing"
"time"
)
func TestTemplateProcess_timestamp(t *testing.T) {
tpl, err := NewTemplate()
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 TestTemplateProcess_user(t *testing.T) {
tpl, err := NewTemplate()
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 TestTemplateValidate(t *testing.T) {
tpl, err := NewTemplate()
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")
}
}
+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) {
+30 -1
View File
@@ -76,8 +76,13 @@ func main() {
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,18 +91,25 @@ 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)
plugin.CleanupClients()
os.Exit(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())
plugin.CleanupClients()
os.Exit(1)
}
@@ -105,6 +117,23 @@ func main() {
os.Exit(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) {
var config config
if err := decodeConfig(bytes.NewBufferString(defaultConfig), &config); err != nil {
+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,
}
+7 -1
View File
@@ -73,7 +73,7 @@ type EnvironmentConfig struct {
func DefaultEnvironmentConfig() *EnvironmentConfig {
config := &EnvironmentConfig{}
config.Commands = make([]string, 0)
config.Ui = &ReaderWriterUi{
config.Ui = &BasicUi{
Reader: os.Stdin,
Writer: os.Stdout,
}
@@ -293,12 +293,18 @@ func (e *coreEnvironment) printHelp() {
synopsis = command.Synopsis()
}
// Machine-readable output of the available command
e.ui.Machine("command", key, synopsis)
// Pad the key with spaces so that they're all the same width
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
// Output the command and the synopsis
e.ui.Say(fmt.Sprintf(" %v %v", key, synopsis))
}
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
+5 -5
View File
@@ -19,7 +19,7 @@ func init() {
func testEnvironment() Environment {
config := DefaultEnvironmentConfig()
config.Ui = &ReaderWriterUi{
config.Ui = &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
@@ -45,8 +45,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 +175,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 +341,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)
}
}
+23 -26
View File
@@ -1,10 +1,10 @@
package packer
import (
"bytes"
"encoding/json"
"fmt"
"github.com/mitchellh/mapstructure"
jsonutil "github.com/mitchellh/packer/common/json"
"io/ioutil"
"sort"
)
@@ -13,6 +13,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,6 +23,7 @@ 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 {
Variables map[string]string
Builders map[string]rawBuilderConfig
Hooks map[string][]string
PostProcessors [][]rawPostProcessorConfig
@@ -65,31 +67,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,11 +102,17 @@ func ParseTemplate(data []byte) (t *Template, err error) {
}
t = &Template{}
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))
// 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
@@ -247,6 +232,17 @@ 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) {
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:
@@ -412,6 +408,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
hooks: hooks,
postProcessors: postProcessors,
provisioners: provisioners,
variables: t.Variables,
}
return
+94
View File
@@ -2,11 +2,47 @@ package packer
import (
"cgl.tideland.biz/asserts"
"io/ioutil"
"reflect"
"sort"
"testing"
)
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 TestParseTemplateFile_basic(t *testing.T) {
data := `
{
"builders": [{"type": "something"}]
}
`
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Write([]byte(data))
tf.Close()
result, err := ParseTemplateFile(tf.Name())
if err != nil {
t.Fatalf("err: %s", err)
}
if len(result.Builders) != 1 {
t.Fatalf("bad: %#v", result.Builders)
}
}
func TestParseTemplate_Basic(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
@@ -300,6 +336,28 @@ func TestParseTemplate_Provisioners(t *testing.T) {
assert.NotNil(result.Provisioners[0].rawConfig, "should have raw config")
}
func TestParseTemplate_Variables(t *testing.T) {
data := `
{
"variables": {
"foo": "bar",
"bar": ""
},
"builders": [{"type": "something"}]
}
`
result, err := ParseTemplate([]byte(data))
if err != nil {
t.Fatalf("err: %s", err)
}
if result.Variables == nil || len(result.Variables) != 2 {
t.Fatalf("bad vars: %#v", result.Variables)
}
}
func TestTemplate_BuildNames(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
@@ -613,3 +671,39 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
assert.Equal(len(coreBuild.provisioners), 1, "should have one provisioner")
assert.Equal(len(coreBuild.provisioners[0].config), 2, "should have two configs on the provisioner")
}
func TestTemplateBuild_variables(t *testing.T) {
data := `
{
"variables": {
"foo": "bar"
},
"builders": [
{
"name": "test1",
"type": "test-builder"
}
]
}
`
template, err := ParseTemplate([]byte(data))
if err != nil {
t.Fatalf("err: %s", err)
}
build, err := template.Build("test1", testComponentFinder())
if err != nil {
t.Fatalf("err: %s", err)
}
coreBuild, ok := build.(*coreBuild)
if !ok {
t.Fatalf("couldn't convert!")
}
if len(coreBuild.variables) != 1 {
t.Fatalf("bad vars: %#v", coreBuild.variables)
}
}
+94 -23
View File
@@ -11,6 +11,7 @@ import (
"runtime"
"strings"
"sync"
"time"
"unicode"
)
@@ -33,6 +34,7 @@ type Ui interface {
Say(string)
Message(string)
Error(string)
Machine(string, ...string)
}
// ColoredUi is a UI that is colored using terminal colors.
@@ -42,23 +44,32 @@ type ColoredUi struct {
Ui Ui
}
// PrefixedUi is a UI that wraps another UI implementation and adds a
// prefix to all the messages going out.
type PrefixedUi struct {
SayPrefix string
MessagePrefix string
Ui Ui
// TargettedUi is a UI that wraps another UI implementation and modifies
// the output to indicate a specific target. Specifically, all Say output
// is prefixed with the target name. Message output is not prefixed but
// is offset by the length of the target so that output is lined up properly
// with Say output. Machine-readable output has the proper target set.
type TargettedUi struct {
Target string
Ui Ui
}
// The ReaderWriterUi is a UI that writes and reads from standard Go
// io.Reader and io.Writer.
type ReaderWriterUi struct {
// The BasicUI is a UI that reads and writes from a standard Go reader
// and writer. It is safe to be called from multiple goroutines. Machine
// readable output is simply logged for this UI.
type BasicUi struct {
Reader io.Reader
Writer io.Writer
l sync.Mutex
interrupted bool
}
// MachineReadableUi is a UI that only outputs machine-readable output
// to the given Writer.
type MachineReadableUi struct {
Writer io.Writer
}
func (u *ColoredUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.colorize(query, u.Color, true))
}
@@ -80,6 +91,11 @@ func (u *ColoredUi) Error(message string) {
u.Ui.Error(u.colorize(message, color, true))
}
func (u *ColoredUi) Machine(t string, args ...string) {
// Don't colorize machine-readable output
u.Ui.Machine(t, args...)
}
func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
if !u.supportsColors() {
return message
@@ -107,33 +123,43 @@ func (u *ColoredUi) supportsColors() bool {
return cygwin
}
func (u *PrefixedUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.prefixLines(u.SayPrefix, query))
func (u *TargettedUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.prefixLines(true, query))
}
func (u *PrefixedUi) Say(message string) {
u.Ui.Say(u.prefixLines(u.SayPrefix, message))
func (u *TargettedUi) Say(message string) {
u.Ui.Say(u.prefixLines(true, message))
}
func (u *PrefixedUi) Message(message string) {
u.Ui.Message(u.prefixLines(u.MessagePrefix, message))
func (u *TargettedUi) Message(message string) {
u.Ui.Message(u.prefixLines(false, message))
}
func (u *PrefixedUi) Error(message string) {
u.Ui.Error(u.prefixLines(u.SayPrefix, message))
func (u *TargettedUi) Error(message string) {
u.Ui.Error(u.prefixLines(true, message))
}
func (u *PrefixedUi) prefixLines(prefix, message string) string {
func (u *TargettedUi) Machine(t string, args ...string) {
// Prefix in the target, then pass through
u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
}
func (u *TargettedUi) prefixLines(arrow bool, message string) string {
arrowText := "==>"
if !arrow {
arrowText = strings.Repeat(" ", len(arrowText))
}
var result bytes.Buffer
for _, line := range strings.Split(message, "\n") {
result.WriteString(fmt.Sprintf("%s: %s\n", prefix, line))
result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
}
return strings.TrimRightFunc(result.String(), unicode.IsSpace)
}
func (rw *ReaderWriterUi) Ask(query string) (string, error) {
func (rw *BasicUi) Ask(query string) (string, error) {
rw.l.Lock()
defer rw.l.Unlock()
@@ -177,7 +203,7 @@ func (rw *ReaderWriterUi) Ask(query string) (string, error) {
}
}
func (rw *ReaderWriterUi) Say(message string) {
func (rw *BasicUi) Say(message string) {
rw.l.Lock()
defer rw.l.Unlock()
@@ -188,7 +214,7 @@ func (rw *ReaderWriterUi) Say(message string) {
}
}
func (rw *ReaderWriterUi) Message(message string) {
func (rw *BasicUi) Message(message string) {
rw.l.Lock()
defer rw.l.Unlock()
@@ -199,7 +225,7 @@ func (rw *ReaderWriterUi) Message(message string) {
}
}
func (rw *ReaderWriterUi) Error(message string) {
func (rw *BasicUi) Error(message string) {
rw.l.Lock()
defer rw.l.Unlock()
@@ -209,3 +235,48 @@ func (rw *ReaderWriterUi) Error(message string) {
panic(err)
}
}
func (rw *BasicUi) Machine(t string, args ...string) {
log.Printf("machine readable: %s %#v", t, args)
}
func (u *MachineReadableUi) Ask(query string) (string, error) {
return "", errors.New("machine-readable UI can't ask")
}
func (u *MachineReadableUi) Say(message string) {
u.Machine("ui", "say", message)
}
func (u *MachineReadableUi) Message(message string) {
u.Machine("ui", "message", message)
}
func (u *MachineReadableUi) Error(message string) {
u.Machine("ui", "error", message)
}
func (u *MachineReadableUi) Machine(category string, args ...string) {
now := time.Now().UTC()
// Determine if we have a target, and set it
target := ""
commaIdx := strings.Index(category, ",")
if commaIdx > -1 {
target = category[0:commaIdx]
category = category[commaIdx+1:]
}
// Prepare the args
for i, v := range args {
args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1)
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
}
argsString := strings.Join(args, ",")
_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
if err != nil {
panic(err)
}
}
+75 -21
View File
@@ -3,11 +3,12 @@ package packer
import (
"bytes"
"cgl.tideland.biz/asserts"
"strings"
"testing"
)
func testUi() *ReaderWriterUi {
return &ReaderWriterUi{
func testUi() *BasicUi {
return &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
@@ -36,23 +37,26 @@ func TestColoredUi(t *testing.T) {
}
}
func TestPrefixedUi(t *testing.T) {
func TestTargettedUi(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
bufferUi := testUi()
prefixUi := &PrefixedUi{"mitchell", "bar", bufferUi}
targettedUi := &TargettedUi{
Target: "foo",
Ui: bufferUi,
}
prefixUi.Say("foo")
assert.Equal(readWriter(bufferUi), "mitchell: foo\n", "should have prefix")
targettedUi.Say("foo")
assert.Equal(readWriter(bufferUi), "==> foo: foo\n", "should have prefix")
prefixUi.Message("foo")
assert.Equal(readWriter(bufferUi), "bar: foo\n", "should have prefix")
targettedUi.Message("foo")
assert.Equal(readWriter(bufferUi), " foo: foo\n", "should have prefix")
prefixUi.Error("bar")
assert.Equal(readWriter(bufferUi), "mitchell: bar\n", "should have prefix")
targettedUi.Error("bar")
assert.Equal(readWriter(bufferUi), "==> foo: bar\n", "should have prefix")
prefixUi.Say("foo\nbar")
assert.Equal(readWriter(bufferUi), "mitchell: foo\nmitchell: bar\n", "should multiline")
targettedUi.Say("foo\nbar")
assert.Equal(readWriter(bufferUi), "==> foo: foo\n==> foo: bar\n", "should multiline")
}
func TestColoredUi_ImplUi(t *testing.T) {
@@ -63,23 +67,23 @@ func TestColoredUi_ImplUi(t *testing.T) {
}
}
func TestPrefixedUi_ImplUi(t *testing.T) {
func TestTargettedUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &PrefixedUi{}
raw = &TargettedUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("PrefixedUi must implement Ui")
t.Fatalf("TargettedUi must implement Ui")
}
}
func TestReaderWriterUi_ImplUi(t *testing.T) {
func TestBasicUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &ReaderWriterUi{}
raw = &BasicUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("ReaderWriterUi must implement Ui")
t.Fatalf("BasicUi must implement Ui")
}
}
func TestReaderWriterUi_Error(t *testing.T) {
func TestBasicUi_Error(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
bufferUi := testUi()
@@ -91,7 +95,7 @@ func TestReaderWriterUi_Error(t *testing.T) {
assert.Equal(readWriter(bufferUi), "5\n", "formatting")
}
func TestReaderWriterUi_Say(t *testing.T) {
func TestBasicUi_Say(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
bufferUi := testUi()
@@ -103,9 +107,59 @@ func TestReaderWriterUi_Say(t *testing.T) {
assert.Equal(readWriter(bufferUi), "5\n", "formatting")
}
func TestMachineReadableUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &MachineReadableUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("MachineReadableUi must implement Ui")
}
}
func TestMachineReadableUi(t *testing.T) {
var data, expected string
buf := new(bytes.Buffer)
ui := &MachineReadableUi{Writer: buf}
// No target
ui.Machine("foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Target
buf.Reset()
ui.Machine("mitchellh,foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = "mitchellh,foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Commas
buf.Reset()
ui.Machine("foo", "foo,bar")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo%!(PACKER_COMMA)bar\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// New lines
buf.Reset()
ui.Machine("foo", "foo\n")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo\\n\n"
if data != expected {
t.Fatalf("bad: %#v", data)
}
}
// This reads the output from the bytes.Buffer in our test object
// and then resets the buffer.
func readWriter(ui *ReaderWriterUi) (result string) {
func readWriter(ui *BasicUi) (result string) {
buffer := ui.Writer.(*bytes.Buffer)
result = buffer.String()
buffer.Reset()
+5 -1
View File
@@ -10,7 +10,7 @@ import (
var GitCommit string
// The version of packer.
const Version = "0.2.2"
const Version = "0.3.1"
// Any pre-release marker for the version. If this is "" (empty string),
// then it means that it is a final release. Otherwise, this is the
@@ -27,6 +27,10 @@ command-line flags for this command.`
}
func (versionCommand) Run(env Environment, args []string) int {
env.Ui().Machine("version", Version)
env.Ui().Machine("version-prelease", VersionPrerelease)
env.Ui().Machine("version-commit", GitCommit)
var versionString bytes.Buffer
fmt.Fprintf(&versionString, "Packer v%s", Version)
if VersionPrerelease != "" {
+34
View File
@@ -1 +1,35 @@
package main
import (
"reflect"
"testing"
)
func TestExtractMachineReadable(t *testing.T) {
var args, expected, result []string
var mr bool
// Not
args = []string{"foo", "bar", "baz"}
result, mr = extractMachineReadable(args)
expected = []string{"foo", "bar", "baz"}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
if mr {
t.Fatal("should not be mr")
}
// Yes
args = []string{"foo", "-machine-readable", "baz"}
result, mr = extractMachineReadable(args)
expected = []string{"foo", "baz"}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
if !mr {
t.Fatal("should be mr")
}
}
+27 -28
View File
@@ -3,19 +3,21 @@ package file
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"sort"
"strings"
)
type config struct {
common.PackerConfig `mapstructure:",squash"`
// The local path of the file to upload.
Source string
// The remote path where the local file will be uploaded to.
Destination string
tpl *common.Template
}
type Provisioner struct {
@@ -23,49 +25,46 @@ type Provisioner struct {
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &p.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
p.config.tpl, err = common.NewTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Accumulate any errors
errs := make([]error, 0)
errs := common.CheckUnusedConfig(md)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
templates := map[string]*string{
"source": &p.config.Source,
"destination": &p.config.Destination,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if _, err := os.Stat(p.config.Source); err != nil {
errs = append(errs,
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad source '%s': %s", p.config.Source, err))
}
if p.config.Destination == "" {
errs = append(errs, errors.New("Destination must be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Destination must be specified."))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
+3
View File
@@ -106,6 +106,9 @@ func (su *stubUi) Ask(string) (string, error) {
func (su *stubUi) Error(string) {
}
func (su *stubUi) Machine(string, ...string) {
}
func (su *stubUi) Message(string) {
}
+47 -16
View File
@@ -5,7 +5,7 @@ package saltmasterless
import (
"errors"
"fmt"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"path/filepath"
@@ -15,6 +15,8 @@ import (
const DefaultTempConfigDir = "/tmp/salt"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// If true, run the salt-bootstrap script
SkipBootstrap bool `mapstructure:"skip_bootstrap"`
BootstrapArgs string `mapstructure:"bootstrap_args"`
@@ -24,6 +26,8 @@ type Config struct {
// Where files will be copied before moving to the /srv/salt directory
TempConfigDir string `mapstructure:"temp_config_dir"`
tpl *common.Template
}
type Provisioner struct {
@@ -36,6 +40,12 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
return err
}
p.config.tpl, err = common.NewTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
if p.config.TempConfigDir == "" {
p.config.TempConfigDir = DefaultTempConfigDir
}
@@ -43,12 +53,26 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
if p.config.LocalStateTree == "" {
errs = packer.MultiErrorAppend(errs,
errors.New("Please specify a local_state_tree"))
} else if _, err := os.Stat(p.config.LocalStateTree); err != nil {
errs = packer.MultiErrorAppend(errs,
errors.New("local_state_tree must exist and be accessible"))
templates := map[string]*string{
"bootstrap_args": &p.config.BootstrapArgs,
"local_state_tree": &p.config.LocalStateTree,
"temp_config_dir": &p.config.TempConfigDir,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if p.config.LocalStateTree != "" {
if _, err := os.Stat(p.config.LocalStateTree); err != nil {
errs = packer.MultiErrorAppend(errs,
errors.New("local_state_tree must exist and be accessible"))
}
}
if errs != nil && len(errs.Errors) > 0 {
@@ -74,7 +98,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
ui.Message(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir))
cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil {
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Error creating remote salt state directory: %s", err)
}
@@ -85,20 +113,22 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
ui.Message(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir))
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)}
if err = cmd.StartWithUi(comm, ui); err != nil {
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err)
}
ui.Message("Running highstate")
cmd = &packer.RemoteCmd{Command: "sudo salt-call --local state.highstate -l info"}
if err = cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Error executing highstate: %s", err)
}
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
}
ui.Message("Removing /srv/salt")
cmd = &packer.RemoteCmd{Command: "sudo rm -r /srv/salt"}
if err = cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Unable to remove /srv/salt: %d", err)
return fmt.Errorf("Error executing highstate: %s", err)
}
return nil
@@ -107,6 +137,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator, ui packer.Ui) (err error) {
visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
localRelPath := strings.Replace(localPath, localDir, "", 1)
localRelPath = strings.Replace(localRelPath, "\\", "/", -1)
remotePath := fmt.Sprintf("%s%s", remoteDir, localRelPath)
if f.IsDir() && f.Name() == ".git" {
return filepath.SkipDir
+100 -45
View File
@@ -4,22 +4,22 @@ package shell
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"text/template"
"time"
)
const DefaultRemotePath = "/tmp/script.sh"
type config struct {
common.PackerConfig `mapstructure:",squash"`
// An inline script to execute. Multiple strings are all executed
// in the context of a single shell.
Inline []string
@@ -46,9 +46,13 @@ type config struct {
// can be used to inject the environment_vars into the environment.
ExecuteCommand string `mapstructure:"execute_command"`
// Packer configurations, these come from Packer itself
PackerBuildName string `mapstructure:"packer_build_name"`
PackerBuilderType string `mapstructure:"packer_builder_type"`
// The timeout for retrying to start the process. Until this timeout
// is reached, if the provisioner can't start a process, it retries.
// This can be set high to allow for reboots.
RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
startRetryTimeout time.Duration
tpl *common.Template
}
type Provisioner struct {
@@ -61,37 +65,19 @@ type ExecuteCommandTemplate struct {
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &p.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
p.config.tpl, err = common.NewTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
@@ -105,6 +91,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
p.config.InlineShebang = "/bin/sh"
}
if p.config.RawStartRetryTimeout == "" {
p.config.RawStartRetryTimeout = "5m"
}
if p.config.RemotePath == "" {
p.config.RemotePath = DefaultRemotePath
}
@@ -118,22 +108,59 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
if p.config.Script != "" && len(p.config.Scripts) > 0 {
errs = append(errs, errors.New("Only one of script or scripts can be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Only one of script or scripts can be specified."))
}
if p.config.Script != "" {
p.config.Scripts = []string{p.config.Script}
}
templates := map[string]*string{
"inline_shebang": &p.config.InlineShebang,
"script": &p.config.Script,
"start_retry_timeout": &p.config.RawStartRetryTimeout,
"remote_path": &p.config.RemotePath,
}
for n, ptr := range templates {
var err error
*ptr, err = p.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
sliceTemplates := map[string][]string{
"inline": p.config.Inline,
"scripts": p.config.Scripts,
"environment_vars": p.config.Vars,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = p.config.tpl.Process(elem, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
if len(p.config.Scripts) == 0 && p.config.Inline == nil {
errs = append(errs, errors.New("Either a script file or inline script must be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Either a script file or inline script must be specified."))
} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
errs = append(errs, errors.New("Only a script file or an inline script can be specified, not both."))
errs = packer.MultiErrorAppend(errs,
errors.New("Only a script file or an inline script can be specified, not both."))
}
for _, path := range p.config.Scripts {
if _, err := os.Stat(path); err != nil {
errs = append(errs, fmt.Errorf("Bad script '%s': %s", path, err))
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad script '%s': %s", path, err))
}
}
@@ -141,14 +168,21 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
for _, kv := range p.config.Vars {
vs := strings.Split(kv, "=")
if len(vs) != 2 || vs[0] == "" {
errs = append(
errs,
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if p.config.RawStartRetryTimeout != "" {
p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -215,14 +249,35 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
flattendVars := strings.Join(envVars, " ")
// Compile the command
var command bytes.Buffer
t := template.Must(template.New("command").Parse(p.config.ExecuteCommand))
t.Execute(&command, &ExecuteCommandTemplate{flattendVars, p.config.RemotePath})
command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{
Vars: flattendVars,
Path: p.config.RemotePath,
})
if err != nil {
return fmt.Errorf("Error processing command: %s", err)
}
cmd := &packer.RemoteCmd{Command: command.String()}
cmd := &packer.RemoteCmd{Command: command}
startTimeout := time.After(p.config.startRetryTimeout)
log.Printf("Executing command: %s", cmd.Command)
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Failed executing command: %s", err)
for {
if err := cmd.StartWithUi(comm, ui); err == nil {
break
}
// Create an error and log it
err = fmt.Errorf("Error executing command: %s", err)
log.Printf(err.Error())
// Check if we timed out, otherwise we retry. It is safe to
// retry since the only error case above is if the command
// failed to START.
select {
case <-startTimeout:
return err
default:
time.Sleep(2 * time.Second)
}
}
if cmd.ExitStatus != 0 {
+1 -1
View File
@@ -17,7 +17,7 @@ DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )"
cd $DIR
# Get the git commit
GIT_COMMIT=$(git rev-parse --short HEAD)
GIT_COMMIT=$(git rev-parse HEAD)
GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
# Compile the main Packer app
@@ -48,5 +48,18 @@ list as contributors come and go.
</div>
</div>
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/2f7fc9cb7558e3ea48f5a86fa90a78da.png?s=125">
<div class="bio">
<h3>Mark Peek (<a href="https://github.com/markpeek">@markpeek</a>)</h3>
<p>
In addition to Packer, Mark Peek helps maintain
various open source projects such as
<a href="https://github.com/cloudtools">cloudtools</a> and
<a href="https://github.com/ironport">IronPort Python libraries</a>.
Mark is also a <a href="https://FreeBSD.org">FreeBSD committer</a>.</p>
</div>
</div>
<div class="clearfix"></div>
</div>
@@ -62,8 +62,8 @@ Required:
* `ami_name` (string) - The name of the resulting AMI that will appear
when managing AMIs in the AWS console or via APIs. This must be unique.
To help make this unique, certain template parameters are available for
this value, which are documented below.
To help make this unique, use a function like `timestamp` (see
[configuration templates](/docs/templates/configuration-templates.html) for more info)
* `secret_key` (string) - The secret key used to communicate with AWS.
If not specified, Packer will attempt to read this from environmental
@@ -78,6 +78,21 @@ Required:
Optional:
* `ami_description` (string) - The description to set for the resulting
AMI(s). By default this description is empty.
* `ami_groups` (array of string) - A list of groups that have access
to launch the resulting AMI(s). By default no groups have permission
to launch the AMI.
* `ami_product_codes` (array of string) - A list of product codes to
associate with the AMI. By default no product codes are associated with
the AMI.
* `ami_users` (array of string) - A list of account IDs that have access
to launch the resulting AMI(s). By default no additional users other than the user
creating the AMI has permissions to launch it.
* `chroot_mounts` (list of list of strings) - This is a list of additional
devices to mount into the chroot environment. This configuration parameter
requires some additional documentation which is in the "Chroot Mounts" section
@@ -115,22 +130,10 @@ Here is a basic example. It is completely valid except for the access keys:
"access_key": "YOUR KEY HERE",
"secret_key": "YOUR SECRET KEY HERE",
"source_ami": "ami-e81d5881",
"ami_name": "packer-amazon-chroot {{.CreateTime}}"
"ami_name": "packer-amazon-chroot {{timestamp}}"
}
</pre>
## AMI Name Variables
The AMI name specified by the `ami_name` configuration variable is actually
treated as a [configuration template](/docs/templates/configuration-templates.html).
Packer provides a set of variables that it will replace
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
The available variables are shown below:
* `CreateTime` - This will be replaced with the Unix timestamp of when
the AMI was built.
## Chroot Mounts
The `chroot_mounts` configuration can be used to mount additional devices
@@ -37,8 +37,8 @@ Required:
* `ami_name` (string) - The name of the resulting AMI that will appear
when managing AMIs in the AWS console or via APIs. This must be unique.
To help make this unique, certain template parameters are available for
this value, which are documented below.
To help make this unique, use a function like `timestamp` (see
[configuration templates](/docs/templates/configuration-templates.html) for more info)
* `instance_type` (string) - The EC2 instance type to use while building
the AMI, such as "m1.small".
@@ -58,6 +58,21 @@ Required:
Optional:
* `ami_description` (string) - The description to set for the resulting
AMI(s). By default this description is empty.
* `ami_groups` (array of string) - A list of groups that have access
to launch the resulting AMI(s). By default no groups have permission
to launch the AMI.
* `ami_product_codes` (array of string) - A list of product codes to
associate with the AMI. By default no product codes are associated with
the AMI.
* `ami_users` (array of string) - A list of account IDs that have access
to launch the resulting AMI(s). By default no additional users other than the user
creating the AMI has permissions to launch it.
* `iam_instance_profile` (string) - The name of an
[IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html)
to launch the EC2 instance with.
@@ -78,6 +93,16 @@ Optional:
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance.
* `tags` (object of key/value strings) - Tags applied to the AMI.
* `user_data` (string) - User data to apply when launching the instance.
Note that you need to be careful about escaping characters due to the
templates being JSON. It is often more convenient to use `user_data_file`,
instead.
* `user_data_file` (string) - Path to a file that will be used for the
user data when launching the instance.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC.
@@ -94,7 +119,7 @@ Here is a basic example. It is completely valid except for the access keys:
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-quick-start {{.CreateTime}}"
"ami_name": "packer-quick-start {{timestamp}}",
}
</pre>
@@ -105,14 +130,24 @@ the section above for more information on what environmental variables Packer
will look for.
</div>
## AMI Name Variables
## Tag Example
The AMI name specified by the `ami_name` configuration variable is actually
treated as a [configuration template](/docs/templates/configuration-templates.html).
Packer provides a set of variables that it will replace
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
Here is an example using the optional AMI tags. This will add the tags
"OS_Version" and "Release" to the finished AMI.
The available variables are shown below:
* `CreateTime` - This will be replaced with the Unix timestamp of when
the AMI was built.
<pre class="prettyprint">
{
"type": "amazon-ebs",
"access_key": "YOUR KEY HERE",
"secret_key": "YOUR SECRET KEY HERE",
"region": "us-east-1",
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-quick-start {{timestamp}}",
"tags": {
"OS_Version": "Ubuntu",
"Release": "Latest"
}
}
</pre>

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