Compare commits

...

248 Commits

Author SHA1 Message Date
Mitchell Hashimoto 3dc96bd21f Up the version to 0.1.0 2013-06-28 10:33:16 -04:00
Mitchell Hashimoto 66fac2e82f builder/virtualbox: close the input source ISO 2013-06-28 10:30:33 -04:00
Mitchell Hashimoto 39c80262d4 Add an empty CHANGELOG 2013-06-28 10:21:45 -04:00
Mitchell Hashimoto b5dfad2c38 website: set base mac address example 2013-06-28 10:20:49 -04:00
Mitchell Hashimoto bc9a09e3fc post-processor/vagrant: provider-specific config works 2013-06-28 10:16:38 -04:00
Mitchell Hashimoto 21ae9a02d6 website: update the interfaces for extending 2013-06-28 09:49:21 -04:00
Mitchell Hashimoto 39aea8c5ee builder/virtualbox: check the proper file path 2013-06-28 09:45:30 -04:00
Mitchell Hashimoto 82bbbbbf32 fmt 2013-06-28 09:44:03 -04:00
Mitchell Hashimoto 358e71d98e builder/vmware: properly test file URLs 2013-06-28 09:43:49 -04:00
Mitchell Hashimoto 29af228a96 Upate the README 2013-06-28 09:36:01 -04:00
Jack Pearkes e8401a08a8 website: update jack's community bio 2013-06-28 14:46:57 +02:00
Mitchell Hashimoto e994ff605d website: vagrant boxes getting started 2013-06-28 08:37:13 -04:00
Mitchell Hashimoto 8dc8fcce77 Merge branch 'shell-env-vars'
Conflicts:
	website/source/docs/provisioners/shell.html.markdown
2013-06-28 08:20:12 -04:00
Mitchell Hashimoto 3a9b00fa4e website: clarify some things in the intro 2013-06-28 08:18:03 -04:00
Jack Pearkes 8d84d0cafa provisioner/shell: remove check for empty env vars config 2013-06-28 14:11:27 +02:00
Mitchell Hashimoto 244a0cc569 website: doc veewee-to-packer 2013-06-28 00:04:41 -04:00
Mitchell Hashimoto a5c2d6014f command/build: include the build name in the err/success output 2013-06-27 22:26:48 -04:00
Mitchell Hashimoto 13bff7e353 builder/virtualbox: return if cancelled, to avoid nil deref 2013-06-27 22:24:53 -04:00
Mitchell Hashimoto 9a318ceddc builder/virtualbox, vmware: delete output dir if cancelled 2013-06-27 22:23:40 -04:00
Mitchell Hashimoto 213cfb3dad builder/vmware: error if output directory exists 2013-06-27 22:15:24 -04:00
Mitchell Hashimoto 73a2e52a75 builder/virtualbox: Error if output directory already exists 2013-06-27 22:14:23 -04:00
Mitchell Hashimoto b308c942d5 packer: Builds use their own UI [GH-21] 2013-06-27 21:55:59 -04:00
Mitchell Hashimoto 6d87e6aa76 builder/amazonebs: Wait for instance to terminate during cleanup 2013-06-27 21:42:07 -04:00
Mitchell Hashimoto 5cd45b1278 post-processor: recognize vmware and build vmware boxes 2013-06-27 19:21:03 -04:00
Mitchell Hashimoto c5790de3db post-processor/vagrant: virtualbox output finds and sets up the mac addr 2013-06-27 19:05:47 -04:00
Mitchell Hashimoto 8d638aaa75 packer: Don't run post-processors if artifact is nil 2013-06-27 18:50:02 -04:00
Mitchell Hashimoto 0836c46e64 website: execute_command example 2013-06-27 18:23:47 -04:00
Mitchell Hashimoto da47d46c45 post-processor/vagrant: virtualbox box provider should be "virtualbox" 2013-06-27 17:21:15 -04:00
Mitchell Hashimoto bd9c54e820 post-processor/vagrant: Only tar files 2013-06-27 14:06:14 -07:00
Mitchell Hashimoto 0d0c403037 website: update provisioner docs "path" to "script" 2013-06-27 10:57:43 -07:00
Mitchell Hashimoto 09fabf1e22 provisioner/shell: rename "path" to "script" 2013-06-27 10:56:46 -07:00
Mitchell Hashimoto 7720be6d84 website: update vagrant PP docs for Artifactid var 2013-06-27 10:54:52 -07:00
Mitchell Hashimoto 2291d5ba79 post-processor/vagrant: validate the template 2013-06-27 10:53:43 -07:00
Mitchell Hashimoto ef1378462c post-processor/vagrant: compile the output path 2013-06-27 10:51:13 -07:00
Mitchell Hashimoto 9c37cf8ef1 website: make a note about deps in deving plugins 2013-06-27 10:43:19 -07:00
Mitchell Hashimoto 8351d01c71 Remove the "compress" post-processor for now 2013-06-27 08:32:15 -07:00
Mitchell Hashimoto 32a5d3c2b7 website: change up some code format sizing 2013-06-27 08:30:21 -07:00
Mitchell Hashimoto 3e4b19a1e5 website: document keep_input_artifact 2013-06-27 08:25:12 -07:00
Mitchell Hashimoto cf62c812f3 website: document the vagrant post-processor 2013-06-27 08:17:00 -07:00
Mitchell Hashimoto 90b97b147b post-processor: Can specify VF template for AWS 2013-06-27 07:40:33 -07:00
Mitchell Hashimoto 2ba65c2328 post-processor: Recognize the virtualbox builder 2013-06-27 07:39:11 -07:00
Mitchell Hashimoto 7884eb5bdf post-processor/vagrant: Ability to specify Vagrantfile template 2013-06-27 07:38:33 -07:00
Mitchell Hashimoto b229e02d33 post-processor/vagrant: VirtualBox post-processor 2013-06-27 07:33:32 -07:00
Mitchell Hashimoto 014fce77d8 post-processor/vagrant: more Ui output 2013-06-27 07:17:08 -07:00
Mitchell Hashimoto b4fe0530d8 post-processor/vagrant: allow config of individual pp's 2013-06-27 07:14:15 -07:00
Jack Pearkes 21025f64d5 website: docuemnt shell provisioner environment variables 2013-06-27 14:42:28 +02:00
Jack Pearkes 7b32212c97 provisioner/shell: add support for environment variables to be injected 2013-06-27 14:42:14 +02:00
Mitchell Hashimoto 95767b9d85 fmt 2013-06-26 19:09:39 -07:00
Mitchell Hashimoto 47c7a30bbd post-processor/vagrant: the proper post-processor is actually run 2013-06-26 19:09:24 -07:00
Mitchell Hashimoto 962e5b1888 post-processor/vagrant: Can make AWS boxes! 2013-06-26 18:55:11 -07:00
Mitchell Hashimoto 2e0a051539 builder/*: Fail if provisioning fails [GH-33] 2013-06-26 17:54:57 -07:00
Mitchell Hashimoto d16d5eeec5 provisioner/shell: Error if a script fails 2013-06-26 17:52:49 -07:00
Mitchell Hashimoto 9e786cf754 packer: Provisioner/Hook can have errors returned 2013-06-26 17:50:25 -07:00
Mitchell Hashimoto 227ab55617 builder/amazonebs: Artifact ID works 2013-06-26 17:40:21 -07:00
Mitchell Hashimoto 0f0041725e post-processor/vagrant: boilerplate 2013-06-26 17:37:46 -07:00
Mitchell Hashimoto accc2c27f5 website: Clarify the HTTPIP/Port in boot commands [GH-32] 2013-06-26 17:08:53 -07:00
Mitchell Hashimoto 32e319920c fmt 2013-06-25 14:31:06 -05:00
Mitchell Hashimoto dd92d492ee packer/plugin: No need for a panic when plugins error 2013-06-25 14:30:08 -05:00
Mitchell Hashimoto 3e1d902560 packer/plugin: Require the magic cookie to be present to run
This is just a silly check to make sure people aren't executing
the plugins directly. If they are, a nicer error message is shown.
2013-06-25 14:27:20 -05:00
Mitchell Hashimoto e3aada4a2f website: fix the links to downloads 2013-06-25 14:16:49 -05:00
Mitchell Hashimoto 504ba96506 website: fix docs installation page 2013-06-25 14:14:45 -05:00
Mitchell Hashimoto 11a4604102 website: hacks on hacks to center the download link 2013-06-24 16:51:26 -07:00
Mitchell Hashimoto f6ae2f242c website: put popular OS on top 2013-06-24 16:46:06 -07:00
Mitchell Hashimoto 3cf1a2fbf4 website: add OS icons 2013-06-24 16:34:59 -07:00
Mitchell Hashimoto a33fac607c website: use image_tag so that the asset hash stuff works 2013-06-24 16:31:25 -07:00
Mitchell Hashimoto 14551e4af7 website: downloads page 2013-06-24 16:25:42 -07:00
Mitchell Hashimoto b117f617cf scripts: SHA256SUMs file for checksumming 2013-06-24 15:13:59 -07:00
Mitchell Hashimoto b9dbefb21a scripts: temporarily password protect ZIPs, upload script 2013-06-24 14:56:53 -07:00
Mitchell Hashimoto 2f77d06756 LICENSE: MPL2 2013-06-24 14:29:15 -07:00
Mitchell Hashimoto 7f68cece3a website: nav min width so it doesn't squish 2013-06-24 13:59:10 -07:00
Mitchell Hashimoto d09debd85a website: proper spacing on community links 2013-06-24 12:09:20 -07:00
Mitchell Hashimoto 3bff2e5ffd website: better homepage cSS 2013-06-24 10:27:27 -07:00
Mitchell Hashimoto c13d867b95 website: better homepage 2013-06-24 10:24:13 -07:00
Mitchell Hashimoto e38b84a2f3 website: fix up the interface pastings to not have comments 2013-06-24 09:39:20 -07:00
Mitchell Hashimoto 5772937515 website: remove the hook TODO 2013-06-24 09:36:55 -07:00
Mitchell Hashimoto d7ec646506 builder/virtualbox: "stopping" is still running [GH-30] 2013-06-24 09:32:08 -07:00
Mitchell Hashimoto f0255837d4 builder/digitalocean: Make tests pass again 2013-06-24 09:25:00 -07:00
Mitchell Hashimoto 7c2475e886 builder/virtualbox: remap versions that don't have guest additions 2013-06-24 09:24:16 -07:00
Jack Pearkes b06b8e67f5 website: update digitalocean state_timeout default 2013-06-24 09:07:33 +02:00
Jack Pearkes 3fb6fa2444 builder/digitalocean: raised state_timeout default to 6 minutes [GH-26] 2013-06-24 09:03:25 +02:00
Mitchell Hashimoto e1cca94579 website: Update virtualbox to note guest additions stuff 2013-06-23 23:51:30 -07:00
Mitchell Hashimoto 1b64c62faf website: rename EC2 to Amazon EC2 2013-06-23 23:45:29 -07:00
Mitchell Hashimoto 777c869c61 builder/virtualbox: checksum the guest additions 2013-06-23 23:44:03 -07:00
Mitchell Hashimoto 972a28c4f4 website: guest_additions_path docs for virtualbox builder 2013-06-23 23:17:20 -07:00
Mitchell Hashimoto 3259bd7e23 builder/virtualbox: treat guest_additions_path as a template 2013-06-23 23:14:19 -07:00
Mitchell Hashimoto 9e8b1b424b builder/virtualbox: upload guest additions to VM 2013-06-23 23:09:52 -07:00
Mitchell Hashimoto 58e677d112 packer: Ui.Message should Fprint, not Fprintf 2013-06-23 23:06:59 -07:00
Mitchell Hashimoto 4fb31f1cab builder/virtualbox: Download guest additions for the VM 2013-06-23 23:05:32 -07:00
Mitchell Hashimoto c51a233970 website: update docs for virtualbox_version_file 2013-06-23 22:46:57 -07:00
Mitchell Hashimoto fc5c63d697 builder/virtualbox: Upload version to a "virtualbox_version_file" 2013-06-23 22:44:58 -07:00
Mitchell Hashimoto d4cdccb51b builder/virtualbox: only power off the machine if it isrunning 2013-06-23 22:00:40 -07:00
Mitchell Hashimoto f6113de170 builder/virtualbox: Message for vboxmanage command, not say 2013-06-23 21:56:14 -07:00
Mitchell Hashimoto f68639c5fa builder/virtualbox: recognize <tab> 2013-06-23 21:50:16 -07:00
Mitchell Hashimoto 688be43811 builder/virtualbox: Copy ISO because VirtualBox can't recognize 2013-06-23 21:47:56 -07:00
Mitchell Hashimoto 1e6fd243b1 builder/virtualbox: Output VBoxManage stderr in error 2013-06-23 21:19:41 -07:00
Mitchell Hashimoto d1ade20413 website: document vboxmanage calls 2013-06-23 21:06:49 -07:00
Mitchell Hashimoto 436d796689 builder/virtualbox: add "vboxmanage" to run custom commands 2013-06-23 20:58:22 -07:00
Mitchell Hashimoto 2ac81bfc4d fmt 2013-06-23 20:43:50 -07:00
Mitchell Hashimoto fb139b2925 builder/virtualbox: Ability to set DiskSize 2013-06-23 20:43:40 -07:00
Mitchell Hashimoto 8f7ea692c6 website: Add ssh_port docs for virtualbox 2013-06-23 20:41:10 -07:00
Mitchell Hashimoto cf2fb01edb builder/vmware, builder/virtualbox: Don't continue if ISO error 2013-06-23 18:23:00 -07:00
Mitchell Hashimoto 4d41d90c97 builder/vmware: Try SSH handshake multiple times 2013-06-23 18:17:38 -07:00
Mitchell Hashimoto fc9604abb0 provisioner/shell: Error message if provisioning fails 2013-06-23 17:37:44 -07:00
Mitchell Hashimoto 641c626f11 communicator/ssh: request a PTY 2013-06-23 17:36:45 -07:00
Mitchell Hashimoto e082abea28 builder/vmware: support the <tab> special in boot commands 2013-06-23 16:09:12 -07:00
Mitchell Hashimoto 7411e8dc41 builder/common: set the proper finalPath if downloading 2013-06-23 15:58:47 -07:00
Mitchell Hashimoto d809444065 website: disk_size docs for VMware 2013-06-23 15:08:37 -07:00
Mitchell Hashimoto 456aec3390 builder/vmware: configurable disk size (default 40 GB) 2013-06-23 15:07:19 -07:00
Mitchell Hashimoto c29c4ff968 website: document ssh_port for VMware builder 2013-06-23 14:32:48 -07:00
Mitchell Hashimoto 62406b5ab5 builder/vmware: Ability to specify the SSH port with "ssh_port" 2013-06-23 14:30:52 -07:00
Mitchell Hashimoto 0c59ad8087 provisioner/shell: copy the scripts [GH-29] 2013-06-23 11:56:49 -07:00
Mitchell Hashimoto 702cef84fa Merge pull request #26 from mitchellh/digital-ocean-state-timeout
DigitalOcean: Add configuration for state timeout
2013-06-23 11:46:55 -07:00
Jack Pearkes 892299346f website: document digitalocean state_timeout configuration 2013-06-23 12:58:00 +02:00
Jack Pearkes 7c98be0e52 builder/digitalocean: add configurable state_timeout
The state_timeout config allows you to determine the timeout
for "waiting for droplet to become [active, off, etc.]".

This still defaults to 3 minutes.
2013-06-23 12:51:51 +02:00
Mitchell Hashimoto 1ced19c3ce builder/digitalocean: Reattempt SSH handshake a few times
I ran into a few cases where the droplet was active and a TCP connection
could be made, but SSH wasn't running yet and the handshake failed. A
race condition with the machine boot. This will retry the SSH handshake
a few times.

/cc @pearkes
2013-06-21 23:02:13 -07:00
Mitchell Hashimoto 1ecfe4b274 website: community! 2013-06-21 20:46:51 -07:00
Mitchell Hashimoto b87cdda9e4 website: parallel and next steps 2013-06-21 20:19:33 -07:00
Mitchell Hashimoto 577e6caa1a website: parallel builds 2013-06-21 20:10:24 -07:00
Mitchell Hashimoto 84d84199f6 website: open source 2013-06-21 16:51:39 -07:00
Mitchell Hashimoto bbb8dd774a website: build/provision 2013-06-21 16:48:01 -07:00
Mitchell Hashimoto b2b85d460f website: installing packer 2013-06-21 14:50:00 -07:00
Mitchell Hashimoto 6538163094 website: supported platforms 2013-06-21 12:18:29 -07:00
Mitchell Hashimoto c2d18a28a4 website: remove redundant docs pages 2013-06-21 11:48:24 -07:00
Mitchell Hashimoto 170198157b website: intro section 2013-06-21 11:46:38 -07:00
Mitchell Hashimoto 79eb0cdfb1 website: darken the links on the docs content cause of white bg 2013-06-21 00:57:50 -07:00
Mitchell Hashimoto 684b6b182d website: more centered download link 2013-06-21 00:55:13 -07:00
Mitchell Hashimoto 7eaccfd522 website: homepage TODO 2013-06-20 23:27:06 -07:00
Mitchell Hashimoto 838aadc579 website: lots of home page work 2013-06-20 23:17:59 -07:00
Mitchell Hashimoto 5d5ec90592 website: Improved inline code styling 2013-06-20 23:00:27 -07:00
Mitchell Hashimoto 3721ba72d8 website: Move the "what is" to the docs homepage 2013-06-20 22:57:39 -07:00
Mitchell Hashimoto 7b1777cf37 website: Use myriad pro 2013-06-20 22:46:33 -07:00
Mitchell Hashimoto 089f00b6f1 website: source prettify ourselves 2013-06-20 22:41:43 -07:00
Mitchell Hashimoto dcf34b6578 website: lots of tweaking around code styling 2013-06-20 22:19:49 -07:00
Mitchell Hashimoto 40a9356b9f website: header padding 2013-06-20 19:12:18 -07:00
Mitchell Hashimoto 0ac56cc13d website: link to HashiCorp works properly 2013-06-20 19:05:42 -07:00
Mitchell Hashimoto 0d6dc0852c website: add favicon 2013-06-20 19:01:32 -07:00
Mitchell Hashimoto 5448a997cc website: Fix hero element centering 2013-06-20 19:00:05 -07:00
Mitchell Hashimoto c0981c412e website: fix homepage link 2013-06-20 18:42:44 -07:00
Mitchell Hashimoto 2d1707ec2a website: fix the includes to be _ prefixed 2013-06-20 18:40:04 -07:00
Mitchell Hashimoto 0b6d0efa7b website: docs layout 2013-06-20 18:35:02 -07:00
Mitchell Hashimoto bf664fce1b website: tweaking the homepage 2013-06-20 17:46:51 -07:00
Mitchell Hashimoto 55889aee64 website: homepage coming in, although a bit rough right now 2013-06-20 17:27:04 -07:00
Mitchell Hashimoto e9593bf49e scripts: script for publishing to the website 2013-06-20 14:48:57 -07:00
Mitchell Hashimoto a68f6bf6de packer: Better docs for communicator interface 2013-06-20 14:46:25 -07:00
Mitchell Hashimoto b019a15c80 website: custom provisioner docs 2013-06-20 14:46:21 -07:00
Mitchell Hashimoto 581173eeb8 website: plugin dev tips headers 2013-06-20 14:30:02 -07:00
Mitchell Hashimoto a148672433 website: build command documentation 2013-06-20 14:24:30 -07:00
Mitchell Hashimoto 7efbfa0b15 website: validate command docs 2013-06-20 14:13:35 -07:00
Mitchell Hashimoto c573cf2370 website: start documenting the command-line 2013-06-20 14:08:04 -07:00
Mitchell Hashimoto 04a2afd8ee website: update terminology 2013-06-20 14:02:04 -07:00
Mitchell Hashimoto 0e421d2eb0 website: update shell provisioner docs 2013-06-20 13:51:18 -07:00
Mitchell Hashimoto badad141d3 provisioner/shell: can specify multiple scripts to provision with 2013-06-20 13:45:54 -07:00
Mitchell Hashimoto ebccdda8ab packer/rpc: Wrap errors in BasicError for RPC config errors 2013-06-20 12:55:11 -07:00
Mitchell Hashimoto 091533246e Make sure the cache dir is absolute 2013-06-20 12:37:17 -07:00
Mitchell Hashimoto 95da55c0fa builder/vmware: Log the output of various commands 2013-06-20 12:33:01 -07:00
Mitchell Hashimoto 23712375aa Default cache to "packer_cache" in CWD 2013-06-20 12:18:03 -07:00
Mitchell Hashimoto 7586b6ec90 Update TODO 2013-06-20 11:53:27 -07:00
Mitchell Hashimoto 0aa9b2df78 Remove "packerrc" from the gitignore, it isn't used anymore 2013-06-20 11:52:54 -07:00
Mitchell Hashimoto 525813722d Ignore the pkg directory 2013-06-20 11:49:53 -07:00
Mitchell Hashimoto 6e805be1d6 scripts: zip the packages for dist 2013-06-20 00:01:12 -07:00
Mitchell Hashimoto 6bb15b7d58 make dist.sh executable 2013-06-19 22:46:24 -07:00
Mitchell Hashimoto 79f230b0b0 move scripts into the scripts/ folder 2013-06-19 22:44:02 -07:00
Mitchell Hashimoto 641ece6376 dist.sh maxes out CPU 2013-06-19 22:40:30 -07:00
Mitchell Hashimoto b37c171676 Begin work on the dist.sh script to create the distribution 2013-06-19 22:20:52 -07:00
Mitchell Hashimoto d1c69048ed fmt 2013-06-19 21:20:54 -07:00
Mitchell Hashimoto 8b9263d38f builder/vmware: properly handle errors 2013-06-19 21:20:48 -07:00
Mitchell Hashimoto 848985b200 builder/virtualbox: proper artifact [GH-23] 2013-06-19 21:12:11 -07:00
Mitchell Hashimoto 0028563253 builder/virtualbox: properly handle errors 2013-06-19 21:07:53 -07:00
Mitchell Hashimoto 7db824f457 builder/digitalocean: Properly return errors 2013-06-19 21:00:51 -07:00
Mitchell Hashimoto c490911eb6 builder/amazonebs: Get rid of TODO since we can specify source states 2013-06-19 20:57:56 -07:00
Mitchell Hashimoto cac0f49bb8 builder/amazonebs: Return proper errors 2013-06-19 20:54:02 -07:00
Mitchell Hashimoto 6d57e0c530 builder/digitalocean: timeout TCP connections to SSH 2013-06-19 13:26:08 -07:00
Mitchell Hashimoto 0235a00545 builder/digitalocean: compile with scrub changes 2013-06-19 13:26:03 -07:00
Mitchell Hashimoto 0770e2ddec builder/digitalocean: Scrub sensitive information out of logs
/cc @pearkes
2013-06-19 13:18:53 -07:00
Mitchell Hashimoto 214d679e2d command/build: output <nothing> properly if no artifact 2013-06-19 13:07:52 -07:00
Mitchell Hashimoto 7dbe330099 packer: Discard log output in tests 2013-06-18 23:18:21 -07:00
Mitchell Hashimoto f76027d449 packer: Improved logging around build runs 2013-06-18 23:05:02 -07:00
Mitchell Hashimoto 360a6939a4 packer: Post-process chain works properly 2013-06-18 22:58:23 -07:00
Mitchell Hashimoto d3e120c6fb command/build: Say if no artifacts were created 2013-06-18 22:53:30 -07:00
Mitchell Hashimoto dabe7ac584 packer: keep_input_artifact will keep prior artifact in a PP
[GH-19]
2013-06-18 22:45:53 -07:00
Mitchell Hashimoto 221281b714 builder/digitalocean: fmt 2013-06-18 22:02:09 -07:00
Mitchell Hashimoto 72f5d84cb7 fmt 2013-06-18 21:54:33 -07:00
Mitchell Hashimoto 91253c4f32 builder/digitalocean: Implement Artifact destroy
/cc @pearkes
2013-06-18 21:54:15 -07:00
Mitchell Hashimoto c3de114585 post-processor/compress: Boilerplate for the compress PP 2013-06-18 21:18:41 -07:00
Mitchell Hashimoto 593295c7d9 packer: Build only adds post-processor artifact if not nil 2013-06-18 21:15:14 -07:00
Mitchell Hashimoto b5546262ca command/validate: lol spelling error 2013-06-18 21:10:46 -07:00
Mitchell Hashimoto 6b42b3b329 command/validate: better logging 2013-06-18 21:10:34 -07:00
Mitchell Hashimoto a8b66cf020 packer/rpc: Convert any errors in configure to basic error 2013-06-18 21:04:33 -07:00
Mitchell Hashimoto 6e0685047e packer/rpc: Environment.PostProcessor() properly sets thigns up 2013-06-18 20:54:40 -07:00
Mitchell Hashimoto 9e78cbaa89 packer: PostProcessor takes a UI [GH-20] 2013-06-18 20:38:21 -07:00
Mitchell Hashimoto 44087ca7df website: finish digitalocean docs 2013-06-18 19:25:54 -07:00
Mitchell Hashimoto 78de34538d rename digital-ocean to digitalocean 2013-06-18 16:52:22 -07:00
Mitchell Hashimoto 31982bcfc6 website: document digitalocean builder 2013-06-18 16:51:46 -07:00
Mitchell Hashimoto 338298b8af command/build, command/validate: Setup proper components to avoid nil 2013-06-18 16:29:29 -07:00
Mitchell Hashimoto 0edeb49467 builder/amazonebs: If only one error on destroy, just return it 2013-06-18 16:25:35 -07:00
Mitchell Hashimoto 4f229dea09 builder/amazonebs: Implement Artifact.Destroy 2013-06-18 16:24:35 -07:00
Mitchell Hashimoto 0f354c79d1 packer: Add Destroy method to artifact
[GH-18]
2013-06-18 16:01:14 -07:00
Mitchell Hashimoto ce1900c477 website: custom post-processor dev 2013-06-18 14:40:37 -07:00
Mitchell Hashimoto 1bf8237d64 website: post-processors docs for templates 2013-06-18 14:40:37 -07:00
Robby Colvin c992de45a3 Fix for find in Linux 2013-06-18 14:07:36 -07:00
Mitchell Hashimoto 51fe46e6d9 packer/plugin: Support PostProcessor 2013-06-18 13:49:07 -07:00
Mitchell Hashimoto 9b9af6dc9d packer/rpc: Support PostProcessor 2013-06-18 13:44:57 -07:00
Mitchell Hashimoto ca7e8dbb74 Allow post-processors in the core configuration 2013-06-18 11:00:31 -07:00
Mitchell Hashimoto e2534fe88d packer: Build runs the post-processors 2013-06-18 10:54:29 -07:00
Mitchell Hashimoto 04a8bfb455 packer: Post-processors are configured 2013-06-18 10:31:52 -07:00
Mitchell Hashimoto 1015df8fa8 packer: Build can return multiple artifacts 2013-06-18 10:24:23 -07:00
Mitchell Hashimoto dab3eb5ece packer/rpc: Get RPC compliant with Environment again 2013-06-18 10:05:45 -07:00
Mitchell Hashimoto 91a6a7797d packer: builds now have post processors as part of them 2013-06-18 09:58:39 -07:00
Mitchell Hashimoto 18f9677b54 packer: Environment can look up post processors 2013-06-18 09:49:05 -07:00
Mitchell Hashimoto 6a549f0548 packer: panic if Prepare called twice on build, lock 2013-06-18 09:37:49 -07:00
Mitchell Hashimoto 7522c36112 packer: Avoid an extra allocation by using clever addressing 2013-06-18 09:30:23 -07:00
Mitchell Hashimoto 9bd36a76e8 packer: Parse post-processors in templates
This includes parsing for the simple, detailed, and sequential
processors.
2013-06-18 09:27:08 -07:00
Mitchell Hashimoto e02afe1775 website: update path to core config 2013-06-17 22:30:37 -07:00
Mitchell Hashimoto a6e0ea8bd2 Don't depend on os/user anymore, which requires cgo 2013-06-17 22:10:11 -07:00
Robby Colvin 148d95def5 fmt 2013-06-17 16:36:22 -07:00
Mitchell Hashimoto 75db279364 Check same directory as packer for plugins. 2013-06-17 15:55:21 -07:00
Mitchell Hashimoto e1c0616a14 builder/amazonebs: Tests for invalid AMI name 2013-06-17 15:24:33 -07:00
Mitchell Hashimoto dc6519f7c1 communicator/ssh: remove unusable code 2013-06-17 15:20:31 -07:00
Mitchell Hashimoto 206ec4e5bf fmt 2013-06-17 15:19:33 -07:00
Jack Pearkes 8d41363085 builder/digitalocean: only execute the snapshotname template if parsed 2013-06-18 00:06:59 +02:00
Jack Pearkes e0a4e72be5 builder/amazonebs: check for err parsing template for ami name 2013-06-17 23:55:08 +02:00
Jack Pearkes 1a6f410257 builder/digitalocean: check for err parsing template for snapshot name 2013-06-17 23:54:28 +02:00
Mitchell Hashimoto 7127ad967b Merge pull request #17 from mitchellh/b-digital-ocean-fixup
DigitalOcean Completion
2013-06-17 14:34:59 -07:00
Mitchell Hashimoto 9bcca1a77b packer: An initial PostProcessor interface 2013-06-17 11:56:26 -07:00
Mitchell Hashimoto 7d63c196d5 command/build: only output artifacts if we have some 2013-06-17 11:49:12 -07:00
Mitchell Hashimoto d31b2d0038 command/build: Improved output when builds error 2013-06-17 11:48:21 -07:00
Mitchell Hashimoto 1cbd3d6a9f packer: Output a newline when interrupted for UI 2013-06-17 11:40:57 -07:00
Jack Pearkes 46d3e7c1a4 builder/digitalocean: print bad status code as string 2013-06-17 14:54:24 +02:00
Jack Pearkes 1e6780e496 builder/digitalocean: improve error messages from DO api 2013-06-17 14:54:18 +02:00
Jack Pearkes 031b20f197 builder/digitalocean: use text/template for the snapshot name 2013-06-17 14:21:15 +02:00
Jack Pearkes 54e8eaab1c builder/digitalocean: add configurable "event_delay" for sleeps 2013-06-17 13:28:21 +02:00
Jack Pearkes 875ee0a871 builder/digitalocean: implement artifacts with the snapshot name 2013-06-17 13:01:42 +02:00
Mitchell Hashimoto c1e7d4314f packer: If interrupted, Ask is disabled 2013-06-15 18:25:34 -07:00
Mitchell Hashimoto 676041dc15 packer: Ui can return an error for Ask, returns one for interrupt 2013-06-15 18:24:38 -07:00
Jack Pearkes 7f8cd0caf7 builder/digitalocean: generate temp rsa keypairs for ssh communication 2013-06-15 22:43:18 +02:00
Mitchell Hashimoto abbf9798b4 packer/rpc: Panic in error case 2013-06-15 11:27:09 -07:00
Mitchell Hashimoto 3435e63b52 packer: Get rid of "name" in template, wasn't used for anything 2013-06-15 11:14:44 -07:00
Mitchell Hashimoto 793877568f builder/digitalocean: Make tests pass
/cc @pearkes
2013-06-15 11:11:03 -07:00
Mitchell Hashimoto ba1c7101c5 builder/digitalocean: Add support for -debug mode
/cc @pearkes
2013-06-15 11:09:26 -07:00
Mitchell Hashimoto d0dc0a769e fmt 2013-06-15 11:07:30 -07:00
Mitchell Hashimoto 5b31c2f073 builder/digitalocean: Adhere to new interface, make default in Packer
@pearkes: I added "digital-ocean" to the default config since it will
be shipping with Packer. :)
2013-06-15 11:06:39 -07:00
Mitchell Hashimoto a6eea4642a Merge pull request #15 from pearkes/f-do-builder
DigitalOcean Builder from @pearkes
2013-06-15 11:04:28 -07:00
Jack Pearkes 8ba8932552 builder/digitalocean: No need for destroy steps, builder works! 2013-06-14 15:26:03 +02:00
Jack Pearkes a774e2b444 builder/digitalocean: completed initial pass at all steps. 2013-06-13 19:56:34 +02:00
Jack Pearkes dd6e4e4933 builder/digitalocean: connect_ssh, create_droplet, droplet_info 2013-06-13 18:48:19 +02:00
Jack Pearkes 4e6993909c builder/digitalocean: builder config tests and create_ssh_key step 2013-06-13 17:58:06 +02:00
Jack Pearkes 8599af62a4 builder/digitalocean: add the do builder as a plugin 2013-06-13 16:29:23 +02:00
Jack Pearkes 787a3178b3 builder/digitalocean: WIP commit of api interface and initial config 2013-06-13 16:03:10 +02:00
215 changed files with 8596 additions and 677 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
/bin
/local
/pkg
/website/.sass-cache
/website/build
packerrc
+3
View File
@@ -0,0 +1,3 @@
## 0.1.0 (June 28, 2013)
* Initial release
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+1 -1
View File
@@ -8,7 +8,7 @@ all:
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
@go get -d -v ./...
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./build.sh
@./scripts/build.sh
format:
go fmt ./...
+12 -5
View File
@@ -11,12 +11,19 @@ Packer is lightweight, runs on every major operating system, and is highly
performant, creating machine images for multiple platforms in parallel.
Packer comes out of the box with support for creating AMIs (EC2), VMware
images, and VirtualBox images. Support for more platforms can be added via
plugins.
plugins. The images that Packer creates an easily be turned into
[Vagrant](http://www.vagrantup.com) boxes.
## Quick Start
First, get Packer by either downloading a pre-built Packer binary for
your operating system or [downloading and compiling Packer yourself](#developing-packer).
**Note:** There is a great
[introduction and getting started guide](http://localhost:4567/intro)
for those with a bit more patience. Otherwise, the quick start below
will get you up and running quickly, at the sacrifice of not explaining some
key points.
First, [downloada pre-built Packer binary](http://localhost:4567/downloads.html)
for your operating system or [compile Packer yourself](#developing-packer).
After Packer is installed, create your first template, which tells Packer
what platforms to build images for and how you want to build them. In our
@@ -32,9 +39,9 @@ own.
"secret_key": "YOUR SECRET KEY HERE",
"region": "us-east-1",
"source_ami": "ami-de0d9eb7",
"instance_type": "m1.small",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-quick-start {{.CreateTime}}"
"ami_name": "packer-example {{.CreateTime}}"
}]
}
```
-2
View File
@@ -1,9 +1,7 @@
* builder/amazonebs: Copy AMI to multiple regions
* builder/vmware: Downloading the ISO
* builder/vmware: VMX templates
* communicator/ssh: Ability to re-establish connection
* communicator/ssh: Download()
* packer: Communicator should have Close() method
* packer/plugin: Better error messages/detection if plugin crashes
* packer/plugin: Testing of client struct/methods
* provisioner/shell: Arguments
+36 -3
View File
@@ -2,12 +2,18 @@ package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/packer"
"log"
"strings"
)
type artifact struct {
// A map of regions to AMI IDs.
amis map[string]string
// EC2 connection for performing API stuff.
conn *ec2.EC2
}
func (*artifact) BuilderId() string {
@@ -19,9 +25,13 @@ func (*artifact) Files() []string {
return nil
}
func (*artifact) Id() string {
// TODO(mitchellh): Id
return "TODO"
func (a *artifact) Id() string {
parts := make([]string, 0, len(a.amis))
for region, amiId := range a.amis {
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
}
return strings.Join(parts, ",")
}
func (a *artifact) String() string {
@@ -33,3 +43,26 @@ func (a *artifact) String() string {
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *artifact) Destroy() error {
errors := make([]error, 0)
for _, imageId := range a.amis {
log.Printf("Degistering image ID: %s", imageId)
if _, err := a.conn.DeregisterImage(imageId); err != nil {
errors = append(errors, err)
}
// TODO(mitchellh): Delete the snapshots associated with an AMI too
}
if len(errors) > 0 {
if len(errors) == 1 {
return errors[0]
} else {
return &packer.MultiError{errors}
}
}
return nil
}
+15 -1
View File
@@ -13,6 +13,20 @@ func TestArtifact_Impl(t *testing.T) {
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
expected := `east:foo,west:bar`
amis := make(map[string]string)
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
result := a.Id()
assert.Equal(result, expected, "should match output")
}
func TestArtifactString(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
@@ -25,7 +39,7 @@ west: bar`
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis}
a := &artifact{amis, nil}
result := a.String()
assert.Equal(result, expected, "should match output")
}
+23 -3
View File
@@ -15,6 +15,7 @@ import (
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
"time"
)
@@ -37,7 +38,7 @@ type config struct {
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
}
@@ -98,6 +99,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
if b.config.AMIName == "" {
errs = append(errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
@@ -145,13 +155,23 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
b.runner.Run(state)
// If there are no AMIs, then jsut return
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
return &artifact{state["amis"].(map[string]string)}, nil
artifact := &artifact{
amis: state["amis"].(map[string]string),
conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
+29
View File
@@ -13,6 +13,7 @@ func testConfig() map[string]interface{} {
"instance_type": "foo",
"region": "us-east-1",
"ssh_username": "root",
"ami_name": "foo",
}
}
@@ -60,6 +61,34 @@ func TestBuilderPrepare_AccessKey(t *testing.T) {
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InstanceType(t *testing.T) {
var b Builder
config := testConfig()
+10 -3
View File
@@ -2,6 +2,7 @@ package amazonebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
@@ -27,7 +28,9 @@ func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
ui.Say(fmt.Sprintf("Error setting up SSH config: %s", err))
err := fmt.Errorf("Error setting up SSH config: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -85,7 +88,9 @@ ConnectWaitLoop:
// We connected. Just break the loop.
break ConnectWaitLoop
case <-timeout:
ui.Error("Timeout while waiting to connect to SSH.")
err := errors.New("Timeout waiting for SSH to become available.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
@@ -101,7 +106,9 @@ ConnectWaitLoop:
}
if err != nil {
ui.Error(fmt.Sprintf("Error connecting to SSH: %s", err))
err := fmt.Errorf("Error connecting to SSH: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+4
View File
@@ -42,6 +42,8 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
createResp, err := ec2conn.CreateImage(createOpts)
if err != nil {
err := fmt.Errorf("Error creating AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -57,6 +59,8 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
for {
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error querying images: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+2
View File
@@ -23,6 +23,8 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := ec2conn.CreateKeyPair(keyName)
if err != nil {
err := fmt.Errorf("Error creating temporary keypair: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+4 -1
View File
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
hook.Run(packer.HookProvision, ui, comm, nil)
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
@@ -31,6 +31,8 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
ui.Say("Launching a source AWS instance...")
runResp, err := ec2conn.RunInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source instance: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -41,6 +43,8 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
ui.Say("Waiting for instance to become ready...")
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
if err != nil {
err := fmt.Errorf("Error waiting for instance to become ready: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -61,5 +65,9 @@ func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
ui.Say("Terminating the source AWS instance...")
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
pending := []string{"pending", "running", "shutting-down", "stopped", "stopping"}
waitForState(ec2conn, s.instance, pending, "terminated")
}
+3
View File
@@ -44,6 +44,8 @@ func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepActi
ui.Say("Authorizing SSH access on the temporary security group...")
if _, err := ec2conn.AuthorizeSecurityGroup(groupResp.SecurityGroup, perms); err != nil {
err := fmt.Errorf("Error creating temporary security group: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -65,6 +67,7 @@ func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
ui.Say("Deleting temporary security group...")
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
if err != nil {
log.Printf("Error deleting security group: %s", err)
ui.Error(fmt.Sprintf(
"Error cleaning up security group. Please delete the group manually: %s", s.groupId))
}
+5 -2
View File
@@ -1,6 +1,7 @@
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
@@ -17,16 +18,18 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio
ui.Say("Stopping the source instance...")
_, err := ec2conn.StopInstances(instance.InstanceId)
if err != nil {
err := fmt.Errorf("Error stopping instance: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the instance to actual stop
// TODO(mitchellh): Handle diff source states, i.e. this force state sucks
ui.Say("Waiting for the instance to stop...")
instance.State.Name = "stopping"
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+3 -1
View File
@@ -86,6 +86,8 @@ func (d *DownloadClient) Get() (string, error) {
if url.Scheme == "file" && !d.config.CopyFile {
finalPath = url.Path
} else {
finalPath = d.config.TargetPath
var ok bool
d.downloader, ok = d.config.DownloaderMap[url.Scheme]
if !ok {
@@ -93,7 +95,7 @@ func (d *DownloadClient) Get() (string, error) {
}
// Otherwise, download using the downloader.
f, err := os.Create(d.config.TargetPath)
f, err := os.Create(finalPath)
if err != nil {
return "", err
}
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
@@ -27,7 +28,12 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
result := make(chan string, 1)
go func() {
result <- ui.Ask(message)
line, err := ui.Ask(message)
if err != nil {
log.Printf("Error asking for input: %s", err)
}
result <- line
}()
for {
+218
View File
@@ -0,0 +1,218 @@
// All of the methods used to communicate with the digital_ocean API
// are here. Their API is on a path to V2, so just plain JSON is used
// in place of a proper client library for now.
package digitalocean
import (
"encoding/json"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
)
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
type Image struct {
Id uint
Name string
Distribution string
}
type ImagesResp struct {
Images []Image
}
type DigitalOceanClient struct {
// The http client for communicating
client *http.Client
// The base URL of the API
BaseURL string
// Credentials
ClientID string
APIKey string
}
// Creates a new client for communicating with DO
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
c := &DigitalOceanClient{
client: http.DefaultClient,
BaseURL: DIGITALOCEAN_API_URL,
ClientID: client,
APIKey: key,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
// Escape the public key
pub = url.QueryEscape(pub)
params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub)
body, err := NewRequest(d, "ssh_keys/new", params)
if err != nil {
return 0, err
}
// Read the SSH key's ID we just created
key := body["ssh_key"].(map[string]interface{})
keyId := key["id"].(float64)
return uint(keyId), nil
}
// Destroys an SSH key
func (d DigitalOceanClient) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequest(d, path, "")
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) {
params := fmt.Sprintf(
"name=%v&image_id=%v&size_id=%v&region_id=%v&ssh_key_ids=%v",
name, image, size, region, keyId)
body, err := NewRequest(d, "droplets/new", params)
if err != nil {
return 0, err
}
// Read the Droplets ID
droplet := body["droplet"].(map[string]interface{})
dropletId := droplet["id"].(float64)
return uint(dropletId), err
}
// Destroys a droplet
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequest(d, path, "")
return err
}
// Powers off a droplet
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequest(d, path, "")
return err
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
params := fmt.Sprintf("name=%v", name)
_, err := NewRequest(d, path, params)
return err
}
// Returns all available images.
func (d DigitalOceanClient) Images() ([]Image, error) {
resp, err := NewRequest(d, "images", "")
if err != nil {
return nil, err
}
var result ImagesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClient) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequest(d, path, "")
return err
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequest(d, path, "")
if err != nil {
return "", "", err
}
var ip string
// Read the droplet's "status"
droplet := body["droplet"].(map[string]interface{})
status := droplet["status"].(string)
if droplet["ip_address"] != nil {
ip = droplet["ip_address"].(string)
}
return ip, status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) {
client := d.client
url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s",
DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey)
var decodedResponse map[string]interface{}
// Do some basic scrubbing so sensitive information doesn't appear in logs
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
resp, err := client.Get(url)
if err != nil {
return decodedResponse, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return decodedResponse, err
}
err = json.Unmarshal(body, &decodedResponse)
log.Printf("response from digitalocean: %v", decodedResponse)
// Catch all non-200 status and return an error
if resp.StatusCode != 200 {
err = errors.New(fmt.Sprintf("Received non-200 HTTP status from DigitalOcean: %v", resp.StatusCode))
return decodedResponse, err
}
if err != nil {
return decodedResponse, err
}
// Catch all non-OK statuses from DO and return an error
status := decodedResponse["status"]
if status != "OK" {
// Get the actual error message if there is one
if status == "ERROR" {
status = decodedResponse["error_message"]
}
err = errors.New(fmt.Sprintf("Received bad status from DigitalOcean: %v", status))
return decodedResponse, err
}
return decodedResponse, nil
}
+39
View File
@@ -0,0 +1,39 @@
package digitalocean
import (
"fmt"
"log"
)
type Artifact struct {
// The name of the snapshot
snapshotName string
// The ID of the image
snapshotId uint
// The client for making API calls
client *DigitalOceanClient
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (*Artifact) Files() []string {
// No files with DigitalOcean
return nil
}
func (a *Artifact) Id() string {
return a.snapshotName
}
func (a *Artifact) String() string {
return fmt.Sprintf("A snapshot was created: %v", a.snapshotName)
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d", a.snapshotId)
return a.client.DestroyImage(a.snapshotId)
}
+23
View File
@@ -0,0 +1,23 @@
package digitalocean
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestArtifact_Impl(t *testing.T) {
var raw interface{}
raw = &Artifact{}
if _, ok := raw.(packer.Artifact); !ok {
t.Fatalf("Artifact should be artifact")
}
}
func TestArtifactString(t *testing.T) {
a := &Artifact{"packer-foobar", 42, nil}
expected := "A snapshot was created: packer-foobar"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
}
}
+225
View File
@@ -0,0 +1,225 @@
// The digitalocean package contains a packer.Builder implementation
// that builds DigitalOcean images (snapshots).
package digitalocean
import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"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
type config struct {
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
RegionID uint `mapstructure:"region_id"`
SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"`
SnapshotName string
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
SSHTimeout time.Duration
EventDelay time.Duration
StateTimeout time.Duration
PackerDebug bool `mapstructure:"packer_debug"`
RawSnapshotName string `mapstructure:"snapshot_name"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawEventDelay string `mapstructure:"event_delay"`
RawStateTimeout string `mapstructure:"state_timeout"`
}
type Builder struct {
config config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
}
// Optional configuration with defaults
//
if b.config.RegionID == 0 {
// Default to Region "New York"
b.config.RegionID = 1
}
if b.config.SizeID == 0 {
// Default to 512mb, the smallest droplet size
b.config.SizeID = 66
}
if b.config.ImageID == 0 {
// Default to base image "Ubuntu 12.04 x64 Server (id: 284203)"
b.config.ImageID = 284203
}
if b.config.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
b.config.SSHUsername = "root"
}
if b.config.SSHPort == 0 {
// Default to port 22 per DO default
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"
}
if b.config.RawEventDelay == "" {
// Default to 5 second delays after creating events
// to allow DO to process
b.config.RawEventDelay = "5s"
}
if b.config.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
b.config.RawStateTimeout = "6m"
}
// A list of errors on the configuration
errs := make([]error, 0)
// Required configurations that will display errors if not set
//
if b.config.ClientID == "" {
errs = append(errs, errors.New("a client_id must be specified"))
}
if b.config.APIKey == "" {
errs = append(errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.SSHTimeout = sshTimeout
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
}
b.config.EventDelay = eventDelay
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
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 = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
} else {
t.Execute(snapNameBuf, tData)
b.config.SnapshotName = snapNameBuf.String()
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
// Initialize the DO API client
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
// Set up the state
state := make(map[string]interface{})
state["config"] = b.config
state["client"] = client
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
new(stepCreateSSHKey),
new(stepCreateDroplet),
new(stepDropletInfo),
new(stepConnectSSH),
new(stepProvision),
new(stepPowerOff),
new(stepSnapshot),
}
// Run the steps
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
if _, ok := state["snapshot_name"]; !ok {
log.Println("Failed to find snapshot_name in state. Bug?")
return nil, nil
}
artifact := &Artifact{
snapshotName: state["snapshot_name"].(string),
snapshotId: state["snapshot_image_id"].(uint),
client: client,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
+323
View File
@@ -0,0 +1,323 @@
package digitalocean
import (
"github.com/mitchellh/packer/packer"
"strconv"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"client_id": "foo",
"api_key": "bar",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"api_key": []string{},
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_APIKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["api_key"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.APIKey != "foo" {
t.Errorf("access key invalid: %s", b.config.APIKey)
}
// Test bad
delete(config, "api_key")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_ClientID(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["client_id"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ClientID != "foo" {
t.Errorf("invalid: %s", b.config.ClientID)
}
// Test bad
delete(config, "client_id")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_RegionID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RegionID != 1 {
t.Errorf("invalid: %d", b.config.RegionID)
}
// Test set
config["region_id"] = 2
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RegionID != 2 {
t.Errorf("invalid: %d", b.config.RegionID)
}
}
func TestBuilderPrepare_SizeID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SizeID != 66 {
t.Errorf("invalid: %d", b.config.SizeID)
}
// Test set
config["size_id"] = 67
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SizeID != 67 {
t.Errorf("invalid: %d", b.config.SizeID)
}
}
func TestBuilderPrepare_ImageID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SizeID != 66 {
t.Errorf("invalid: %d", b.config.SizeID)
}
// Test set
config["size_id"] = 2
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SizeID != 2 {
t.Errorf("invalid: %d", b.config.SizeID)
}
}
func TestBuilderPrepare_SSHUsername(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "root" {
t.Errorf("invalid: %d", b.config.SSHUsername)
}
// Test set
config["ssh_username"] = "foo"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername)
}
}
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawSSHTimeout != "1m" {
t.Errorf("invalid: %d", b.config.RawSSHTimeout)
}
// Test set
config["ssh_timeout"] = "30s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ssh_timeout"] = "tubes"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_EventDelay(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawEventDelay != "5s" {
t.Errorf("invalid: %d", b.config.RawEventDelay)
}
// Test set
config["event_delay"] = "10s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["event_delay"] = "tubes"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawStateTimeout != "6m" {
t.Errorf("invalid: %d", b.config.RawStateTimeout)
}
// Test set
config["state_timeout"] = "5m"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["state_timeout"] = "tubes"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SnapshotName(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
t.Errorf("invalid: %d", b.config.RawSnapshotName)
}
// Test set
config["snapshot_name"] = "foobarbaz"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test set with template
config["snapshot_name"] = "{{.CreateTime}}"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
_, err = strconv.ParseInt(b.config.SnapshotName, 0, 0)
if err != nil {
t.Fatalf("failed to parse int in template: %s", err)
}
}
+137
View File
@@ -0,0 +1,137 @@
package digitalocean
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"net"
"time"
)
type stepConnectSSH struct {
conn net.Conn
}
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
ipAddress := state["droplet_ip"]
// Build the keyring for authentication. This stores the private key
// we'll use to authenticate.
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
err := fmt.Errorf("Error setting up SSH config: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Build the actual SSH client configuration
sshConfig := &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}
// Start trying to connect to SSH
connected := make(chan error, 1)
connectQuit := make(chan bool, 1)
defer func() {
connectQuit <- true
}()
var comm packer.Communicator
go func() {
var err error
ui.Say("Connecting to the droplet via SSH...")
attempts := 0
handshakeAttempts := 0
for {
select {
case <-connectQuit:
return
default:
}
attempts += 1
log.Printf(
"Opening TCP conn for SSH to %s:%d (attempt %d)",
ipAddress, config.SSHPort, attempts)
s.conn, err = net.DialTimeout(
"tcp",
fmt.Sprintf("%s:%d", ipAddress, config.SSHPort),
10*time.Second)
if err == nil {
log.Println("TCP connection made. Attempting SSH handshake.")
comm, err = ssh.New(s.conn, sshConfig)
if err == nil {
log.Println("Connected to SSH!")
break
}
handshakeAttempts += 1
log.Printf("SSH handshake error: %s", err)
if handshakeAttempts > 5 {
connected <- err
return
}
}
// A brief sleep so we're not being overly zealous attempting
// to connect to the instance.
time.Sleep(500 * time.Millisecond)
}
connected <- nil
}()
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
timeout := time.After(config.SSHTimeout)
ConnectWaitLoop:
for {
select {
case err := <-connected:
if err != nil {
err := fmt.Errorf("Error connecting to SSH: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// We connected. Just break the loop.
break ConnectWaitLoop
case <-timeout:
err := errors.New("Timeout waiting for SSH to become available.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
// Set the communicator on the state bag so it can be used later
state["communicator"] = comm
return multistep.ActionContinue
}
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
if s.conn != nil {
s.conn.Close()
}
}
@@ -0,0 +1,74 @@
package digitalocean
import (
"cgl.tideland.biz/identifier"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepCreateDroplet struct {
dropletId uint
}
func (s *stepCreateDroplet) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
sshKeyId := state["ssh_key_id"].(uint)
ui.Say("Creating droplet...")
// Some random droplet name as it's temporary
name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw()))
// Create the droplet based on configuration
dropletId, err := client.CreateDroplet(name, c.SizeID, c.ImageID, c.RegionID, sshKeyId)
if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// We use this in cleanup
s.dropletId = dropletId
// Store the droplet id for later
state["droplet_id"] = dropletId
return multistep.ActionContinue
}
func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) {
// If the dropletid isn't there, we probably never created it
if s.dropletId == 0 {
return
}
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
// Destroy the droplet we just created
ui.Say("Destroying droplet...")
// Sleep arbitrarily before sending destroy request
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.EventDelay)
err := client.DestroyDroplet(s.dropletId)
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
DIGITALOCEAN_API_URL, s.dropletId, c.ClientID, c.APIKey)
if err != nil {
ui.Error(fmt.Sprintf(
"Error destroying droplet. Please destroy it manually: %v", curlstr))
}
}
@@ -0,0 +1,88 @@
package digitalocean
import (
"cgl.tideland.biz/identifier"
"code.google.com/p/go.crypto/ssh"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepCreateSSHKey struct {
keyId uint
}
func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
ui.Say("Creating temporary ssh key for droplet...")
priv, err := rsa.GenerateKey(rand.Reader, 2014)
// ASN.1 DER encoded form
priv_der := x509.MarshalPKCS1PrivateKey(priv)
priv_blk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: priv_der,
}
// Set the private key in the statebag for later
state["privateKey"] = string(pem.EncodeToMemory(&priv_blk))
// Marshal the public key into SSH compatible format
pub := priv.PublicKey
pub_sshformat := string(ssh.MarshalAuthorizedKey(&pub))
// The name of the public key on DO
name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw()))
// Create the key!
keyId, err := client.CreateKey(name, pub_sshformat)
if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// We use this to check cleanup
s.keyId = keyId
log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future
state["ssh_key_id"] = keyId
return multistep.ActionContinue
}
func (s *stepCreateSSHKey) Cleanup(state map[string]interface{}) {
// If no key name is set, then we never created it, so just return
if s.keyId == 0 {
return
}
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId)
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
DIGITALOCEAN_API_URL, s.keyId, c.ClientID, c.APIKey)
if err != nil {
log.Printf("Error cleaning up ssh key: %v", err.Error())
ui.Error(fmt.Sprintf(
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr))
}
}
+43
View File
@@ -0,0 +1,43 @@
package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
dropletId := state["droplet_id"].(uint)
ui.Say("Waiting for droplet to become active...")
err := waitForDropletState("active", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the IP on the state for later
ip, _, err := client.DropletStatus(dropletId)
if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["droplet_ip"] = ip
return multistep.ActionContinue
}
func (s *stepDropletInfo) Cleanup(state map[string]interface{}) {
// no cleanup
}
+50
View File
@@ -0,0 +1,50 @@
package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepPowerOff struct{}
func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
c := state["config"].(config)
ui := state["ui"].(packer.Ui)
dropletId := state["droplet_id"].(uint)
// Sleep arbitrarily before sending power off request
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.EventDelay)
// Poweroff the droplet so it can be snapshot
err := client.PowerOffDroplet(dropletId)
if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Waiting for droplet to power off...")
err = waitForDropletState("off", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepPowerOff) Cleanup(state map[string]interface{}) {
// no cleanup
}
+25
View File
@@ -0,0 +1,25 @@
package digitalocean
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepProvision struct{}
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
hook := state["hook"].(packer.Hook)
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*stepProvision) Cleanup(map[string]interface{}) {}
+71
View File
@@ -0,0 +1,71 @@
package digitalocean
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepSnapshot struct{}
func (s *stepSnapshot) Run(state map[string]interface{}) multistep.StepAction {
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
dropletId := state["droplet_id"].(uint)
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
err := client.CreateSnapshot(dropletId, c.SnapshotName)
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
images, err := client.Images()
if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
var imageId uint
for _, image := range images {
if image.Name == c.SnapshotName {
imageId = image.Id
break
}
}
if imageId == 0 {
err := errors.New("Couldn't find snapshot to get the image ID. Bug?")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Snapshot image ID: %d", imageId)
state["snapshot_image_id"] = imageId
state["snapshot_name"] = c.SnapshotName
return multistep.ActionContinue
}
func (s *stepSnapshot) Cleanup(state map[string]interface{}) {
// no cleanup
}
+56
View File
@@ -0,0 +1,56 @@
package digitalocean
import (
"errors"
"log"
"time"
)
// waitForState simply blocks until the droplet is in
// a state we expect, while eventually timing out.
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error {
active := make(chan bool, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId)
if err != nil {
log.Println(err)
break
}
if status == desiredState {
break
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
}
active <- true
}()
log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState)
timeout := time.After(c.StateTimeout)
ActiveWaitLoop:
for {
select {
case <-active:
// We connected. Just break the loop.
break ActiveWaitLoop
case <-timeout:
err := errors.New("Timeout while waiting to for droplet to become active")
return err
}
}
// If we got this far, there were no errors
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package virtualbox
import (
"fmt"
"os"
)
// Artifact is the result of running the VirtualBox builder, namely a set
// of files associated with the resulting machine.
type Artifact struct {
dir string
f []string
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (a *Artifact) Files() []string {
return a.f
}
func (*Artifact) Id() string {
return "VM"
}
func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) Destroy() error {
return os.RemoveAll(a.dir)
}
+82 -20
View File
@@ -11,6 +11,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
@@ -24,24 +25,28 @@ type Builder struct {
}
type config struct {
BootCommand []string `mapstructure:"boot_command"`
BootWait time.Duration ``
GuestOSType string `mapstructure:"guest_os_type"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"`
ShutdownCommand string `mapstructure:"shutdown_command"`
ShutdownTimeout time.Duration ``
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHWaitTimeout time.Duration ``
VMName string `mapstructure:"vm_name"`
BootCommand []string `mapstructure:"boot_command"`
BootWait time.Duration ``
DiskSize uint `mapstructure:"disk_size"`
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
GuestOSType string `mapstructure:"guest_os_type"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"`
ShutdownCommand string `mapstructure:"shutdown_command"`
ShutdownTimeout time.Duration ``
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHWaitTimeout time.Duration ``
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
VBoxManage [][]string `mapstructure:"vboxmanage"`
VMName string `mapstructure:"vm_name"`
PackerDebug bool `mapstructure:"packer_debug"`
@@ -60,6 +65,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
}
if b.config.GuestAdditionsPath == "" {
b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
if b.config.GuestOSType == "" {
b.config.GuestOSType = "Other"
}
@@ -88,6 +101,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.SSHPort = 22
}
if b.config.VBoxManage == nil {
b.config.VBoxManage = make([][]string, 0)
}
if b.config.VBoxVersionFile == "" {
b.config.VBoxVersionFile = ".vbox_version"
}
if b.config.VMName == "" {
b.config.VMName = "packer"
}
@@ -116,7 +137,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if url.Scheme == "file" {
if _, err := os.Stat(b.config.ISOUrl); err != nil {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
@@ -143,6 +164,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
}
if b.config.RawBootWait != "" {
b.config.BootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
@@ -190,6 +215,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
steps := []multistep.Step{
new(stepDownloadGuestAdditions),
new(stepDownloadISO),
new(stepPrepareOutputDir),
new(stepHTTPServer),
@@ -198,9 +224,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepCreateDisk),
new(stepAttachISO),
new(stepForwardSSH),
new(stepVBoxManage),
new(stepRun),
new(stepTypeBootCommand),
new(stepWaitForSSH),
new(stepUploadVersion),
new(stepUploadGuestAdditions),
new(stepProvision),
new(stepShutdown),
new(stepExport),
@@ -226,7 +255,40 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
b.runner.Run(state)
return nil, nil
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state[multistep.StateCancelled]; ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := state[multistep.StateHalted]; ok {
return nil, errors.New("Build was halted.")
}
// Compile the artifact list
files := make([]string, 0, 5)
visit := func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, path)
}
return err
}
if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
return nil, err
}
artifact := &Artifact{
dir: b.config.OutputDir,
f: files,
}
return artifact, nil
}
func (b *Builder) Cancel() {
+141
View File
@@ -4,6 +4,7 @@ import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"reflect"
"testing"
)
@@ -75,6 +76,58 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
}
}
func TestBuilderPrepare_DiskSize(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "disk_size")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if b.config.DiskSize != 40000 {
t.Fatalf("bad size: %d", b.config.DiskSize)
}
config["disk_size"] = 60000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.DiskSize != 60000 {
t.Fatalf("bad size: %s", b.config.DiskSize)
}
}
func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "disk_size")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if b.config.GuestAdditionsPath != "VBoxGuestAdditions.iso" {
t.Fatalf("bad: %s", b.config.GuestAdditionsPath)
}
config["guest_additions_path"] = "foo"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.GuestAdditionsPath != "foo" {
t.Fatalf("bad size: %s", b.config.GuestAdditionsPath)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
var b Builder
config := testConfig()
@@ -171,6 +224,31 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
var b Builder
config := testConfig()
// Test with existing dir
dir, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir)
config["output_directory"] = dir
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
var b Builder
config := testConfig()
@@ -253,3 +331,66 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_VBoxManage(t *testing.T) {
var b Builder
config := testConfig()
// Test with empty
delete(config, "vboxmanage")
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(b.config.VBoxManage, [][]string{}) {
t.Fatalf("bad: %#v", b.config.VBoxManage)
}
// Test with a good one
config["vboxmanage"] = [][]interface{}{
[]interface{}{"foo", "bar", "baz"},
}
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := [][]string{
[]string{"foo", "bar", "baz"},
}
if !reflect.DeepEqual(b.config.VBoxManage, expected) {
t.Fatalf("bad: %#v", b.config.VBoxManage)
}
}
func TestBuilderPrepare_VBoxVersionFile(t *testing.T) {
var b Builder
config := testConfig()
// Test empty
delete(config, "virtualbox_version_file")
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.VBoxVersionFile != ".vbox_version" {
t.Fatalf("bad value: %s", b.config.VBoxVersionFile)
}
// Test with a good one
config["virtualbox_version_file"] = "foo"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.VBoxVersionFile != "foo" {
t.Fatalf("bad value: %s", b.config.VBoxVersionFile)
}
}
+37 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os/exec"
"regexp"
"strings"
"time"
)
@@ -29,6 +30,9 @@ type Driver interface {
// properly. If there is any indication the driver can't function,
// this will return an error.
Verify() error
// Version reads the version of VirtualBox that is installed.
Version() (string, error)
}
type VBox42Driver struct {
@@ -49,6 +53,12 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
if line == `VMState="running"` {
return true, nil
}
// We consider "stopping" to still be running. We wait for it to
// be completely stopped or some other state.
if line == `VMState="stopping"` {
return true, nil
}
}
return false, nil
@@ -88,8 +98,15 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("VBoxManage error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}
@@ -97,3 +114,21 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
func (d *VBox42Driver) Verify() error {
return nil
}
func (d *VBox42Driver) Version() (string, error) {
var stdout bytes.Buffer
cmd := exec.Command(d.VBoxManagePath, "--version")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
versionRe := regexp.MustCompile("[^.0-9]")
matches := versionRe.Split(stdout.String(), 2)
if len(matches) == 0 {
return "", fmt.Errorf("No version found: %s", stdout.String())
}
return matches[0], nil
}
+3 -1
View File
@@ -31,7 +31,9 @@ func (s *stepAttachISO) Run(state map[string]interface{}) multistep.StepAction {
"--medium", isoPath,
}
if err := driver.VBoxManage(command...); err != nil {
ui.Error(fmt.Sprintf("Error attaching hard drive: %s", err))
err := fmt.Errorf("Error attaching ISO: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+11 -4
View File
@@ -5,6 +5,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"path/filepath"
"strconv"
"strings"
)
@@ -24,7 +25,7 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
command := []string{
"createhd",
"--filename", path,
"--size", "40000",
"--size", strconv.FormatUint(uint64(config.DiskSize), 10),
"--format", format,
"--variant", "Standard",
}
@@ -32,7 +33,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
ui.Say("Creating hard drive...")
err := driver.VBoxManage(command...)
if err != nil {
ui.Error(fmt.Sprintf("Error creating hard drive: %s", err))
err := fmt.Errorf("Error creating hard drive: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -40,7 +43,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
controllerName := "IDE Controller"
err = driver.VBoxManage("storagectl", vmName, "--name", controllerName, "--add", "ide")
if err != nil {
ui.Error(fmt.Sprintf("Error creating disk controller: %s", err))
err := fmt.Errorf("Error creating disk controller: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -54,7 +59,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
"--medium", path,
}
if err := driver.VBoxManage(command...); err != nil {
ui.Error(fmt.Sprintf("Error attaching hard drive: %s", err))
err := fmt.Errorf("Error attaching hard drive: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+3 -1
View File
@@ -34,7 +34,9 @@ func (s *stepCreateVM) Run(state map[string]interface{}) multistep.StepAction {
for _, command := range commands {
err := driver.VBoxManage(command...)
if err != nil {
ui.Error(fmt.Sprintf("Error creating VM: %s", err))
err := fmt.Errorf("Error creating VM: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -0,0 +1,181 @@
package virtualbox
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"strings"
"time"
)
var additionsVersionMap = map[string]string{
"4.2.1": "4.2.0",
"4.1.23": "4.1.22",
}
// This step uploads a file containing the VirtualBox version, which
// can be useful for various provisioning reasons.
//
// Produces:
// guest_additions_path string - Path to the guest additions.
type stepDownloadGuestAdditions struct{}
func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
var action multistep.StepAction
cache := state["cache"].(packer.Cache)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
version, err := driver.Version()
if err != nil {
state["error"] = fmt.Errorf("Error reading version for guest additions download: %s", err)
return multistep.ActionHalt
}
if newVersion, ok := additionsVersionMap[version]; ok {
log.Printf("Rewriting guest additions version: %s to %s", version, newVersion)
version = newVersion
}
// First things first, we get the list of checksums for the files available
// for this version.
checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", version)
checksumsFile, err := ioutil.TempFile("", "packer")
if err != nil {
state["error"] = fmt.Errorf(
"Failed creating temporary file to store guest addition checksums: %s",
err)
return multistep.ActionHalt
}
checksumsFile.Close()
defer os.Remove(checksumsFile.Name())
downloadConfig := &common.DownloadConfig{
Url: checksumsUrl,
TargetPath: checksumsFile.Name(),
Hash: nil,
}
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
download := common.NewDownloadClient(downloadConfig)
checksumsPath, action := s.progressDownload(download, state)
if action != multistep.ActionContinue {
return action
}
additionsName := fmt.Sprintf("VBoxGuestAdditions_%s.iso", version)
// Next, we find the checksum for the file we're looking to download.
// It is an error if the checksum cannot be found.
checksumsF, err := os.Open(checksumsPath)
if err != nil {
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
return multistep.ActionHalt
}
defer checksumsF.Close()
// We copy the contents of the file into memory. In general this file
// is quite small so that is okay. In the future, we probably want to
// use bufio and iterate line by line.
var contents bytes.Buffer
io.Copy(&contents, checksumsF)
checksum := ""
for _, line := range strings.Split(contents.String(), "\n") {
parts := strings.Fields(line)
log.Printf("Checksum file parts: %#v", parts)
if len(parts) != 2 {
// Bogus line
continue
}
if strings.HasSuffix(parts[1], additionsName) {
checksum = parts[0]
log.Printf("Guest additions checksum: %s", checksum)
break
}
}
if checksum == "" {
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
return multistep.ActionHalt
}
checksumBytes, err := hex.DecodeString(checksum)
if err != nil {
state["error"] = fmt.Errorf("Couldn't decode checksum into bytes: %s", checksum)
return multistep.ActionHalt
}
url := fmt.Sprintf(
"http://download.virtualbox.org/virtualbox/%s/%s",
version, additionsName)
log.Printf("Guest additions URL: %s", url)
log.Printf("Acquiring lock to download the guest additions ISO.")
cachePath := cache.Lock(url)
defer cache.Unlock(url)
downloadConfig = &common.DownloadConfig{
Url: url,
TargetPath: cachePath,
Hash: sha256.New(),
Checksum: checksumBytes,
}
download = common.NewDownloadClient(downloadConfig)
ui.Say("Downloading VirtualBox guest additions. Progress will be shown periodically.")
state["guest_additions_path"], action = s.progressDownload(download, state)
return action
}
func (s *stepDownloadGuestAdditions) Cleanup(state map[string]interface{}) {}
func (s *stepDownloadGuestAdditions) progressDownload(c *common.DownloadClient, state map[string]interface{}) (string, multistep.StepAction) {
ui := state["ui"].(packer.Ui)
var result string
downloadCompleteCh := make(chan error, 1)
// Start a goroutine to actually do the download...
go func() {
var err error
result, err = c.Get()
downloadCompleteCh <- err
}()
progressTicker := time.NewTicker(5 * time.Second)
defer progressTicker.Stop()
// A loop that handles showing progress as well as timing out and handling
// interrupts and all that.
DownloadWaitLoop:
for {
select {
case err := <-downloadCompleteCh:
if err != nil {
state["error"] = fmt.Errorf("Error downloading: %s", err)
return "", multistep.ActionHalt
}
break DownloadWaitLoop
case <-progressTicker.C:
ui.Message(fmt.Sprintf("Download progress: %d%%", c.PercentProgress()))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return "", multistep.ActionHalt
}
}
}
return result, multistep.ActionContinue
}
+51 -7
View File
@@ -7,7 +7,11 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
@@ -20,16 +24,20 @@ import (
//
// Produces:
// iso_path string
type stepDownloadISO struct{}
type stepDownloadISO struct {
isoCopyDir string
}
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
checksum, err := hex.DecodeString(config.ISOMD5)
if err != nil {
ui.Error(fmt.Sprintf("Error parsing checksum: %s", err))
err := fmt.Errorf("Error parsing checksum: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -62,12 +70,15 @@ DownloadWaitLoop:
select {
case err := <-downloadCompleteCh:
if err != nil {
ui.Error(fmt.Sprintf("Error downloading ISO: %s", err))
err := fmt.Errorf("Error downloading ISO: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
break DownloadWaitLoop
case <-progressTicker.C:
ui.Say(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
ui.Message(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
@@ -76,10 +87,43 @@ DownloadWaitLoop:
}
}
// VirtualBox is really dumb and can't figure out that the file is an
// ISO unless it has a ".iso" extension. We can't modify the cache
// filenames so we just do a copy.
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
s.isoCopyDir = tempdir
f, err := os.Create(filepath.Join(tempdir, "image.iso"))
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer f.Close()
sourceF, err := os.Open(cachePath)
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer sourceF.Close()
if _, err := io.Copy(f, sourceF); err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = cachePath
state["iso_path"] = f.Name()
return multistep.ActionContinue
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
func (s *stepDownloadISO) Cleanup(map[string]interface{}) {
if s.isoCopyDir != "" {
os.RemoveAll(s.isoCopyDir)
}
}
+3 -1
View File
@@ -34,7 +34,9 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Exporting virtual machine...")
err := driver.VBoxManage(command...)
if err != nil {
ui.Error(fmt.Sprintf("Error exporting virtual machine: %s", err))
err := fmt.Errorf("Error exporting virtual machine: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+3 -1
View File
@@ -44,7 +44,9 @@ func (s *stepForwardSSH) Run(state map[string]interface{}) multistep.StepAction
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, config.SSHPort),
}
if err := driver.VBoxManage(command...); err != nil {
ui.Error(fmt.Sprintf("Error creating port forwarding rule: %s", err))
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+14 -1
View File
@@ -2,6 +2,7 @@ package virtualbox
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
@@ -11,10 +12,22 @@ func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepActi
config := state["config"].(*config)
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepPrepareOutputDir) Cleanup(map[string]interface{}) {}
func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
_, cancelled := state[multistep.StateCancelled]
_, halted := state[multistep.StateHalted]
if cancelled || halted {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
ui.Say("Deleting output directory...")
os.RemoveAll(config.OutputDir)
}
}
+4 -1
View File
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
hook.Run(packer.HookProvision, ui, comm, nil)
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
+8 -3
View File
@@ -25,7 +25,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Starting the virtual machine...")
command := []string{"startvm", vmName, "--type", "gui"}
if err := driver.VBoxManage(command...); err != nil {
ui.Error(fmt.Sprintf("Error starting VM: %s", err))
err := fmt.Errorf("Error starting VM: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -46,7 +48,10 @@ func (s *stepRun) Cleanup(state map[string]interface{}) {
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
if err := driver.VBoxManage("controlvm", s.vmName, "poweroff"); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
if running, _ := driver.IsRunning(s.vmName); running {
if err := driver.VBoxManage("controlvm", s.vmName, "poweroff"); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
}
}
}
+10 -3
View File
@@ -1,6 +1,7 @@
package virtualbox
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
@@ -34,7 +35,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
ui.Error(fmt.Sprintf("Failed to send shutdown command: %s", err))
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -52,7 +55,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
select {
case <-shutdownTimer:
ui.Error("Timeout while waiting for machine to shut down.")
err := errors.New("Timeout while waiting for machine to shut down.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
default:
time.Sleep(1 * time.Second)
@@ -61,7 +66,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
} else {
ui.Say("Halting the virtual machine...")
if err := driver.Stop(vmName); err != nil {
ui.Error(fmt.Sprintf("Error stopping VM: %s", err))
err := fmt.Errorf("Error stopping VM: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ func (stepSuppressMessages) Run(state map[string]interface{}) multistep.StepActi
log.Println("Suppressing annoying messages in VirtualBox")
if err := driver.SuppressMessages(); err != nil {
ui.Error(fmt.Sprintf("Error configuring VirtualBox to suppress messages: %s", err))
err := fmt.Errorf("Error configuring VirtualBox to suppress messages: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+5 -2
View File
@@ -66,7 +66,9 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
}
if err := driver.VBoxManage("controlvm", vmName, "keyboardputscancode", code); err != nil {
ui.Error(fmt.Sprintf("Error sending boot command: %s", err))
err := fmt.Errorf("Error sending boot command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
@@ -80,8 +82,9 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func scancodes(message string) []string {
special := make(map[string][]string)
special["<enter>"] = []string{"1c", "9c"}
special["<return>"] = []string{"1c", "9c"}
special["<esc>"] = []string{"01", "81"}
special["<return>"] = []string{"1c", "9c"}
special["<tab>"] = []string{"0f", "8f"}
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
@@ -0,0 +1,55 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
"text/template"
)
type guestAdditionsPathTemplate struct {
Version string
}
// This step uploads the guest additions ISO to the VM.
type stepUploadGuestAdditions struct{}
func (s *stepUploadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*config)
driver := state["driver"].(Driver)
guestAdditionsPath := state["guest_additions_path"].(string)
ui := state["ui"].(packer.Ui)
version, err := driver.Version()
if err != nil {
state["error"] = fmt.Errorf("Error reading version for guest additions upload: %s", err)
return multistep.ActionHalt
}
f, err := os.Open(guestAdditionsPath)
if err != nil {
state["error"] = fmt.Errorf("Error opening guest additions ISO: %s", err)
return multistep.ActionHalt
}
tplData := &guestAdditionsPathTemplate{
Version: version,
}
var processedPath bytes.Buffer
t := template.Must(template.New("path").Parse(config.GuestAdditionsPath))
t.Execute(&processedPath, tplData)
ui.Say("Uploading VirtualBox guest additions ISO...")
if err := comm.Upload(processedPath.String(), f); err != nil {
state["error"] = fmt.Errorf("Error uploading guest additions: %s", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepUploadGuestAdditions) Cleanup(state map[string]interface{}) {}
+43
View File
@@ -0,0 +1,43 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// This step uploads a file containing the VirtualBox version, which
// can be useful for various provisioning reasons.
type stepUploadVersion struct{}
func (s *stepUploadVersion) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*config)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
if config.VBoxVersionFile == "" {
log.Println("VBoxVersionFile is empty. Not uploading.")
return multistep.ActionContinue
}
version, err := driver.Version()
if err != nil {
state["error"] = fmt.Errorf("Error reading version for metadata upload: %s", err)
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Uploading VirtualBox version info (%s)", version))
var data bytes.Buffer
data.WriteString(version)
if err := comm.Upload(config.VBoxVersionFile, &data); err != nil {
state["error"] = fmt.Errorf("Error uploading VirtualBox version: %s", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepUploadVersion) Cleanup(state map[string]interface{}) {}
+61
View File
@@ -0,0 +1,61 @@
package virtualbox
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strings"
"text/template"
)
type commandTemplate struct {
Name string
}
// This step executes additional VBoxManage commands as specified by the
// template.
//
// Uses:
//
// Produces:
type stepVBoxManage struct{}
func (s *stepVBoxManage) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
vmName := state["vmName"].(string)
if len(config.VBoxManage) > 0 {
ui.Say("Executing custom VBoxManage commands...")
}
tplData := &commandTemplate{
Name: vmName,
}
for _, originalCommand := range config.VBoxManage {
command := make([]string, len(originalCommand))
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()
}
ui.Message(fmt.Sprintf("Executing: %s", strings.Join(command, " ")))
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error executing command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepVBoxManage) Cleanup(state map[string]interface{}) {}
+8 -1
View File
@@ -1,6 +1,9 @@
package vmware
import "fmt"
import (
"fmt"
"os"
)
// Artifact is the result of running the VMware builder, namely a set
// of files associated with the resulting machine.
@@ -24,3 +27,7 @@ func (*Artifact) Id() string {
func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) Destroy() error {
return os.RemoveAll(a.dir)
}
+23 -3
View File
@@ -26,6 +26,7 @@ type Builder struct {
type config struct {
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
GuestOSType string `mapstructure:"guest_os_type"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"`
@@ -40,6 +41,7 @@ type config struct {
ShutdownTimeout time.Duration ``
SSHUser string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHWaitTimeout time.Duration ``
VMXData map[string]string `mapstructure:"vmx_data"`
VNCPortMin uint `mapstructure:"vnc_port_min"`
@@ -64,6 +66,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.DiskName = "disk"
}
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
}
if b.config.GuestOSType == "" {
b.config.GuestOSType = "other"
}
@@ -92,6 +98,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.OutputDir = "vmware"
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
// Accumulate any errors
var err error
errs := make([]error, 0)
@@ -118,7 +128,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if url.Scheme == "file" {
if _, err := os.Stat(b.config.ISOUrl); err != nil {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
@@ -145,6 +155,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
}
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
@@ -225,15 +239,21 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state[multistep.StateCancelled]; ok {
return nil, nil
return nil, errors.New("Build was cancelled.")
}
if _, ok := state[multistep.StateHalted]; ok {
return nil, nil
return nil, errors.New("Build was halted.")
}
// Compile the artifact list
+79
View File
@@ -68,6 +68,32 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
}
}
func TestBuilderPrepare_DiskSize(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "disk_size")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if b.config.DiskSize != 40000 {
t.Fatalf("bad size: %d", b.config.DiskSize)
}
config["disk_size"] = 60000
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.DiskSize != 60000 {
t.Fatalf("bad size: %s", b.config.DiskSize)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
var b Builder
config := testConfig()
@@ -164,6 +190,31 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
var b Builder
config := testConfig()
// Test with existing dir
dir, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir)
config["output_directory"] = dir
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
var b Builder
config := testConfig()
@@ -200,6 +251,34 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
}
}
func TestBuilderPrepare_SSHPort(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
delete(config, "ssh_port")
err := b.Prepare(config)
if err != nil {
t.Fatalf("bad err: %s", err)
}
if b.config.SSHPort != 22 {
t.Fatalf("bad ssh port: %d", b.config.SSHPort)
}
// Test with a good one
config["ssh_port"] = 44
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 44 {
t.Fatalf("bad ssh port: %d", b.config.SSHPort)
}
}
func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
var b Builder
config := testConfig()
+21 -7
View File
@@ -3,6 +3,7 @@ package vmware
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
@@ -38,7 +39,7 @@ type Fusion5Driver struct {
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if err := cmd.Run(); err != nil {
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
@@ -51,14 +52,13 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
return false, err
}
stdout := new(bytes.Buffer)
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout.String(), "\n") {
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
@@ -69,7 +69,7 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
func (d *Fusion5Driver) Start(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, "gui")
if err := cmd.Run(); err != nil {
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
@@ -78,7 +78,7 @@ func (d *Fusion5Driver) Start(vmxPath string) error {
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if err := cmd.Run(); err != nil {
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
@@ -120,3 +120,17 @@ func (d *Fusion5Driver) vdiskManagerPath() string {
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
return stdout.String(), stderr.String(), err
}
+9 -3
View File
@@ -29,13 +29,17 @@ func (stepConfigureVNC) Run(state map[string]interface{}) multistep.StepAction {
f, err := os.Open(vmxPath)
if err != nil {
ui.Error(fmt.Sprintf("Error while reading VMX data: %s", err))
err := fmt.Errorf("Error reading VMX data: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
ui.Error(fmt.Sprintf("Error reading VMX data: %s", err))
err := fmt.Errorf("Error reading VMX data: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -62,7 +66,9 @@ func (stepConfigureVNC) Run(state map[string]interface{}) multistep.StepAction {
vmxData["RemoteDisplay.vnc.port"] = fmt.Sprintf("%d", vncPort)
if err := WriteVMX(vmxPath, vmxData); err != nil {
ui.Error(fmt.Sprintf("Error writing VMX data: %s", err))
err := fmt.Errorf("Error writing VMX data: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+4 -5
View File
@@ -19,17 +19,16 @@ import (
type stepCreateDisk struct{}
func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
// TODO(mitchellh): Configurable disk size
// TODO(mitchellh): Capture error output in case things go wrong to report it
config := state["config"].(*config)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
ui.Say("Creating virtual machine disk")
output := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
if err := driver.CreateDisk(output, "40000M"); err != nil {
ui.Error(fmt.Sprintf("Error creating VMware disk: %s", err))
if err := driver.CreateDisk(output, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
err := fmt.Errorf("Error creating disk: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+3 -1
View File
@@ -57,7 +57,9 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
vmxPath := filepath.Join(config.OutputDir, config.VMName+".vmx")
if err := WriteVMX(vmxPath, vmxData); err != nil {
ui.Error(fmt.Sprintf("Error creating VMX: %s", err))
err := fmt.Errorf("Error creating VMX file: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+7 -2
View File
@@ -29,7 +29,9 @@ func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction
checksum, err := hex.DecodeString(config.ISOMD5)
if err != nil {
ui.Error(fmt.Sprintf("Error parsing checksum: %s", err))
err := fmt.Errorf("Error parsing checksum: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -62,7 +64,10 @@ DownloadWaitLoop:
select {
case err := <-downloadCompleteCh:
if err != nil {
ui.Error(fmt.Sprintf("Error downloading ISO: %s", err))
err := fmt.Errorf("Error downloading ISO: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
break DownloadWaitLoop
+14 -1
View File
@@ -2,6 +2,7 @@ package vmware
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
@@ -11,10 +12,22 @@ func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepActi
config := state["config"].(*config)
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepPrepareOutputDir) Cleanup(map[string]interface{}) {}
func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
_, cancelled := state[multistep.StateCancelled]
_, halted := state[multistep.StateHalted]
if cancelled || halted {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
ui.Say("Deleting output directory...")
os.RemoveAll(config.OutputDir)
}
}
+4 -1
View File
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
ui := state["ui"].(packer.Ui)
log.Println("Running the provision hook")
hook.Run(packer.HookProvision, ui, comm, nil)
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state["error"] = err
return multistep.ActionHalt
}
return multistep.ActionContinue
}
+3 -1
View File
@@ -34,7 +34,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Starting virtual machine...")
if err := driver.Start(vmxPath); err != nil {
ui.Error(fmt.Sprintf("Error starting VM: %s", err))
err := fmt.Errorf("Error starting VM: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
+10 -3
View File
@@ -1,6 +1,7 @@
package vmware
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
@@ -34,7 +35,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
ui.Error(fmt.Sprintf("Failed to send shutdown command: %s", err))
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -52,7 +55,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
select {
case <-shutdownTimer:
ui.Error("Timeout while waiting for machine to shut down.")
err := errors.New("Timeout while waiting for machine to shut down.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
default:
time.Sleep(1 * time.Second)
@@ -60,7 +65,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
}
} else {
if err := driver.Stop(vmxPath); err != nil {
ui.Error(fmt.Sprintf("Error stopping VM: %s", err))
err := fmt.Errorf("Error stopping VM: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
+11 -4
View File
@@ -45,14 +45,18 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
ui.Say("Connecting to VM via VNC")
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", vncPort))
if err != nil {
ui.Error(fmt.Sprintf("Error connecting to VNC: %s", err))
err := fmt.Errorf("Error connecting to VNC: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
defer nc.Close()
c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: true})
if err != nil {
ui.Error(fmt.Sprintf("Error handshaking with VNC: %s", err))
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
defer c.Close()
@@ -63,7 +67,9 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
ipFinder := &IfconfigIPFinder{"vmnet8"}
hostIp, err := ipFinder.HostIP()
if err != nil {
ui.Error(fmt.Sprintf("Error detecting host IP: %s", err))
err := fmt.Errorf("Error detecting host IP: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -90,8 +96,9 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func vncSendString(c *vnc.ClientConn, original string) {
special := make(map[string]uint32)
special["<enter>"] = 0xFF0D
special["<return>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
+11 -1
View File
@@ -113,6 +113,8 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
handshakeAttempts := 0
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
var nc net.Conn
@@ -144,7 +146,7 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
log.Printf("Detected IP: %s", ip)
// Attempt to connect to SSH port
nc, err = net.Dial("tcp", fmt.Sprintf("%s:22", ip))
nc, err = net.Dial("tcp", fmt.Sprintf("%s:%d", ip, config.SSHPort))
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
@@ -160,6 +162,14 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
comm, err = ssh.New(nc, sshConfig)
if err != nil {
log.Printf("SSH handshake err: %s", err)
handshakeAttempts += 1
if handshakeAttempts < 10 {
// Try to connect via SSH a handful of times
continue
}
return nil, err
}
+38 -30
View File
@@ -63,9 +63,10 @@ func (c Command) Run(env packer.Environment, args []string) int {
// The component finder for our builds
components := &packer.ComponentFinder{
Builder: env.Builder,
Hook: env.Hook,
Provisioner: env.Provisioner,
Builder: env.Builder,
Hook: env.Hook,
PostProcessor: env.PostProcessor,
Provisioner: env.Provisioner,
}
// Go through each builder and compile the builds that we care about
@@ -127,19 +128,11 @@ func (c Command) Run(env packer.Environment, args []string) int {
buildUis := make(map[string]packer.Ui)
for i, b := range builds {
var ui packer.Ui
ui = &packer.ColoredUi{
ui := &packer.ColoredUi{
Color: colors[i%len(colors)],
Ui: env.Ui(),
}
ui = &packer.PrefixedUi{
fmt.Sprintf("==> %s", b.Name()),
fmt.Sprintf(" %s", b.Name()),
ui,
}
buildUis[b.Name()] = ui
ui.Say(fmt.Sprintf("%s output will be in this color.", b.Name()))
}
@@ -163,7 +156,8 @@ func (c Command) Run(env packer.Environment, args []string) int {
// Run all the builds in parallel and wait for them to complete
var interruptWg, wg sync.WaitGroup
interrupted := false
artifacts := make(map[string]packer.Artifact)
artifacts := make(map[string][]packer.Artifact)
errors := make(map[string]error)
for _, b := range builds {
// Increment the waitgroup so we wait for this item to finish properly
wg.Add(1)
@@ -187,15 +181,17 @@ func (c Command) Run(env packer.Environment, args []string) int {
go func(b packer.Build) {
defer wg.Done()
var err error
log.Printf("Starting build run: %s", b.Name())
ui := buildUis[b.Name()]
artifacts[b.Name()], err = b.Run(ui, env.Cache())
name := b.Name()
log.Printf("Starting build run: %s", name)
ui := buildUis[name]
runArtifacts, err := b.Run(ui, env.Cache())
if err != nil {
ui.Error(fmt.Sprintf("Build errored: %s", err))
ui.Error(fmt.Sprintf("Build '%s' errored: %s", name, err))
errors[name] = err
} else {
ui.Say("Build finished.")
ui.Say(fmt.Sprintf("Build '%s' finished.", name))
artifacts[name] = runArtifacts
}
}(b)
@@ -223,19 +219,31 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 1
}
// Output all the artifacts
env.Ui().Say("\n==> The build completed! The artifacts created were:")
for name, artifact := range artifacts {
var message bytes.Buffer
fmt.Fprintf(&message, "--> %s: ", name)
if artifact != nil {
fmt.Fprintf(&message, artifact.String())
} else {
fmt.Print("<nothing>")
if len(errors) > 0 {
env.Ui().Error("\n==> Some builds didn't complete successfully and had errors:")
for name, err := range errors {
env.Ui().Error(fmt.Sprintf("--> %s: %s", name, err))
}
}
env.Ui().Say(message.String())
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 {
var message bytes.Buffer
fmt.Fprintf(&message, "--> %s: ", name)
if artifact != nil {
fmt.Fprintf(&message, artifact.String())
} else {
fmt.Fprint(&message, "<nothing>")
}
env.Ui().Say(message.String())
}
}
} else {
env.Ui().Say("\n==> Builds finished but no artifacts were created.")
}
return 0
@@ -56,15 +56,17 @@ func (c Command) Run(env packer.Environment, args []string) int {
// The component finder for our builds
components := &packer.ComponentFinder{
Builder: env.Builder,
Hook: env.Hook,
Provisioner: env.Provisioner,
Builder: env.Builder,
Hook: env.Hook,
PostProcessor: env.PostProcessor,
Provisioner: env.Provisioner,
}
// 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))
@@ -76,6 +78,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
// Check the configuration of all builds
for _, b := range builds {
log.Printf("Preparing build: %s", b.Name())
err := b.Prepare()
if err != nil {
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
+11 -1
View File
@@ -34,6 +34,17 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
return
}
log.Printf("starting remote command: %s", cmd.Command)
err = session.Start(cmd.Command + "\n")
if err != nil {
@@ -147,5 +158,4 @@ func (c *comm) Upload(path string, input io.Reader) error {
func (c *comm) Download(string, io.Writer) error {
panic("not implemented yet")
return nil
}
+1 -50
View File
@@ -3,12 +3,9 @@ package ssh
import (
"bytes"
"code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/packer"
"net"
"strings"
"testing"
"time"
)
// private key for mock server
@@ -98,38 +95,6 @@ func newMockLineServer(t *testing.T) string {
channel.Accept()
t.Log("Accepted channel")
defer channel.Close()
data := make([]byte, 0)
_, err = channel.Read(data)
if err == nil {
t.Error("should've gotten a request (exec)")
return
}
req, ok := err.(ssh.ChannelRequest)
if !ok {
t.Errorf("couldn't convert err to channel request: %s", err)
return
}
if req.Request != "exec" {
t.Errorf("unexpected request type: %s", req.Request)
return
}
// Ack it
channel.AckRequest(true)
// Just respond back with the payload. We trim the first 4 bytes
// off of here because it is "\x00\x00\x00\t" and I don't really know
// why.
payload := strings.TrimSpace(string(req.Payload[4:]))
response := fmt.Sprintf("ack: %s", payload)
_, err = channel.Write([]byte(response))
if err != nil {
t.Errorf("error writing response: %s", err)
return
}
}()
return l.Addr().String()
}
@@ -184,19 +149,5 @@ func TestStart(t *testing.T) {
cmd.Command = "echo foo"
cmd.Stdout = stdout
err = client.Start(&cmd)
if err != nil {
t.Fatalf("error executing command: %s", err)
}
// Wait for it to complete
t.Log("Waiting for command to complete")
for !cmd.Exited {
time.Sleep(50 * time.Millisecond)
}
// Should have the correct output
if stdout.String() != "ack: echo foo" {
t.Fatalf("unknown output: %#v", stdout.String())
}
client.Start(&cmd)
}
+49 -4
View File
@@ -2,11 +2,13 @@ package main
import (
"encoding/json"
"github.com/mitchellh/osext"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/packer/plugin"
"io"
"log"
"os/exec"
"path/filepath"
)
// This is the default, built-in configuration that ships with
@@ -18,6 +20,7 @@ const defaultConfig = `
"builders": {
"amazon-ebs": "packer-builder-amazon-ebs",
"digitalocean": "packer-builder-digitalocean",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
},
@@ -27,6 +30,10 @@ const defaultConfig = `
"validate": "packer-command-validate"
},
"post-processors": {
"vagrant": "packer-post-processor-vagrant"
},
"provisioners": {
"shell": "packer-provisioner-shell"
}
@@ -37,9 +44,10 @@ type config struct {
PluginMinPort uint
PluginMaxPort uint
Builders map[string]string
Commands map[string]string
Provisioners map[string]string
Builders map[string]string
Commands map[string]string
PostProcessors map[string]string `json:"post-processors"`
Provisioners map[string]string
}
// Decodes configuration in JSON format from the given io.Reader into
@@ -52,7 +60,7 @@ func decodeConfig(r io.Reader, c *config) error {
// Returns an array of defined command names.
func (c *config) CommandNames() (result []string) {
result = make([]string, 0, len(c.Commands))
for name, _ := range c.Commands {
for name := range c.Commands {
result = append(result, name)
}
return
@@ -91,6 +99,19 @@ func (c *config) LoadHook(name string) (packer.Hook, error) {
return c.pluginClient(name).Hook()
}
// This is a proper packer.PostProcessorFunc that can be used to load
// packer.PostProcessor implementations from defined plugins.
func (c *config) LoadPostProcessor(name string) (packer.PostProcessor, error) {
log.Printf("Loading post-processor: %s", name)
bin, ok := c.PostProcessors[name]
if !ok {
log.Printf("Post-processor not found: %s", name)
return nil, nil
}
return c.pluginClient(bin).PostProcessor()
}
// This is a proper packer.ProvisionerFunc that can be used to load
// packer.Provisioner implementations from defined plugins.
func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
@@ -105,6 +126,30 @@ func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
}
func (c *config) pluginClient(path string) *plugin.Client {
originalPath := path
// First attempt to find the executable by consulting the PATH.
path, err := exec.LookPath(path)
if err != nil {
// If that doesn't work, look for it in the same directory
// as the `packer` executable (us).
log.Printf("Plugin could not be found. Checking same directory as executable.")
exePath, err := osext.Executable()
if err != nil {
log.Printf("Couldn't get current exe path: %s", err)
} else {
log.Printf("Current exe path: %s", exePath)
path = filepath.Join(filepath.Dir(exePath), filepath.Base(originalPath))
}
}
// If everything failed, just use the original path and let the error
// bubble through.
if path == "" {
path = originalPath
}
log.Printf("Creating plugin client for path: %s", path)
var config plugin.ClientConfig
config.Cmd = exec.Command(path)
config.Managed = true
+9
View File
@@ -0,0 +1,9 @@
package main
// ConfigFile returns the default path to the configuration file. On
// Unix-like systems this is the ".packerconfig" file in the home directory.
// On Windows, this is the "packer.config" file in the application data
// directory.
func ConfigFile() (string, error) {
return configFile()
}
+45
View File
@@ -0,0 +1,45 @@
// +build darwin freebsd linux netbsd openbsd
package main
import (
"bytes"
"errors"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
func configFile() (string, error) {
dir, err := configDir()
if err != nil {
return "", err
}
return filepath.Join(dir, ".packerconfig"), nil
}
func configDir() (string, error) {
// First prefer the HOME environmental variable
if home := os.Getenv("HOME"); home != "" {
log.Printf("Detected home directory from env var: %s", home)
return home, nil
}
// If that fails, try the shell
var stdout bytes.Buffer
cmd := exec.Command("sh", "-c", "eval echo ~$USER")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output")
}
return result, nil
}
+37
View File
@@ -0,0 +1,37 @@
// +build windows
package main
import (
"path/filepath"
"syscall"
"unsafe"
)
var (
shell = syscall.MustLoadDLL("Shell32.dll")
getFolderPath = shell.MustFindProc("SHGetFolderPathW")
)
const CSIDL_APPDATA = 26
func configFile() (string, error) {
dir, err := configDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "packer.config"), nil
}
func configDir() (string, error) {
b := make([]uint16, syscall.MAX_PATH)
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181(v=vs.85).aspx
r, _, err := getFolderPath.Call(0, CSIDL_APPDATA, 0, 0, uintptr(unsafe.Pointer(&b[0])))
if uint32(r) != 0 {
return "", err
}
return syscall.UTF16ToString(b), nil
}
+34 -22
View File
@@ -9,7 +9,6 @@ import (
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"runtime"
)
@@ -36,25 +35,34 @@ func main() {
log.Printf("Packer config: %+v", config)
defer plugin.CleanupClients()
var cache packer.Cache
if cacheDir := os.Getenv("PACKER_CACHE_DIR"); cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
os.Exit(1)
}
log.Printf("Setting cache directory: %s", cacheDir)
cache = &packer.FileCache{CacheDir: cacheDir}
cacheDir := os.Getenv("PACKER_CACHE_DIR")
if cacheDir == "" {
cacheDir = "packer_cache"
}
cacheDir, err = filepath.Abs(cacheDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
os.Exit(1)
}
if err := os.MkdirAll(cacheDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
os.Exit(1)
}
log.Printf("Setting cache directory: %s", cacheDir)
cache := &packer.FileCache{CacheDir: cacheDir}
defer plugin.CleanupClients()
envConfig := packer.DefaultEnvironmentConfig()
envConfig.Cache = cache
envConfig.Commands = config.CommandNames()
envConfig.Components.Builder = config.LoadBuilder
envConfig.Components.Command = config.LoadCommand
envConfig.Components.Hook = config.LoadHook
envConfig.Components.PostProcessor = config.LoadPostProcessor
envConfig.Components.Provisioner = config.LoadProvisioner
env, err := packer.NewEnvironment(envConfig)
@@ -82,19 +90,23 @@ func loadConfig() (*config, error) {
}
mustExist := true
configFile := os.Getenv("PACKER_CONFIG")
if configFile == "" {
u, err := user.Current()
if err != nil {
return nil, err
}
configFile = filepath.Join(u.HomeDir, ".packerconfig")
configFilePath := os.Getenv("PACKER_CONFIG")
if configFilePath == "" {
var err error
configFilePath, err = configFile()
mustExist = false
if err != nil {
log.Printf("Error detecing default config file path: %s", err)
}
}
log.Printf("Attempting to open config file: %s", configFile)
f, err := os.Open(configFile)
if configFilePath == "" {
return &config, nil
}
log.Printf("Attempting to open config file: %s", configFilePath)
f, err := os.Open(configFilePath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
+5
View File
@@ -24,4 +24,9 @@ type Artifact interface {
// Returns human-readable output that describes the artifact created.
// This is used for UI output. It can be multiple lines.
String() string
// Destroy deletes the artifact. Packer calls this for various reasons,
// such as if a post-processor has processed this artifact and it is
// no longer needed.
Destroy() error
}
+32
View File
@@ -0,0 +1,32 @@
package packer
type TestArtifact struct {
id string
destroyCalled bool
}
func (*TestArtifact) BuilderId() string {
return "bid"
}
func (*TestArtifact) Files() []string {
return []string{"a", "b"}
}
func (a *TestArtifact) Id() string {
id := a.id
if id == "" {
id = "id"
}
return id
}
func (*TestArtifact) String() string {
return "string"
}
func (a *TestArtifact) Destroy() error {
a.destroyCalled = true
return nil
}
+135 -10
View File
@@ -1,6 +1,10 @@
package packer
import "log"
import (
"fmt"
"log"
"sync"
)
// This is the key in configurations that is set to "true" when Packer
// debugging is enabled.
@@ -20,7 +24,7 @@ type Build interface {
// Run runs the actual builder, returning an artifact implementation
// of what is built. If anything goes wrong, an error is returned.
Run(Ui, Cache) (Artifact, error)
Run(Ui, Cache) ([]Artifact, error)
// Cancel will cancel a running build. This will block until the build
// is actually completely cancelled.
@@ -41,16 +45,28 @@ type Build interface {
// multiple files, of course, but it should be for only a single provider
// (such as VirtualBox, EC2, etc.).
type coreBuild struct {
name string
builder Builder
builderConfig interface{}
hooks map[string][]Hook
provisioners []coreBuildProvisioner
name string
builder Builder
builderConfig interface{}
builderType string
hooks map[string][]Hook
postProcessors [][]coreBuildPostProcessor
provisioners []coreBuildProvisioner
debug bool
l sync.Mutex
prepareCalled bool
}
// Keeps track of the post-processor and the configuration of the
// post-processor used within a build.
type coreBuildPostProcessor struct {
processor PostProcessor
processorType string
config interface{}
keepInputArtifact bool
}
// Keeps track of the provisioner and the configuration of the provisioner
// within the build.
type coreBuildProvisioner struct {
@@ -66,7 +82,13 @@ 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) {
// TODO: lock
b.l.Lock()
defer b.l.Unlock()
if b.prepareCalled {
panic("prepare already called")
}
b.prepareCalled = true
debugConfig := map[string]interface{}{
@@ -91,11 +113,20 @@ func (b *coreBuild) Prepare() (err error) {
}
}
// Prepare the post-processors
for _, ppSeq := range b.postProcessors {
for _, corePP := range ppSeq {
if err = corePP.processor.Configure(corePP.config); err != nil {
return
}
}
}
return
}
// Runs the actual build. Prepare must be called prior to running this.
func (b *coreBuild) Run(ui Ui, cache Cache) (Artifact, error) {
func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
if !b.prepareCalled {
panic("Prepare must be called first")
}
@@ -122,7 +153,101 @@ func (b *coreBuild) Run(ui Ui, cache Cache) (Artifact, error) {
}
hook := &DispatchHook{hooks}
return b.builder.Run(ui, hook, cache)
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,
}
log.Printf("Running builder: %s", b.builderType)
builderArtifact, err := b.builder.Run(builderUi, hook, cache)
if err != nil {
return nil, err
}
// If there was no result, don't worry about running post-processors
// because there is nothing they can do, just return.
if builderArtifact == nil {
return nil, nil
}
errors := make([]error, 0)
keepOriginalArtifact := len(b.postProcessors) == 0
// Run the post-processors
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,
}
builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType))
artifact, err := corePP.processor.PostProcess(ppUi, priorArtifact)
if err != nil {
errors = append(errors, fmt.Errorf("Post-processor failed: %s", err))
continue PostProcessorRunSeqLoop
}
if artifact == nil {
log.Println("Nil artifact, halting post-processor chain.")
continue PostProcessorRunSeqLoop
}
if i == 0 {
// This is the first post-processor. We handle deleting
// previous artifacts a bit different because multiple
// post-processors may be using the original and need it.
if !keepOriginalArtifact && corePP.keepInputArtifact {
log.Printf(
"Flagging to keep original artifact from post-processor '%s'",
corePP.processorType)
keepOriginalArtifact = true
}
} else {
// We have a prior artifact. If we want to keep it, we append
// it to the results list. Otherwise, we destroy it.
if corePP.keepInputArtifact {
artifacts = append(artifacts, priorArtifact)
} else {
log.Printf("Deleting prior artifact from post-processor '%s'", corePP.processorType)
if err := priorArtifact.Destroy(); err != nil {
errors = append(errors, fmt.Errorf("Failed cleaning up prior artifact: %s", err))
}
}
}
priorArtifact = artifact
}
// Add on the last artifact to the results
if priorArtifact != nil {
artifacts = append(artifacts, priorArtifact)
}
}
if keepOriginalArtifact {
artifacts = append(artifacts, nil)
copy(artifacts[1:], artifacts)
artifacts[0] = builderArtifact
} else {
log.Printf("Deleting original artifact for build '%s'", b.name)
if err := builderArtifact.Destroy(); err != nil {
errors = append(errors, fmt.Errorf("Error destroying builder artifact: %s", err))
}
}
if len(errors) > 0 {
err = &MultiError{errors}
}
return artifacts, err
}
func (b *coreBuild) SetDebug(val bool) {
+157 -18
View File
@@ -2,13 +2,14 @@ package packer
import (
"cgl.tideland.biz/asserts"
"reflect"
"testing"
)
func testBuild() Build {
func testBuild() *coreBuild {
return &coreBuild{
name: "test",
builder: &TestBuilder{},
builder: &TestBuilder{artifactId: "b"},
builderConfig: 42,
hooks: map[string][]Hook{
"foo": []Hook{&TestHook{}},
@@ -16,6 +17,11 @@ func testBuild() Build {
provisioners: []coreBuildProvisioner{
coreBuildProvisioner{&TestProvisioner{}, []interface{}{42}},
},
postProcessors: [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
},
},
}
}
@@ -36,17 +42,41 @@ func TestBuild_Prepare(t *testing.T) {
debugFalseConfig := map[string]interface{}{DebugConfigKey: false}
build := testBuild()
coreB := build.(*coreBuild)
builder := coreB.builder.(*TestBuilder)
builder := build.builder.(*TestBuilder)
build.Prepare()
assert.True(builder.prepareCalled, "prepare should be called")
assert.Equal(builder.prepareConfig, []interface{}{42, debugFalseConfig}, "prepare config should be 42")
coreProv := coreB.provisioners[0]
coreProv := build.provisioners[0]
prov := coreProv.provisioner.(*TestProvisioner)
assert.True(prov.prepCalled, "prepare should be called")
assert.Equal(prov.prepConfigs, []interface{}{42, debugFalseConfig}, "prepare should be called with proper config")
corePP := build.postProcessors[0][0]
pp := corePP.processor.(*TestPostProcessor)
assert.True(pp.configCalled, "config should be called")
assert.Equal(pp.configVal, 42, "config should have right value")
}
func TestBuild_Prepare_Twice(t *testing.T) {
build := testBuild()
if err := build.Prepare(); err != nil {
t.Fatalf("bad error: %s", err)
}
defer func() {
p := recover()
if p == nil {
t.Fatalf("should've paniced")
}
if p.(string) != "prepare already called" {
t.Fatalf("Invalid panic: %s", p)
}
}()
build.Prepare()
}
func TestBuild_Prepare_Debug(t *testing.T) {
@@ -55,15 +85,14 @@ func TestBuild_Prepare_Debug(t *testing.T) {
debugConfig := map[string]interface{}{DebugConfigKey: true}
build := testBuild()
coreB := build.(*coreBuild)
builder := coreB.builder.(*TestBuilder)
builder := build.builder.(*TestBuilder)
build.SetDebug(true)
build.Prepare()
assert.True(builder.prepareCalled, "prepare should be called")
assert.Equal(builder.prepareConfig, []interface{}{42, debugConfig}, "prepare config should be 42")
coreProv := coreB.provisioners[0]
coreProv := build.provisioners[0]
prov := coreProv.provisioner.(*TestProvisioner)
assert.True(prov.prepCalled, "prepare should be called")
assert.Equal(prov.prepConfigs, []interface{}{42, debugConfig}, "prepare should be called with proper config")
@@ -77,27 +106,139 @@ func TestBuild_Run(t *testing.T) {
build := testBuild()
build.Prepare()
build.Run(ui, cache)
coreB := build.(*coreBuild)
artifacts, err := build.Run(ui, cache)
assert.Nil(err, "should not error")
assert.Equal(len(artifacts), 2, "should have two artifacts")
// Verify builder was run
builder := coreB.builder.(*TestBuilder)
builder := build.builder.(*TestBuilder)
assert.True(builder.runCalled, "run should be called")
assert.Equal(builder.runUi, ui, "run should be called with ui")
// Verify hooks are disapatchable
dispatchHook := builder.runHook
dispatchHook.Run("foo", nil, nil, 42)
hook := coreB.hooks["foo"][0].(*TestHook)
hook := build.hooks["foo"][0].(*TestHook)
assert.True(hook.runCalled, "run should be called")
assert.Equal(hook.runData, 42, "should have correct data")
// Verify provisioners run
dispatchHook.Run(HookProvision, nil, nil, 42)
prov := coreB.provisioners[0].provisioner.(*TestProvisioner)
prov := build.provisioners[0].provisioner.(*TestProvisioner)
assert.True(prov.provCalled, "provision should be called")
// Verify post-processor was run
pp := build.postProcessors[0][0].processor.(*TestPostProcessor)
assert.True(pp.ppCalled, "post processor should be called")
}
func TestBuild_Run_Artifacts(t *testing.T) {
cache := &TestCache{}
ui := testUi()
// Test case: Test that with no post-processors, we only get the
// main build.
build := testBuild()
build.postProcessors = [][]coreBuildPostProcessor{}
build.Prepare()
artifacts, err := build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
}
expectedIds := []string{"b"}
artifactIds := make([]string, len(artifacts))
for i, artifact := range artifacts {
artifactIds[i] = artifact.Id()
}
if !reflect.DeepEqual(artifactIds, expectedIds) {
t.Fatalf("unexpected ids: %#v", artifactIds)
}
// Test case: Test that with a single post-processor that doesn't keep
// inputs, only that post-processors results are returned.
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", 42, false},
},
}
build.Prepare()
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
}
expectedIds = []string{"pp"}
artifactIds = make([]string, len(artifacts))
for i, artifact := range artifacts {
artifactIds[i] = artifact.Id()
}
if !reflect.DeepEqual(artifactIds, expectedIds) {
t.Fatalf("unexpected ids: %#v", artifactIds)
}
// Test case: Test that with multiple post-processors, as long as one
// keeps the original, the original is kept.
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", 42, false},
},
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", 42, true},
},
}
build.Prepare()
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
}
expectedIds = []string{"b", "pp1", "pp2"}
artifactIds = make([]string, len(artifacts))
for i, artifact := range artifacts {
artifactIds[i] = artifact.Id()
}
if !reflect.DeepEqual(artifactIds, expectedIds) {
t.Fatalf("unexpected ids: %#v", artifactIds)
}
// Test case: Test that with sequences, intermediaries are kept if they
// want to be.
build = testBuild()
build.postProcessors = [][]coreBuildPostProcessor{
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", 42, true},
},
[]coreBuildPostProcessor{
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", 42, false},
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", 42, false},
},
}
build.Prepare()
artifacts, err = build.Run(ui, cache)
if err != nil {
t.Fatalf("err: %s", err)
}
expectedIds = []string{"pp1a", "pp1b", "pp2b"}
artifactIds = make([]string, len(artifacts))
for i, artifact := range artifacts {
artifactIds[i] = artifact.Id()
}
if !reflect.DeepEqual(artifactIds, expectedIds) {
t.Fatalf("unexpected ids: %#v", artifactIds)
}
}
func TestBuild_RunBeforePrepare(t *testing.T) {
@@ -118,8 +259,6 @@ func TestBuild_Cancel(t *testing.T) {
build := testBuild()
build.Cancel()
coreB := build.(*coreBuild)
builder := coreB.builder.(*TestBuilder)
builder := build.builder.(*TestBuilder)
assert.True(builder.cancelCalled, "cancel should be called")
}
+3 -1
View File
@@ -1,6 +1,8 @@
package packer
type TestBuilder struct {
artifactId string
prepareCalled bool
prepareConfig []interface{}
runCalled bool
@@ -21,7 +23,7 @@ func (tb *TestBuilder) Run(ui Ui, h Hook, c Cache) (Artifact, error) {
tb.runHook = h
tb.runUi = ui
tb.runCache = c
return nil, nil
return &TestArtifact{id: tb.artifactId}, nil
}
func (tb *TestBuilder) Cancel() {
+13
View File
@@ -39,8 +39,21 @@ type RemoteCmd struct {
// Communicators must be safe for concurrency, meaning multiple calls to
// Start or any other method may be called at the same time.
type Communicator interface {
// Start takes a RemoteCmd and starts it. The RemoteCmd must not be
// modified after being used with Start, and it must not be used with
// Start again. The Start method returns immediately once the command
// is started. It does not wait for the command to complete. The
// RemoteCmd.Exited field should be used for this.
Start(*RemoteCmd) error
// Upload uploads a file to the machine to the given path with the
// contents coming from the given reader. This method will block until
// it completes.
Upload(string, io.Reader) error
// Download downloads a file from the machine from the given remote path
// with the contents writing to the given writer. This method will
// block until it completes.
Download(string, io.Writer) error
}
+28 -4
View File
@@ -19,6 +19,9 @@ type CommandFunc func(name string) (Command, error)
// The function type used to lookup Hook implementations.
type HookFunc func(name string) (Hook, error)
// The function type used to lookup PostProcessor implementations.
type PostProcessorFunc func(name string) (PostProcessor, error)
// The function type used to lookup Provisioner implementations.
type ProvisionerFunc func(name string) (Provisioner, error)
@@ -26,10 +29,11 @@ type ProvisionerFunc func(name string) (Provisioner, error)
// pointers necessary to look up components of Packer such as builders,
// commands, etc.
type ComponentFinder struct {
Builder BuilderFunc
Command CommandFunc
Hook HookFunc
Provisioner ProvisionerFunc
Builder BuilderFunc
Command CommandFunc
Hook HookFunc
PostProcessor PostProcessorFunc
Provisioner ProvisionerFunc
}
// The environment interface provides access to the configuration and
@@ -42,6 +46,7 @@ type Environment interface {
Cache() Cache
Cli([]string) (int, error)
Hook(string) (Hook, error)
PostProcessor(string) (PostProcessor, error)
Provisioner(string) (Provisioner, error)
Ui() Ui
}
@@ -104,6 +109,10 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
env.components.Hook = func(string) (Hook, error) { return nil, nil }
}
if env.components.PostProcessor == nil {
env.components.PostProcessor = func(string) (PostProcessor, error) { return nil, nil }
}
if env.components.Provisioner == nil {
env.components.Provisioner = func(string) (Provisioner, error) { return nil, nil }
}
@@ -152,6 +161,21 @@ func (e *coreEnvironment) Hook(name string) (h Hook, err error) {
return
}
// Returns a PostProcessor for the given name that is registered with this
// environment.
func (e *coreEnvironment) PostProcessor(name string) (p PostProcessor, err error) {
p, err = e.components.PostProcessor(name)
if err != nil {
return
}
if p == nil {
err = fmt.Errorf("No post processor found for name: %s", name)
}
return
}
// Returns a provisioner for the given name that is registered with this
// environment.
func (e *coreEnvironment) Provisioner(name string) (p Provisioner, err error) {
+49
View File
@@ -5,11 +5,18 @@ import (
"cgl.tideland.biz/asserts"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"testing"
)
func init() {
// Disable log output for tests
log.SetOutput(ioutil.Discard)
}
func testEnvironment() Environment {
config := DefaultEnvironmentConfig()
config.Ui = &ReaderWriterUi{
@@ -67,6 +74,7 @@ func TestEnvironment_NilComponents(t *testing.T) {
env.Builder("foo")
env.Cli([]string{"foo"})
env.Hook("foo")
env.PostProcessor("foo")
env.Provisioner("foo")
}
@@ -246,6 +254,47 @@ func TestEnvironment_Hook_Error(t *testing.T) {
assert.Nil(returned, "should be no hook")
}
func TestEnvironment_PostProcessor(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
pp := &TestPostProcessor{}
pps := make(map[string]PostProcessor)
pps["foo"] = pp
config := DefaultEnvironmentConfig()
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return pps[n], nil }
env, _ := NewEnvironment(config)
returned, err := env.PostProcessor("foo")
assert.Nil(err, "should be no error")
assert.Equal(returned, pp, "should return correct pp")
}
func TestEnvironment_PostProcessor_NilError(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
config := DefaultEnvironmentConfig()
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return nil, nil }
env, _ := NewEnvironment(config)
returned, err := env.PostProcessor("foo")
assert.NotNil(err, "should be an error")
assert.Nil(returned, "should be no pp")
}
func TestEnvironment_PostProcessor_Error(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
config := DefaultEnvironmentConfig()
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return nil, errors.New("foo") }
env, _ := NewEnvironment(config)
returned, err := env.PostProcessor("foo")
assert.NotNil(err, "should be an error")
assert.Equal(err.Error(), "foo", "should be correct error")
assert.Nil(returned, "should be no pp")
}
func TestEnvironmentProvisioner(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
+8 -4
View File
@@ -12,7 +12,7 @@ const HookProvision = "packer_provision"
// in. In addition to that, the Hook is given access to a UI so that it can
// output things to the user.
type Hook interface {
Run(string, Ui, Communicator, interface{})
Run(string, Ui, Communicator, interface{}) error
}
// A Hook implementation that dispatches based on an internal mapping.
@@ -23,14 +23,18 @@ type DispatchHook struct {
// Runs the hook with the given name by dispatching it to the proper
// hooks if a mapping exists. If a mapping doesn't exist, then nothing
// happens.
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
hooks, ok := h.Mapping[name]
if !ok {
// No hooks for that name. No problem.
return
return nil
}
for _, hook := range hooks {
hook.Run(name, ui, comm, data)
if err := hook.Run(name, ui, comm, data); err != nil {
return err
}
}
return nil
}
+2 -1
View File
@@ -13,12 +13,13 @@ type TestHook struct {
runUi Ui
}
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
t.runCalled = true
t.runComm = comm
t.runData = data
t.runName = name
t.runUi = ui
return nil
}
func TestDispatchHook_Implements(t *testing.T) {
+12
View File
@@ -140,6 +140,17 @@ func (c *Client) Hook() (packer.Hook, error) {
return &cmdHook{packrpc.Hook(client), c}, nil
}
// Returns a post-processor implementation that is communicating over
// this client. If the client hasn't been started, this will start it.
func (c *Client) PostProcessor() (packer.PostProcessor, error) {
client, err := c.rpcClient()
if err != nil {
return nil, err
}
return &cmdPostProcessor{packrpc.PostProcessor(client), c}, nil
}
// Returns a provisioner implementation that is communicating over this
// client. If the client hasn't been started, this will start it.
func (c *Client) Provisioner() (packer.Provisioner, error) {
@@ -194,6 +205,7 @@ func (c *Client) Start() (address string, err error) {
}
env := []string{
fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort),
fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
}
+2 -2
View File
@@ -10,13 +10,13 @@ type cmdHook struct {
client *Client
}
func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) {
func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) error {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
c.hook.Run(name, ui, comm, data)
return c.hook.Run(name, ui, comm, data)
}
func (c *cmdHook) checkExit(p interface{}, cb func()) {
+3 -1
View File
@@ -8,7 +8,9 @@ import (
type helperHook byte
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) {}
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) error {
return nil
}
func TestHook_NoExist(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
+30 -4
View File
@@ -8,6 +8,7 @@
package plugin
import (
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
@@ -21,9 +22,16 @@ import (
"strings"
)
const MagicCookieKey = "PACKER_PLUGIN_MAGIC_COOKIE"
const MagicCookieValue = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2"
// This serves a single RPC connection on the given RPC server on
// a random port.
func serve(server *rpc.Server) (err error) {
if os.Getenv(MagicCookieKey) != MagicCookieValue {
return errors.New("Please do not execute plugins directly. Packer will execute these for you.")
}
// If there is no explicit number of Go threads to use, then set it
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
@@ -107,7 +115,8 @@ func ServeBuilder(builder packer.Builder) {
swallowInterrupts()
if err := serve(server); err != nil {
log.Panic(err)
log.Printf("ERROR: %s", err)
os.Exit(1)
}
}
@@ -120,7 +129,8 @@ func ServeCommand(command packer.Command) {
swallowInterrupts()
if err := serve(server); err != nil {
log.Panic(err)
log.Printf("ERROR: %s", err)
os.Exit(1)
}
}
@@ -133,7 +143,22 @@ func ServeHook(hook packer.Hook) {
swallowInterrupts()
if err := serve(server); err != nil {
log.Panic(err)
log.Printf("ERROR: %s", err)
os.Exit(1)
}
}
// Serves a post-processor from a plugin.
func ServePostProcessor(p packer.PostProcessor) {
log.Println("Preparing to serve a post-processor plugin...")
server := rpc.NewServer()
packrpc.RegisterPostProcessor(server, p)
swallowInterrupts()
if err := serve(server); err != nil {
log.Printf("ERROR: %s", err)
os.Exit(1)
}
}
@@ -146,6 +171,7 @@ func ServeProvisioner(p packer.Provisioner) {
swallowInterrupts()
if err := serve(server); err != nil {
log.Panic(err)
log.Printf("ERROR: %s", err)
os.Exit(1)
}
}
+2
View File
@@ -59,6 +59,8 @@ func TestHelperProcess(*testing.T) {
case "mock":
fmt.Println(":1234")
<-make(chan int)
case "post-processor":
ServePostProcessor(new(helperPostProcessor))
case "provisioner":
ServeProvisioner(new(helperProvisioner))
case "start-timeout":
+37
View File
@@ -0,0 +1,37 @@
package plugin
import (
"github.com/mitchellh/packer/packer"
"log"
)
type cmdPostProcessor struct {
p packer.PostProcessor
client *Client
}
func (c *cmdPostProcessor) Configure(config interface{}) error {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
return c.p.Configure(config)
}
func (c *cmdPostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Artifact, error) {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
return c.p.PostProcess(ui, a)
}
func (c *cmdPostProcessor) checkExit(p interface{}, cb func()) {
if c.client.Exited() {
cb()
} else if p != nil {
log.Panic(p)
}
}
+37
View File
@@ -0,0 +1,37 @@
package plugin
import (
"github.com/mitchellh/packer/packer"
"os/exec"
"testing"
)
type helperPostProcessor byte
func (helperPostProcessor) Configure(interface{}) error {
return nil
}
func (helperPostProcessor) PostProcess(packer.Ui, packer.Artifact) (packer.Artifact, error) {
return nil, nil
}
func TestPostProcessor_NoExist(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill()
_, err := c.PostProcessor()
if err == nil {
t.Fatal("should have error")
}
}
func TestPostProcessor_Good(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: helperProcess("post-processor")})
defer c.Kill()
_, err := c.PostProcessor()
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
+2 -2
View File
@@ -19,13 +19,13 @@ func (c *cmdProvisioner) Prepare(configs ...interface{}) error {
return c.p.Prepare(configs...)
}
func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) {
func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
defer func() {
r := recover()
c.checkExit(r, nil)
}()
c.p.Provision(ui, comm)
return c.p.Provision(ui, comm)
}
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
+3 -1
View File
@@ -12,7 +12,9 @@ func (helperProvisioner) Prepare(...interface{}) error {
return nil
}
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) {}
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) error {
return nil
}
func TestProvisioner_NoExist(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
+17
View File
@@ -0,0 +1,17 @@
package packer
// A PostProcessor is responsible for taking an artifact of a build
// and doing some sort of post-processing to turn this into another
// artifact. An example of a post-processor would be something that takes
// the result of a build, compresses it, and returns a new artifact containing
// a single file of the prior artifact compressed.
type PostProcessor interface {
// Configure is responsible for setting up configuration, storing
// the state for later, and returning and errors, such as validation
// errors.
Configure(interface{}) error
// PostProcess takes a previously created Artifact and produces another
// Artifact. If an error occurs, it should return that error.
PostProcess(Ui, Artifact) (Artifact, error)
}
+23
View File
@@ -0,0 +1,23 @@
package packer
type TestPostProcessor struct {
artifactId string
configCalled bool
configVal interface{}
ppCalled bool
ppArtifact Artifact
ppUi Ui
}
func (pp *TestPostProcessor) Configure(v interface{}) error {
pp.configCalled = true
pp.configVal = v
return nil
}
func (pp *TestPostProcessor) PostProcess(ui Ui, a Artifact) (Artifact, error) {
pp.ppCalled = true
pp.ppArtifact = a
pp.ppUi = ui
return &TestArtifact{id: pp.artifactId}, nil
}
+7 -3
View File
@@ -12,7 +12,7 @@ type Provisioner interface {
// given to communicate with the user, and a communicator is given that
// is guaranteed to be connected to some machine so that provisioning
// can be done.
Provision(Ui, Communicator)
Provision(Ui, Communicator) error
}
// A Hook implementation that runs the given provisioners.
@@ -23,8 +23,12 @@ type ProvisionHook struct {
}
// Runs the provisioners in order.
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
for _, p := range h.Provisioners {
p.Provision(ui, comm)
if err := p.Provision(ui, comm); err != nil {
return err
}
}
return nil
}
+2 -1
View File
@@ -14,8 +14,9 @@ func (t *TestProvisioner) Prepare(configs ...interface{}) error {
return nil
}
func (t *TestProvisioner) Provision(Ui, Communicator) {
func (t *TestProvisioner) Provision(Ui, Communicator) error {
t.provCalled = true
return nil
}
func TestProvisionHook_Impl(t *testing.T) {
+19
View File
@@ -41,6 +41,15 @@ func (a *artifact) String() (result string) {
return
}
func (a *artifact) Destroy() error {
var result error
if err := a.client.Call("Artifact.Destroy", new(interface{}), &result); err != nil {
return err
}
return result
}
func (s *ArtifactServer) BuilderId(args *interface{}, reply *string) error {
*reply = s.artifact.BuilderId()
return nil
@@ -60,3 +69,13 @@ func (s *ArtifactServer) String(args *interface{}, reply *string) error {
*reply = s.artifact.String()
return nil
}
func (s *ArtifactServer) Destroy(args *interface{}, reply *error) error {
err := s.artifact.Destroy()
if err != nil {
err = NewBasicError(err)
}
*reply = err
return nil
}
+4
View File
@@ -25,6 +25,10 @@ func (testArtifact) String() string {
return "string"
}
func (testArtifact) Destroy() error {
return nil
}
func TestArtifactRPC(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)

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