Compare commits

..

242 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
782 changed files with 8357 additions and 52969 deletions
-5
View File
@@ -1,10 +1,5 @@
/bin
/local
/pkg
/src
/website/.sass-cache
/website/build
.DS_Store
.vagrant
Vagrantfile
test/.env
-21
View File
@@ -1,21 +0,0 @@
language: go
go:
- 1.2
- tip
install: make deps
script:
- go test ./...
#- go test -race ./...
notifications:
irc:
channels:
- "chat.freenode.net#packer-tool"
on_success: change
on_failure: always
matrix:
allow_failures:
- go: tip
-934
View File
@@ -1,937 +1,3 @@
## 0.6.1 (July 20, 2014)
FEATURES:
* **New post processor:** `vagrant-cloud` - Push box files generated by
vagrant post processor to Vagrant Cloud. [GH-1289]
* Vagrant post-processor can now packer Hyper-V boxes.
IMPROVEMENTS:
* builder/amazon: Support for enhanced networking on HVM images. [GH-1228]
* builder/amazon-ebs: Support encrypted EBS volumes [GH-1194]
* builder/ansible: Add `playbook_dir` option. [GH-1000]
* builder/openstack: Add ability to configure networks. [GH-1261]
* builder/openstack: Skip certificate verification. [GH-1121]
* builder/parallels/all: Add ability to select interface to connect to.
* builder/parallels/pvm: Support `boot_command`. [GH-1082]
* builder/virtualbox/all: Attempt to use local guest additions ISO
before downloading from internet. [GH-1123]
* builder/virtualbox/ovf: Supports `guest_additions_mode` [GH-1035]
* builder/vmware/all: Increase cleanup timeout to 120 seconds [GH-1167]
* builder/vmware/all: Add `vmx_data_post` for modifying VMX data
after shutdown. [GH-1149]
* builder/vmware/vmx: Supports tools uploading. [GH-1154]
BUG FIXES:
* core: `isotime` is the same time during the entire build. [GH-1153]
* builder/amazon-common: Sort AMI strings before outputting [GH-1305]
* builder/amazon: User data can use templates/variables. [GH-1343]
* builder/amazon: Can now build AMIs in GovCloud.
* builder/null: SSH info can use templates/variables. [GH-1343]
* builder/openstack: Workaround for gophercloud.ServerById crashing [GH-1257]
* builder/openstack: Force IPv4 addresses from address pools [GH-1258]
* builder/parallels: Do not delete entire CDROM device. [GH-1115]
* builder/parallels: Errors while creating floppy disk. [GH-1225]
* builder/parallels: Errors while removing floppy drive. [GH-1226]
* builder/virtualbox-ovf: Supports guest additions options. [GH-1120]
* builder/vmware-iso: Fix esx5 path separator in windows. [GH-1316]
* builder/vmware: Remote ESXi builder now uploads floppy. [GH-1106]
* builder/vmware: Remote ESXi builder no longer re-uploads ISO every
time. [GH-1244]
* post-processor/vsphere: Accept DOMAIN\account usernames [GH-1178]
* provisioner/chef-*: Fix remotePaths for Windows [GH-394]
## 0.6.0 (May 2, 2014)
FEATURES:
* **New builder:** `null` - The null builder does not produce any
artifacts, but is useful for debugging provisioning scripts. [GH-970]
* **New builder:** `parallels-iso` and `parallels-pvm` - These can be
used to build Parallels virtual machines. [GH-1101]
* **New provisioner:** `chef-client` - Provision using a the `chef-client`
command, which talks to a Chef Server. [GH-855]
* **New provisioner:** `puppet-server` - Provision using Puppet by
communicating to a Puppet master. [GH-796]
* `min_packer_version` can be specified in a Packer template to force
a minimum version. [GH-487]
IMPROVEMENTS:
* core: RPC transport between plugins switched to MessagePack
* core: Templates array values can now be comma separated strings.
Most importantly, this allows for user variables to fill
array configurations. [GH-950]
* builder/amazon: Added `ssh_private_key_file` option [GH-971]
* builder/amazon: Added `ami_virtualization_type` option [GH-1021]
* builder/digitalocean: Regions, image names, and sizes can be
names that are looked up for their valid ID. [GH-960]
* builder/googlecompute: Configurable instance name. [GH-1065]
* builder/openstack: Support for conventional OpenStack environmental
variables such as `OS_USERNAME`, `OS_PASSWORD`, etc. [GH-768]
* builder/openstack: Support `openstack_provider` option to automatically
fill defaults for different OpenStack variants. [GH-912]
* builder/openstack: Support security groups. [GH-848]
* builder/qemu: User variable expansion in `ssh_key_path` [GH-918]
* builder/qemu: Floppy disk files list can also include globs
and directories. [GH-1086]
* builder/virtualbox: Support an `export_opts` option which allows
specifying arbitrary arguments when exporting the VM. [GH-945]
* builder/virtualbox: Added `vboxmanage_post` option to run vboxmanage
commands just before exporting [GH-664]
* builder/virtualbox: Floppy disk files list can also include globs
and directories. [GH-1086]
* builder/vmware: Workstation 10 support for Linux. [GH-900]
* builder/vmware: add cloning support on Windows [GH-824]
* builder/vmware: Floppy disk files list can also include globs
and directories. [GH-1086]
* command/build: Added `-parallel` flag so you can disable parallelization
with `-no-parallel`. [GH-924]
* post-processors/vsphere: `disk_mode` option. [GH-778]
* provisioner/ansible: Add `inventory_file` option [GH-1006]
* provisioner/chef-client: Add `validation_client_name` option. [GH-1056]
BUG FIXES:
* core: Errors are properly shown when adding bad floppy files. [GH-1043]
* core: Fix some URL parsing issues on Windows.
* core: Create Cache directory only when it is needed. [GH-367]
* builder/amazon-instance: Use S3Endpoint for ec2-upload-bundle arg,
which works for every region. [GH-904]
* builder/digitalocean: updated default image_id [GH-1032]
* builder/googlecompute: Create persistent disk as boot disk via
API v1 [GH-1001]
* builder/openstack: Return proper error on invalid instance states [GH-1018]
* builder/virtualbox-iso: Retry unregister a few times to deal with
VBoxManage randomness. [GH-915]
* provisioner/ansible: Fix paths when provisioning Linux from
Windows [GH-963]
* provisioner/ansible: set cwd to staging directory [GH-1016]
* provisioners/chef-client: Don't chown directory with Ubuntu. [GH-939]
* provisioners/chef-solo: Deeply nested JSON works properly. [GH-1076]
* provisioners/shell: Env var values can have equal signs. [GH-1045]
* provisioners/shell: chmod the uploaded script file to 0777. [GH-994]
* post-processor/docker-push: Allow repositories with ports. [GH-923]
* post-processor/vagrant: Create parent directories for `output` path [GH-1059]
* post-processor/vsphere: datastore, network, and folder are no longer
required. [GH-1091]
## 0.5.2 (02/21/2014)
FEATURES:
* **New post-processor:** `docker-import` - Import a Docker image
and give it a specific repository/tag.
* **New post-processor:** `docker-push` - Push an imported image to
a registry.
IMPROVEMENTS:
* core: Most downloads made by Packer now use a custom user agent. [GH-803]
* builder/googlecompute: SSH private key will be saved to disk if `-debug`
is specified. [GH-867]
* builder/qemu: Can specify the name of the qemu binary. [GH-854]
* builder/virtualbox-ovf: Can specify import options such as "keepallmacs".
[GH-883]
BUG FIXES:
* core: Fix crash case if blank parameters are given to Packer. [GH-832]
* core: Fix crash if big file uploads are done. [GH-897]
* core: Fix crash if machine-readable output is going to a closed
pipe. [GH-875]
* builder/docker: user variables work properly. [GH-777]
* builder/qemu: reboots are now possible in provisioners. [GH-864]
* builder/virtualbox,vmware: iso\_checksum is not required if the
checksum type is "none"
* builder/virtualbox,vmware/qemu: Support for additional scancodes for
`boot_command` such as `<up>`, `<left>`, `<insert>`, etc. [GH-808]
* communicator/ssh: Send TCP keep-alives on connections. [GH-872]
* post-processor/vagrant: AWS/DigitalOcean keep input artifacts by
default. [GH-55]
* provisioners/ansible-local: Properly upload custom playbooks. [GH-829]
* provisioners/ansible-local: Better error if ansible isn't installed.
[GH-836]
## 0.5.1 (01/02/2014)
BUG FIXES:
* core: If a stream ID loops around, don't let it use stream ID 0 [GH-767]
* core: Fix issue where large writes to plugins would result in stream
corruption. [GH-727]
* builders/virtualbox-ovf: `shutdown_timeout` config works. [GH-772]
* builders/vmware-iso: Remote driver works properly again. [GH-773]
## 0.5.0 (12/30/2013)
BACKWARDS INCOMPATIBILITIES:
* "virtualbox" builder has been renamed to "virtualbox-iso". Running your
template through `packer fix` will resolve this.
* "vmware" builder has been renamed to "vmware-iso". Running your template
through `packer fix` will resolve this.
* post-processor/vagrant: Syntax for overriding by provider has changed.
See the documentation for more information. Running your template
through `packer fix` should resolve this.
* post-processor/vsphere: Some available configuration options were
changed. Running your template through `packer fix` should resolve
this.
* provisioner/puppet-masterless: The `execute_command` no longer has
the `Has*` variables, since the templating language now supports
comparison operations. See the Go documentation for more info:
http://golang.org/pkg/text/template/
FEATURES:
* **New builder:** Google Compute Engine. You can now build images for
use in Google Compute Engine. See the documentation for more information.
[GH-715]
* **New builder:** "virtualbox-ovf" can build VirtualBox images from
an existing OVF or OVA. [GH-201]
* **New builder:** "vmware-vmx" can build VMware images from an existing
VMX. [GH-201]
* Environmental variables can now be accessed as default values for
user variables using the "env" function. See the documentation for more
information.
* "description" field in templates: write a human-readable description
of what a template does. This will be shown in `packer inspect`.
* Vagrant post-processor now accepts a list of files to include in the
box.
* All provisioners can now have a "pause\_before" parameter to wait
some period of time before running that provisioner. This is useful
for reboots. [GH-737]
IMPROVEMENTS:
* core: Plugins communicate over a single TCP connection per plugin now,
instead of sometimes dozens. Performance around plugin communication
dramatically increased.
* core: Build names are now template processed so you can use things
like user variables in them. [GH-744]
* core: New "pwd" function available globally that returns the working
directory. [GH-762]
* builder/amazon/all: Launched EC2 instances now have a name of
"Packer Builder" so that they are easily recognizable. [GH-642]
* builder/amazon/all: Copying AMIs to multiple regions now happens
in parallel. [GH-495]
* builder/amazon/all: Ability to specify "run\_tags" to tag the instance
while running. [GH-722]
* builder/digitalocean: Private networking support. [GH-698]
* builder/docker: A "run\_command" can be specified, configuring how
the container is started. [GH-648]
* builder/openstack: In debug mode, the generated SSH keypair is saved
so you can SSH into the machine. [GH-746]
* builder/qemu: Floppy files are supported. [GH-686]
* builder/qemu: Next `run_once` option tells Qemu to run only once,
which is useful for Windows installs that handle reboots for you.
[GH-687]
* builder/virtualbox: Nice errors if Packer can't write to
the output directory.
* builder/virtualbox: ISO is ejected prior to export.
* builder/virtualbox: Checksum type can be "none" [GH-471]
* builder/vmware: Can now specify path to the Fusion application. [GH-677]
* builder/vmware: Checksum type can be "none" [GH-471]
* provisioner/puppet-masterless: Can now specify a `manifest_dir` to
upload manifests to the remote machine for imports. [GH-655]
BUG FIXES:
* core: No colored output in machine-readable output. [GH-684]
* core: User variables can now be used for non-string fields. [GH-598]
* core: Fix bad download paths if the download URL contained a "."
before a "/" [GH-716]
* core: "{{timestamp}}" values will always be the same for the entire
duration of a build. [GH-744]
* builder/amazon: Handle cases where security group isn't instantly
available. [GH-494]
* builder/virtualbox: don't download guest additions if disabled. [GH-731]
* post-processor/vsphere: Uploads VM properly. [GH-694]
* post-processor/vsphere: Process user variables.
* provisioner/ansible-local: all configurations are processed as templates
[GH-749]
* provisioner/ansible-local: playbook paths are properly validated
as directories, not files. [GH-710]
* provisioner/chef-solo: Environments are recognized. [GH-726]
## 0.4.1 (December 7, 2013)
IMPROVEMENTS:
* builder/amazon/ebs: New option allows associating a public IP with
non-default VPC instances. [GH-660]
* builder/openstack: A "proxy\_url" setting was added to define an HTTP
proxy to use when building with this builder. [GH-637]
BUG FIXES:
* core: Don't change background color on CLI anymore, making things look
a tad nicer in some terminals.
* core: multiple ISO URLs works properly in all builders. [GH-683]
* builder/amazon/chroot: Block when obtaining file lock to allow
parallel builds. [GH-689]
* builder/amazon/instance: Add location flag to upload bundle command
so that building AMIs works out of us-east-1 [GH-679]
* builder/qemu: Qemu arguments are templated. [GH-688]
* builder/vmware: Cleanup of VMX keys works properly so cd-rom won't
get stuck with ISO. [GH-685]
* builder/vmware: File cleanup is more resilient to file delete races
with the operating system. [GH-675]
* provisioner/puppet-masterless: Check for hiera config path existence
properly. [GH-656]
## 0.4.0 (November 19, 2013)
FEATURES:
* Docker builder: build and export Docker containers, easily provisioned
with any of the Packer built-in provisioners.
* QEMU builder: builds a new VM compatible with KVM or Xen using QEMU.
* Remote ESXi builder: builds a VMware VM using ESXi remotely using only
SSH to an ESXi machine directly.
* vSphere post-processor: Can upload VMware artifacts to vSphere
* Vagrant post-processor can now make DigitalOcean provider boxes. [GH-504]
IMPROVEMENTS:
* builder/amazon/all: Can now specify a list of multiple security group
IDs to apply. [GH-499]
* builder/amazon/all: AWS API requests are now retried when a temporary
network error occurs as well as 500 errors. [GH-559]
* builder/virtualbox: Use VBOX\_INSTALL\_PATH env var on Windows to find
VBoxManage. [GH-628]
* post-processor/vagrant: skips gzip compression when compression_level=0
* provisioner/chef-solo: Encrypted data bag support [GH-625]
BUG FIXES:
* builder/amazon/chroot: Copying empty directories works. [GH-588]
* builder/amazon/chroot: Chroot commands work with shell provisioners. [GH-581]
* builder/amazon/chroot: Don't choose a mount point that is a partition of
an already mounted device. [GH-635]
* builder/virtualbox: Ctrl-C interrupts during waiting for boot. [GH-618]
* builder/vmware: VMX modifications are now case-insensitive. [GH-608]
* builder/vmware: VMware Fusion won't ask for VM upgrade.
* builder/vmware: Ctrl-C interrupts during waiting for boot. [GH-618]
* provisioner/chef-solo: Output is slightly prettier and more informative.
## 0.3.11 (November 4, 2013)
FEATURES:
* builder/amazon/ebs: Ability to specify which availability zone to create
instance in. [GH-536]
IMPROVEMENTS:
* core: builders can now give warnings during validation. warnings won't
fail the build but may hint at potential future problems.
* builder/digitalocean: Can now specify a droplet name
* builder/virtualbox: Can now disable guest addition download entirely
by setting "guest_additions_mode" to "disable" [GH-580]
* builder/virtualbox,vmware: ISO urls can now be https [GH-587]
* builder/virtualbox,vmware: Warning if shutdown command is not specified,
since it is a common case of data loss.
BUG FIXES:
* core: Won't panic when writing to a bad pipe. [GH-560]
* builder/amazon/all: Properly scrub access key and secret key from logs.
[GH-554]
* builder/openstack: Properly scrub password from logs [GH-554]
* builder/virtualbox: No panic if SSH host port min/max is the same. [GH-594]
* builder/vmware: checks if `ifconfig` is in `/sbin` [GH-591]
* builder/vmware: Host IP lookup works for non-C locales. [GH-592]
* common/uuid: Use cryptographically secure PRNG when generating
UUIDs. [GH-552]
* communicator/ssh: File uploads that exceed the size of memory no longer
cause crashes. [GH-561]
## 0.3.10 (October 20, 2013)
FEATURES:
* Ansible provisioner
IMPROVEMENTS:
* post-processor/vagrant: support instance-store AMIs built by Packer. [GH-502]
* post-processor/vagrant: can now specify compression level to use
when creating the box. [GH-506]
BUG FIXES:
* builder/all: timeout waiting for SSH connection is a failure. [GH-491]
* builder/amazon: Scrub sensitive data from the logs. [GH-521]
* builder/amazon: Handle the situation where an EC2 instance might not
be immediately available. [GH-522]
* builder/amazon/chroot: Files copied into the chroot remove destination
before copy, fixing issues with dangling symlinks. [GH-500]
* builder/digitalocean: don't panic if erroneous API response doesn't
contain error message. [GH-492]
* builder/digitalocean: scrub API keys from config debug output [GH-516]
* builder/virtualbox: error if VirtualBox version cant be detected. [GH-488]
* builder/virtualbox: detect if vboxdrv isn't properly setup. [GH-488]
* builder/virtualbox: sleep a bit before export to ensure the sesssion
is unlocked. [GH-512]
* builder/virtualbox: create SATA drives properly on VirtualBox 4.3 [GH-547]
* builder/virtualbox: support user templates in SSH key path. [GH-539]
* builder/vmware: support user templates in SSH key path. [GH-539]
* communicator/ssh: Fix issue where a panic could arise from a nil
dereference. [GH-525]
* post-processor/vagrant: Fix issue with VirtualBox OVA. [GH-548]
* provisioner/salt: Move salt states to correct remote directory. [GH-513]
* provisioner/shell: Won't block on certain scripts on Windows anymore.
[GH-507]
## 0.3.9 (October 2, 2013)
FEATURES:
* The Amazon chroot builder is now able to run without any `sudo` privileges
by using the "command_wrapper" configuration. [GH-430]
* Chef provisioner supports environments. [GH-483]
BUG FIXES:
* core: default user variable values don't need to be strings. [GH-456]
* builder/amazon-chroot: Fix errors with waitin for state change. [GH-459]
* builder/digitalocean: Use proper error message JSON key (DO API change).
* communicator/ssh: SCP uploads now work properly when directories
contain symlinks. [GH-449]
* provisioner/chef-solo: Data bags and roles path are now properly
populated when set. [GH-470]
* provisioner/shell: Windows line endings are actually properly changed
to Unix line endings. [GH-477]
## 0.3.8 (September 22, 2013)
FEATURES:
* core: You can now specify `only` and `except` configurations on any
provisioner or post-processor to specify a list of builds that they
are valid for. [GH-438]
* builders/virtualbox: Guest additions can be attached rather than uploaded,
easier to handle for Windows guests. [GH-405]
* provisioner/chef-solo: Ability to specify a custom Chef configuration
template.
* provisioner/chef-solo: Roles and data bags support. [GH-348]
IMPROVEMENTS:
* core: User variables can now be used for integer, boolean, etc.
values. [GH-418]
* core: Plugins made with incompatible versions will no longer load.
* builder/amazon/all: Interrupts work while waiting for AMI to be ready.
* provisioner/shell: Script line-endings are automatically converted to
Unix-style line-endings. Can be disabled by setting "binary" to "true".
[GH-277]
BUG FIXES:
* core: Set TCP KeepAlives on internally created RPC connections so that
they don't die. [GH-416]
* builder/amazon/all: While waiting for AMI, will detect "failed" state.
* builder/amazon/all: Waiting for state will detect if the resource (AMI,
instance, etc.) disappears from under it.
* builder/amazon/instance: Exclude only contents of /tmp, not /tmp
itself. [GH-437]
* builder/amazon/instance: Make AccessKey/SecretKey available to bundle
command even when they come from the environment. [GH-434]
* builder/virtualbox: F1-F12 and delete scancodes now work. [GH-425]
* post-processor/vagrant: Override configurations properly work. [GH-426]
* provisioner/puppet-masterless: Fix failure case when both facter vars
are used and prevent_sudo. [GH-415]
* provisioner/puppet-masterless: User variables now work properly in
manifest file and hiera path. [GH-448]
## 0.3.7 (September 9, 2013)
BACKWARDS INCOMPATIBILITIES:
* The "event_delay" option for the DigitalOcean builder is now gone.
The builder automatically waits for events to go away. Run your templates
through `packer fix` to get rid of these.
FEATURES:
* **NEW PROVISIONER:** `puppet-masterless`. You can now provision with
a masterless Puppet setup. [GH-234]
* New globally available template function: `uuid`. Generates a new random
UUID.
* New globally available template function: `isotime`. Generates the
current time in ISO standard format.
* New Amazon template function: `clean_ami_name`. Substitutes '-' for
characters that are illegal to use in an AMI name.
IMPROVEMENTS:
* builder/amazon/all: Ability to specify the format of the temporary
keypair created. [GH-389]
* builder/amazon/all: Support the NoDevice flag for block mappings. [GH-396]
* builder/digitalocean: Retry on any pending event errors.
* builder/openstack: Can now specify a project. [GH-382]
* builder/virtualbox: Can now attach hard drive over SATA. [GH-391]
* provisioner/file: Can now upload directories. [GH-251]
BUG FIXES:
* core: Detect if SCP is not enabled on the other side. [GH-386]
* builder/amazon/all: When copying AMI to multiple regions, copy
the metadata (tags and attributes) as well. [GH-388]
* builder/amazon/all: Fix panic case where eventually consistent
instance state caused an index out of bounds.
* builder/virtualbox: The `vm_name` setting now properly sets the OVF
name of the output. [GH-401]
* builder/vmware: Autoanswer VMware dialogs. [GH-393]
* command/inspect: Fix weird output for default values for optional vars.
## 0.3.6 (September 2, 2013)
FEATURES:
* User variables can now be specified as "required", meaning the user
MUST specify a value. Just set the default value to "null". [GH-374]
IMPROVEMENTS:
* core: Much improved interrupt handling. For example, interrupts now
cancel much more quickly within provisioners.
* builder/amazon: In `-debug` mode, the keypair used will be saved to
the current directory so you can access the machine. [GH-373]
* builder/amazon: In `-debug` mode, the DNS is outputted.
* builder/openstack: IPv6 addresses supported for SSH. [GH-379]
* communicator/ssh: Support for private keys encrypted using PKCS8. [GH-376]
* provisioner/chef-solo: You can now use user variables in the `json`
configuration for Chef. [GH-362]
BUG FIXES:
* core: Concurrent map access is completely gone, fixing rare issues
with runtime memory corruption. [GH-307]
* core: Fix possible panic when ctrl-C during provisioner run.
* builder/digitalocean: Retry destroy a few times because DO sometimes
gives false errors.
* builder/openstack: Properly handle the case no image is made. [GH-375]
* builder/openstack: Specifying a region is now required in a template.
* provisioners/salt-masterless: Use filepath join to properly join paths.
## 0.3.5 (August 28, 2013)
FEATURES:
* **NEW BUILDER:** `openstack`. You can now build on OpenStack. [GH-155]
* **NEW PROVISIONER:** `chef-solo`. You can now provision with Chef
using `chef-solo` from local cookbooks.
* builder/amazon: Copy AMI to multiple regions with `ami_regions`. [GH-322]
* builder/virtualbox,vmware: Can now use SSH keys as an auth mechanism for
SSH using `ssh_key_path`. [GH-70]
* builder/virtualbox,vmware: Support SHA512 as a checksum type. [GH-356]
* builder/vmware: The root hard drive type can now be specified with
"disk_type_id" for advanced users. [GH-328]
* provisioner/salt-masterless: Ability to specfy a minion config. [GH-264]
* provisioner/salt-masterless: Ability to upload pillars. [GH-353]
IMPROVEMENTS:
* core: Output message when Ctrl-C received that we're cleaning up. [GH-338]
* builder/amazon: Tagging now works with all amazon builder types.
* builder/vmware: Option `ssh_skip_request_pty` for not requesting a PTY
for the SSH connection. [GH-270]
* builder/vmware: Specify a `vmx_template_path` in order to customize
the generated VMX. [GH-270]
* command/build: Machine-readable output now contains build errors, if any.
* command/build: An "end" sentinel is outputted in machine-readable output
for artifact listing so it is easier to know when it is over.
BUG FIXES:
* core: Fixed a couple cases where a double ctrl-C could panic.
* core: Template validation fails if an override is specified for a
non-existent builder. [GH-336]
* core: The SSH connection is heartbeated so that drops can be
detected. [GH-200]
* builder/amazon/instance: Remove check for ec2-ami-tools because it
didn't allow absolute paths to work properly. [GH-330]
* builder/digitalocean: Send a soft shutdown request so that files
are properly synced before shutdown. [GH-332]
* command/build,command/validate: If a non-existent build is specified to
'-only' or '-except', it is now an error. [GH-326]
* post-processor/vagrant: Setting OutputPath with a timestamp now
always works properly. [GH-324]
* post-processor/vagrant: VirtualBox OVA formats now turn into
Vagrant boxes properly. [GH-331]
* provisioner/shell: Retry upload if start command fails, making reboot
handling much more robust.
## 0.3.4 (August 21, 2013)
IMPROVEMENTS:
* post-processor/vagrant: the file being compressed will be shown
in the UI [GH-314]
BUG FIXES:
* core: Avoid panics when double-interrupting Packer.
* provisioner/shell: Retry shell script uploads, making reboots more
robust if they happen to fail in this stage. [GH-282]
## 0.3.3 (August 19, 2013)
FEATURES:
* builder/virtualbox: support exporting in OVA format. [GH-309]
IMPROVEMENTS:
* core: All HTTP downloads across Packer now support the standard
proxy environmental variables (`HTTP_PROXY`, `NO_PROXY`, etc.) [GH-252]
* builder/amazon: API requests will use HTTP proxy if specified by
enviromental variables.
* builder/digitalocean: API requests will use HTTP proxy if specified
by environmental variables.
BUG FIXES:
* core: TCP connection between plugin processes will keep-alive. [GH-312]
* core: No more "unused key keep_input_artifact" for post processors [GH-310]
* post-processor/vagrant: `output_path` templates now work again.
## 0.3.2 (August 18, 2013)
FEATURES:
* New command: `packer inspect`. This command tells you the components of
a template. It respects the `-machine-readable` flag as well so you can
parse out components of a template.
* Packer will detect its own crashes (always a bug) and save a "crash.log"
file.
* builder/virtualbox: You may now specify multiple URLs for an ISO
using "iso_url" in a template. The URLs will be tried in order.
* builder/vmware: You may now specify multiple URLs for an ISO
using "iso_url" in a template. The URLs will be tried in order.
IMPROVEMENTS:
* core: built with Go 1.1.2
* core: packer help output now loads much faster.
* builder/virtualbox: guest_additions_url can now use the `Version`
variable to get the VirtualBox version. [GH-272]
* builder/virtualbox: Do not check for VirtualBox as part of template
validation; only check at execution.
* builder/vmware: Do not check for VMware as part of template validation;
only check at execution.
* command/build: A path of "-" will read the template from stdin.
* builder/amazon: add block device mappings [GH-90]
BUG FIXES:
* windows: file URLs are easier to get right as Packer
has better parsing and error handling for Windows file paths. [GH-284]
* builder/amazon/all: Modifying more than one AMI attribute type no longer
crashes.
* builder/amazon-instance: send IAM instance profile data. [GH-294]
* builder/digitalocean: API request parameters are properly URL
encoded. [GH-281]
* builder/virtualbox: dowload progress won't be shown until download
actually starts. [GH-288]
* builder/virtualbox: floppy files names of 13 characters are now properly
written to the FAT12 filesystem. [GH-285]
* builder/vmware: dowload progress won't be shown until download
actually starts. [GH-288]
* builder/vmware: interrupt works while typing commands over VNC.
* builder/virtualbox: floppy files names of 13 characters are now properly
written to the FAT12 filesystem. [GH-285]
* post-processor/vagrant: Process user variables. [GH-295]
## 0.3.1 (August 12, 2013)
IMPROVEMENTS:
* provisioner/shell: New setting `start_retry_timeout` which is the timeout
for the provisioner to attempt to _start_ the remote process. This allows
the shell provisioner to work properly with reboots. [GH-260]
BUG FIXES:
* core: Remote command output containing '\r' now looks much better
within the Packer output.
* builder/vmware: Fix issue with finding driver files. [GH-279]
* provisioner/salt-masterless: Uploads work properly from Windows. [GH-276]
## 0.3.0 (August 12, 2013)
BACKWARDS INCOMPATIBILITIES:
* All `{{.CreateTime}}` variables within templates (such as for AMI names)
are now replaced with `{{timestamp}}`. Run `packer fix` to fix your
templates.
FEATURES:
* **User Variables** allow you to specify variables within your templates
that can be replaced using the command-line, files, or environmental
variables. This dramatically improves the portability of packer templates.
See the documentation for more information.
* **Machine-readable output** can be enabled by passing the
`-machine-readable` flag to _any_ Packer command.
* All strings in a template are now processed for variables/functions,
so things like `{{timestamp}}` can be used everywhere. More features will
be added in the future.
* The `amazon` builders (all of them) can now have attributes of their
resulting AMIs modified, such as access permissions and product codes.
IMPROVEMENTS:
* builder/amazon/all: User data can be passed to start the instances. [GH-253]
* provisioner/salt-masterless: `local_state_tree` is no longer required,
allowing you to use shell provisioner (or others) to bring this down.
[GH-269]
BUG FIXES:
* builder/amazon/ebs,instance: Retry deleing security group a few times.
[GH-278]
* builder/vmware: Workstation works on Windows XP now. [GH-238]
* builder/vmware: Look for files on Windows in multiple locations
using multiple environmental variables. [GH-263]
* provisioner/salt-masterless: states aren't deleted after the run
anymore. [GH-265]
* provisioner/salt-masterless: error if any commands exit with a non-zero
exit status. [GH-266]
## 0.2.3 (August 7, 2013)
IMPROVEMENTS:
* builder/amazon/all: Added Amazon AMI tag support [GH-233]
BUG FIXES:
* core: Absolute/relative filepaths on Windows now work for iso_url
and other settings. [GH-240]
* builder/amazon/all: instance info is refreshed while waiting for SSH,
allowing Packer to see updated IP/DNS info. [GH-243]
## 0.2.2 (August 1, 2013)
FEATURES:
* New builder: `amazon-chroot` can create EBS-backed AMIs without launching
a new EC2 instance. This can shave minutes off of the AMI creation process.
See the docs for more info.
* New provisioner: `salt-masterless` will provision the node using Salt
without a master.
* The `vmware` builder now works with Workstation 9 on Windows. [GH-222]
* The `vmware` builder now works with Player 5 on Linux. [GH-190]
IMPROVEMENTS:
* core: Colors won't be outputted on Windows unless in Cygwin.
* builder/amazon/all: Added `iam_instance_profile` to launch the source
image with a given IAM profile. [GH-226]
BUG FIXES:
* builder/virtualbox,vmware: relative paths work properly as URL
configurations. [GH-215]
* builder/virtualbox,vmware: fix race condition in deleting the output
directory on Windows by retrying.
## 0.2.1 (July 26, 2013)
FEATURES:
* New builder: `amazon-instance` can create instance-storage backed
AMIs.
* VMware builder now works with Workstation 9 on Linux.
IMPROVEMENTS:
* builder/amazon/all: Ctrl-C while waiting for state change works
* builder/amazon/ebs: Can now launch instances into a VPC for added protection [GH-210]
* builder/virtualbox,vmware: Add backspace, delete, and F1-F12 keys to the boot
command.
* builder/virtualbox: massive performance improvements with big ISO files because
an expensive copy is avoided. [GH-202]
* builder/vmware: CD is removed prior to exporting final machine. [GH-198]
BUG FIXES:
* builder/amazon/all: Gracefully handle when AMI appears to not exist
while AWS state is propogating. [GH-207]
* builder/virtualbox: Trim carriage returns for Windows to properly
detect VM state on Windows. [GH-218]
* core: build names no longer cause invalid config errors. [GH-197]
* command/build: If any builds fail, exit with non-zero exit status.
* communicator/ssh: SCP exit codes are tested and errors are reported. [GH-195]
* communicator/ssh: Properly change slash direction for Windows hosts. [GH-218]
## 0.2.0 (July 16, 2013)
BACKWARDS INCOMPATIBILITIES:
* "iso_md5" in the virtualbox and vmware builders is replaced with
"iso_checksum" and "iso_checksum_type" (with the latter set to "md5").
See the announce below on `packer fix` to automatically fix your templates.
FEATURES:
* **NEW COMMAND:** `packer fix` will attempt to fix templates from older
versions of Packer that are now broken due to backwards incompatibilities.
This command will fix the backwards incompatibilities introduced in this
version.
* Amazon EBS builder can now optionally use a pre-made security group
instead of randomly generating one.
* DigitalOcean API key and client IDs can now be passed in as
environmental variables. See the documentatin for more details.
* VirtualBox and VMware can now have `floppy_files` specified to attach
floppy disks when booting. This allows for unattended Windows installs.
* `packer build` has a new `-force` flag that forces the removal of
existing artifacts if they exist. [GH-173]
* You can now log to a file (instead of just stderr) by setting the
`PACKER_LOG_FILE` environmental variable. [GH-168]
* Checksums other than MD5 can now be used. SHA1 and SHA256 can also
be used. See the documentation on `iso_checksum_type` for more info. [GH-175]
IMPROVEMENTS:
* core: invalid keys in configuration are now considered validation
errors. [GH-104]
* core: all builders now share a common SSH connection core, improving
SSH reliability over all the builders.
* amazon-ebs: Credentials will come from IAM role if available. [GH-160]
* amazon-ebs: Verify the source AMI is EBS-backed before launching. [GH-169]
* shell provisioner: the build name and builder type are available in
the `PACKER_BUILD_NAME` and `PACKER_BUILDER_TYPE` env vars by default,
respectively. [GH-154]
* vmware: error if shutdown command has non-zero exit status.
BUG FIXES:
* core: UI messages are now properly prefixed with spaces again.
* core: If SSH connection ends, re-connection attempts will take
place. [GH-152]
* virtualbox: "paused" doesn't mean the VM is stopped, improving
shutdown detection.
* vmware: error if guest IP could not be detected. [GH-189]
## 0.1.5 (July 7, 2013)
FEATURES:
* "file" uploader will upload files from the machine running Packer to the
remote machine.
* VirtualBox guest additions URL and checksum can now be specified, allowing
the VirtualBox builder to have the ability to be used completely offline.
IMPROVEMENTS:
* core: If SCP is not available, a more descriptive error message
is shown telling the user. [GH-127]
* shell: Scripts are now executed by default according to their shebang,
not with `/bin/sh`. [GH-105]
* shell: You can specify what interpreter you want inline scripts to
run with `inline_shebang`.
* virtualbox: Delete the packer-made SSH port forwarding prior to
exporting the VM.
BUG FIXES:
* core: Non-200 response codes on downloads now show proper errors.
[GH-141]
* amazon-ebs: SSH handshake is retried. [GH-130]
* vagrant: The `BuildName` template propery works properly in
the output path.
* vagrant: Properly configure the provider-specific post-processors so
things like `vagrantfile_template` work. [GH-129]
* vagrant: Close filehandles when copying files so Windows can
rename files. [GH-100]
## 0.1.4 (July 2, 2013)
FEATURES:
* virtualbox: Can now be built headless with the "Headless" option. [GH-99]
* virtualbox: <wait5> and <wait10> codes for waiting 5 and 10 seconds
during the boot sequence, respectively. [GH-97]
* vmware: Can now be built headless with the "Headless" option. [GH-99]
* vmware: <wait5> and <wait10> codes for waiting 5 and 10 seconds
during the boot sequence, respectively. [GH-97]
* vmware: Disks are defragmented and compacted at the end of the build.
This can be disabled using "skip_compaction"
IMPROVEMENTS:
* core: Template syntax errors now show line and character number. [GH-56]
* amazon-ebs: Access key and secret access key default to
environmental variables. [GH-40]
* virtualbox: Send password for keyboard-interactive auth [GH-121]
* vmware: Send password for keyboard-interactive auth [GH-121]
BUG FIXES:
* vmware: Wait until shut down cleans up properly to avoid corrupt
disk files [GH-111]
## 0.1.3 (July 1, 2013)
FEATURES:
* The VMware builder can now upload the VMware tools for you into
the VM. This is opt-in, you must specify the `tools_upload_flavor`
option. See the website for more documentation.
IMPROVEMENTS:
* digitalocean: Errors contain human-friendly error messages. [GH-85]
BUG FIXES:
* core: More plugin server fixes that avoid hangs on OS X 10.7 [GH-87]
* vagrant: AWS boxes will keep the AMI artifact around [GH-55]
* virtualbox: More robust version parsing for uploading guest additions. [GH-69]
* virtualbox: Output dir and VM name defaults depend on build name,
avoiding collisions. [GH-91]
* vmware: Output dir and VM name defaults depend on build name,
avoiding collisions. [GH-91]
## 0.1.2 (June 29, 2013)
IMPROVEMENTS:
* core: Template doesn't validate if there are no builders.
* vmware: Delete any VMware files in the VM that aren't necessary for
it to function.
BUG FIXES:
* core: Plugin servers consider a port in use if there is any
error listening to it. This fixes I18n issues and Windows. [GH-58]
* amazon-ebs: Sleep between checking instance state to avoid
RequestLimitExceeded [GH-50]
* vagrant: Rename VirtualBox ovf to "box.ovf" [GH-64]
* vagrant: VMware boxes have the correct provider type.
* vmware: Properly populate files in artifact so that the Vagrant
post-processor works. [GH-63]
## 0.1.1 (June 28, 2013)
BUG FIXES:
* core: plugins listen explicitly on 127.0.0.1, fixing odd hangs. [GH-37]
* core: fix race condition on verifying checksum of large ISOs which
could cause panics [GH-52]
* virtualbox: `boot_wait` defaults to "10s" rather than 0. [GH-44]
* virtualbox: if `http_port_min` and max are the same, it will no longer
panic [GH-53]
* vmware: `boot_wait` defaults to "10s" rather than 0. [GH-44]
* vmware: if `http_port_min` and max are the same, it will no longer
panic [GH-53]
## 0.1.0 (June 28, 2013)
* Initial release
-87
View File
@@ -1,87 +0,0 @@
# Contributing to Packer
**First:** if you're unsure or afraid of _anything_, just ask
or submit the issue or pull request anyways. You won't be yelled at for
giving your best effort. The worst that can happen is that you'll be
politely asked to change something. We appreciate any sort of contributions,
and don't want a wall of rules to get in the way of that.
However, for those individuals who want a bit more guidance on the
best way to contribute to the project, read on. This document will cover
what we're looking for. By addressing all the points we're looking for,
it raises the chances we can quickly merge or address your contributions.
## Issues
### Reporting an Issue
* Make sure you test against the latest released version. It is possible
we already fixed the bug you're experiencing.
* Run the command with debug ouput with the environment variable
`PACKER_LOG`. For example: `PACKER_LOG=1 packer build template.json`. Take
the *entire* output and create a [gist](https://gist.github.com) for linking
to in your issue. Packer should strip sensitive keys from the output,
but take a look through just in case.
* Provide a reproducible test case. If a contributor can't reproduce an
issue, then it dramatically lowers the chances it'll get fixed. And in
some cases, the issue will eventually be closed.
* Respond promptly to any questions made by the Packer team to your issue.
Stale issues will be closed.
### Issue Lifecycle
1. The issue is reported.
2. The issue is verified and categorized by a Packer collaborator.
Categorization is done via tags. For example, bugs are marked as "bugs"
and easy fixes are marked as "easy".
3. Unless it is critical, the issue is left for a period of time (sometimes
many weeks), giving outside contributors a chance to address the issue.
4. The issue is addressed in a pull request or commit. The issue will be
referenced in the commit message so that the code that fixes it is clearly
linked.
5. The issue is closed.
## Setting up Go to work on Packer
If you have never worked with Go before, you will have to complete the
following steps in order to be able to compile and test Packer.
1. Install Go. Make sure the Go version is at least Go 1.2. Packer will not work with anything less than
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2.
2. Set and export the `GOPATH` environment variable and update your `PATH`.
For example, you can add to your `.bash_profile`.
```
export GOPATH=$HOME/Documents/golang
export PATH=$PATH:$GOPATH/bin
```
3. Install and build `gox` with
```
go get github.com/mitchellh/gox
cd $GOPATH/src/github.com/mitchellh/gox
go build
```
4. Download the Packer source (and its dependencies) by running
`go get github.com/mitchellh/packer`. This will download the Packer
source to `$GOPATH/src/github.com/mitchellh/packer`.
5. Make your changes to the Packer source. You can run `make` from the main
source directory to recompile all the binaries. Any compilation errors
will be shown when the binaries are rebuilding.
6. Test your changes by running `make test` and then running
`$GOPATH/src/github.com/mitchellh/packer/bin/packer` to build a machine.
7. If everything works well and the tests pass, run `go fmt` on your code
before submitting a pull request.
+13 -30
View File
@@ -1,39 +1,22 @@
NO_COLOR=\033[0m
OK_COLOR=\033[32;01m
ERROR_COLOR=\033[31;01m
WARN_COLOR=\033[33;01m
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
UNAME := $(shell uname -s)
ifeq ($(UNAME),Darwin)
ECHO=echo
else
ECHO=/bin/echo -e
endif
NO_COLOR=\x1b[0m
OK_COLOR=\x1b[32;01m
ERROR_COLOR=\x1b[31;01m
WARN_COLOR=\x1b[33;01m
all: deps
all:
@mkdir -p bin/
@$(ECHO) "$(OK_COLOR)==> Building$(NO_COLOR)"
@bash --norc -i ./scripts/devcompile.sh
deps:
@$(ECHO) "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
@go get -d -v ./...
@go get github.com/mitchellh/gox
@echo $(DEPS) | xargs -n1 go get -d
updatedeps:
@$(ECHO) "$(OK_COLOR)==> Updating all dependencies$(NO_COLOR)"
@go get -d -v -u ./...
@echo $(DEPS) | xargs -n1 go get -d -u
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./scripts/build.sh
format:
go fmt ./...
test: deps
@$(ECHO) "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
test:
@echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
@go list -f '{{range .TestImports}}{{.}}\
{{end}}' ./... | xargs -n1 go get -d
go test ./...
.PHONY: all clean deps format test updatedeps
.PHONY: all format test
+10 -45
View File
@@ -9,31 +9,20 @@ from a single source configuration.
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 the following platforms:
* Amazon EC2 (AMI). Both EBS-backed and instance-store AMIs
* DigitalOcean
* Docker
* Google Compute Engine
* OpenStack
* Parallels
* QEMU. Both KVM and Xen images.
* VirtualBox
* VMware
Support for other platforms can be added via plugins.
The images that Packer creates can easily be turned into
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. The images that Packer creates an easily be turned into
[Vagrant](http://www.vagrantup.com) boxes.
## Quick Start
**Note:** There is a great
[introduction and getting started guide](http://www.packer.io/intro)
[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, [download a pre-built Packer binary](http://www.packer.io/downloads.html)
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
@@ -52,7 +41,7 @@ own.
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-example {{timestamp}}"
"ami_name": "packer-example {{.CreateTime}}"
}]
}
```
@@ -79,22 +68,8 @@ http://www.packer.io/docs
## Developing Packer
If you wish to work on Packer itself, you'll first need [Go](http://golang.org)
installed (version 1.2+ is _required_). Make sure you have Go properly installed,
including setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH).
For some additional dependencies, Go needs [Mercurial](http://mercurial.selenic.com/)
and [Bazaar](http://bazaar.canonical.com/en/) to be installed.
Packer itself doesn't require these, but a dependency of a dependency does.
You'll also need [`gox`](https://github.com/mitchellh/gox)
to compile packer. You can install that with:
```
$ go get -u github.com/mitchellh/gox
```
Next, clone this repository into `$GOPATH/src/github.com/mitchellh/packer` and
then just type `make`. In a few moments, you'll have a working `packer` executable:
installed (version 1.1+ is _required_). Next, clone this repository then just type `make`.
In a few moments, you'll have a working `packer` executable:
```
$ make
@@ -103,15 +78,5 @@ $ bin/packer
...
```
If you need to cross-compile Packer for other platforms, take a look at
`scripts/dist.sh`.
You can run tests by typing `make test`.
This will run tests for Packer core along with all the core builders and commands and such that come with Packer.
If you make any changes to the code, run `make format` in order to automatically
format the code according to Go standards.
When new dependencies are added to packer you can use `make updatedeps` to
get the latest and subsequently use `make` to compile and generate the `packer` binary.
You can run tests by typing `make test`. This will run tests for Packer core
along with all the core builders and commands and such that come with Packer.
+7
View File
@@ -0,0 +1,7 @@
* builder/amazonebs: Copy AMI to multiple regions
* 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
* provisioner/shell: Arguments
-253
View File
@@ -1,253 +0,0 @@
// The chroot package is able to create an Amazon AMI without requiring
// the launch of a new instance for every build. It does this by attaching
// and mounting the root volume of another AMI and chrooting into that
// directory. It then creates an AMI from that attached drive.
package chroot
import (
"errors"
"fmt"
"log"
"runtime"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazon.chroot"
// Config is the configuration that is chained through the steps and
// settable from the template.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CommandWrapper string `mapstructure:"command_wrapper"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
MountPath string `mapstructure:"mount_path"`
SourceAmi string `mapstructure:"source_ami"`
tpl *packer.ConfigTemplate
}
type wrappedCommandTemplate struct {
Command string
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return nil, err
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
b.config.tpl.UserVars = b.config.PackerUserVars
b.config.tpl.Funcs(awscommon.TemplateFuncs)
// Defaults
if b.config.ChrootMounts == nil {
b.config.ChrootMounts = make([][]string, 0)
}
if b.config.CopyFiles == nil {
b.config.CopyFiles = make([]string, 0)
}
if len(b.config.ChrootMounts) == 0 {
b.config.ChrootMounts = [][]string{
[]string{"proc", "proc", "/proc"},
[]string{"sysfs", "sysfs", "/sys"},
[]string{"bind", "/dev", "/dev"},
[]string{"devpts", "devpts", "/dev/pts"},
[]string{"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"},
}
}
if len(b.config.CopyFiles) == 0 {
b.config.CopyFiles = []string{"/etc/resolv.conf"}
}
if b.config.CommandWrapper == "" {
b.config.CommandWrapper = "{{.Command}}"
}
if b.config.MountPath == "" {
b.config.MountPath = "/mnt/packer-amazon-chroot-volumes/{{.Device}}"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
for i, mounts := range b.config.ChrootMounts {
if len(mounts) != 3 {
errs = packer.MultiErrorAppend(
errs, errors.New("Each chroot_mounts entry should be three elements."))
break
}
for j, entry := range mounts {
b.config.ChrootMounts[i][j], err = b.config.tpl.Process(entry, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing chroot_mounts[%d][%d]: %s",
i, j, err))
}
}
}
for i, file := range b.config.CopyFiles {
var err error
b.config.CopyFiles[i], err = b.config.tpl.Process(file, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing copy_files[%d]: %s",
i, err))
}
}
if b.config.SourceAmi == "" {
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
}
templates := map[string]*string{
"device_path": &b.config.DevicePath,
"source_ami": &b.config.SourceAmi,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
if runtime.GOOS != "linux" {
return nil, errors.New("The amazon-chroot builder only works on Linux environments.")
}
region, err := b.config.Region()
if err != nil {
return nil, err
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
wrappedCommand := func(command string) (string, error) {
return b.config.tpl.Process(
b.config.CommandWrapper, &wrappedCommandTemplate{
Command: command,
})
}
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("ec2", ec2conn)
state.Put("hook", hook)
state.Put("ui", ui)
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
// Build the steps
steps := []multistep.Step{
&StepInstanceInfo{},
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
},
&StepCheckRootDevice{},
&StepFlock{},
&StepPrepareDevice{},
&StepCreateVolume{},
&StepAttachVolume{},
&StepEarlyUnflock{},
&StepMountDevice{},
&StepMountExtra{},
&StepCopyFiles{},
&StepChrootProvision{},
&StepEarlyCleanup{},
&StepSnapshot{},
&StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state.GetOk("amis"); !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state.Get("amis").(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
-119
View File
@@ -1,119 +0,0 @@
package chroot
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"ami_name": "foo",
"source_ami": "foo",
}
}
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 TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_ChrootMounts(t *testing.T) {
b := &Builder{}
config := testConfig()
config["chroot_mounts"] = nil
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Errorf("err: %s", err)
}
config["chroot_mounts"] = [][]string{
[]string{"bad"},
}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SourceAmi(t *testing.T) {
b := &Builder{}
config := testConfig()
config["source_ami"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
config["source_ami"] = "foo"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_CommandWrapper(t *testing.T) {
b := &Builder{}
config := testConfig()
config["command_wrapper"] = "echo hi; {{.Command}}"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Errorf("err: %s", err)
}
}
-10
View File
@@ -1,10 +0,0 @@
package chroot
import (
"github.com/mitchellh/multistep"
)
// Cleanup is an interface that some steps implement for early cleanup.
type Cleanup interface {
CleanupFunc(multistep.StateBag) error
}
-15
View File
@@ -1,15 +0,0 @@
package chroot
import (
"os/exec"
)
// CommandWrapper is a type that given a command, will possibly modify that
// command in-flight. This might return an error.
type CommandWrapper func(string) (string, error)
// ShellCommand takes a command string and returns an *exec.Cmd to execute
// it within the context of a shell (/bin/sh).
func ShellCommand(command string) *exec.Cmd {
return exec.Command("/bin/sh", "-c", command)
}
-131
View File
@@ -1,131 +0,0 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
// Communicator is a special communicator that works by executing
// commands locally but within a chroot.
type Communicator struct {
Chroot string
CmdWrapper CommandWrapper
}
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
command, err := c.CmdWrapper(
fmt.Sprintf("chroot %s /bin/sh -c \"%s\"", c.Chroot, cmd.Command))
if err != nil {
return err
}
localCmd := ShellCommand(command)
localCmd.Stdin = cmd.Stdin
localCmd.Stdout = cmd.Stdout
localCmd.Stderr = cmd.Stderr
log.Printf("Executing: %s %#v", localCmd.Path, localCmd.Args)
if err := localCmd.Start(); err != nil {
return err
}
go func() {
exitStatus := 0
if err := localCmd.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus = 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
}
}
log.Printf(
"Chroot execution exited with '%d': '%s'",
exitStatus, cmd.Command)
cmd.SetExited(exitStatus)
}()
return nil
}
func (c *Communicator) Upload(dst string, r io.Reader) error {
dst = filepath.Join(c.Chroot, dst)
log.Printf("Uploading to chroot dir: %s", dst)
tf, err := ioutil.TempFile("", "packer-amazon-chroot")
if err != nil {
return fmt.Errorf("Error preparing shell script: %s", err)
}
defer os.Remove(tf.Name())
io.Copy(tf, r)
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp %s %s", tf.Name(), dst))
if err != nil {
return err
}
return ShellCommand(cpCmd).Run()
}
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
// If src ends with a trailing "/", copy from "src/." so that
// directory contents (including hidden files) are copied, but the
// directory "src" is omitted. BSD does this automatically when
// the source contains a trailing slash, but linux does not.
if src[len(src)-1] == '/' {
src = src + "."
}
// TODO: remove any file copied if it appears in `exclude`
chrootDest := filepath.Join(c.Chroot, dst)
log.Printf("Uploading directory '%s' to '%s'", src, chrootDest)
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R '%s' %s", src, chrootDest))
if err != nil {
return err
}
var stderr bytes.Buffer
cmd := ShellCommand(cpCmd)
cmd.Env = append(cmd.Env, "LANG=C")
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Stderr = &stderr
err = cmd.Run()
if err == nil {
return err
}
if strings.Contains(stderr.String(), "No such file") {
// This just means that the directory was empty. Just ignore it.
return nil
}
return err
}
func (c *Communicator) Download(src string, w io.Writer) error {
src = filepath.Join(c.Chroot, src)
log.Printf("Downloading from chroot dir: %s", src)
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return err
}
return nil
}
@@ -1,14 +0,0 @@
package chroot
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
var raw interface{}
raw = &Communicator{}
if _, ok := raw.(packer.Communicator); !ok {
t.Fatalf("Communicator should be a communicator")
}
}
-1
View File
@@ -1 +0,0 @@
package chroot
-49
View File
@@ -1,49 +0,0 @@
package chroot
import (
"fmt"
"io/ioutil"
"os"
"runtime"
"testing"
)
func TestCopyFile(t *testing.T) {
if runtime.GOOS == "windows" {
return
}
first, err := ioutil.TempFile("", "copy_files_test")
if err != nil {
t.Fatalf("couldn't create temp file.")
}
defer os.Remove(first.Name())
newName := first.Name() + "-new"
payload := "copy_files_test.go payload"
if _, err = first.WriteString(payload); err != nil {
t.Fatalf("Couldn't write payload to first file.")
}
first.Sync()
cmd := ShellCommand(fmt.Sprintf("cp %s %s", first.Name(), newName))
if err := cmd.Run(); err != nil {
t.Fatalf("Couldn't copy file")
}
defer os.Remove(newName)
second, err := os.Open(newName)
if err != nil {
t.Fatalf("Couldn't open copied file.")
}
defer second.Close()
var copiedPayload = make([]byte, len(payload))
if _, err := second.Read(copiedPayload); err != nil {
t.Fatalf("Couldn't open copied file for reading.")
}
if string(copiedPayload) != payload {
t.Fatalf("payload not copied.")
}
}
-69
View File
@@ -1,69 +0,0 @@
package chroot
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// AvailableDevice finds an available device and returns it. Note that
// you should externally hold a flock or something in order to guarantee
// that this device is available across processes.
func AvailableDevice() (string, error) {
prefix, err := devicePrefix()
if err != nil {
return "", err
}
letters := "fghijklmnop"
for _, letter := range letters {
device := fmt.Sprintf("/dev/%s%c", prefix, letter)
// If the block device itself, i.e. /dev/sf, exists, then we
// can't use any of the numbers either.
if _, err := os.Stat(device); err == nil {
continue
}
for i := 1; i < 16; i++ {
device := fmt.Sprintf("/dev/%s%c%d", prefix, letter, i)
if _, err := os.Stat(device); err != nil {
return device, nil
}
}
}
return "", errors.New("available device could not be found")
}
// devicePrefix returns the prefix ("sd" or "xvd" or so on) of the devices
// on the system.
func devicePrefix() (string, error) {
available := []string{"sd", "xvd"}
f, err := os.Open("/sys/block")
if err != nil {
return "", err
}
defer f.Close()
dirs, err := f.Readdirnames(-1)
if dirs != nil && len(dirs) > 0 {
for _, dir := range dirs {
dirBase := filepath.Base(dir)
for _, prefix := range available {
if strings.HasPrefix(dirBase, prefix) {
return prefix, nil
}
}
}
}
if err != nil {
return "", err
}
return "", errors.New("device prefix could not be detected")
}
-16
View File
@@ -1,16 +0,0 @@
// +build windows
package chroot
import (
"errors"
"os"
)
func lockFile(*os.File) error {
return errors.New("not supported on Windows")
}
func unlockFile(f *os.File) error {
return nil
}
-26
View File
@@ -1,26 +0,0 @@
// +build !windows
package chroot
import (
"os"
"syscall"
)
// See: http://linux.die.net/include/sys/file.h
const LOCK_EX = 2
const LOCK_NB = 4
const LOCK_UN = 8
func lockFile(f *os.File) error {
err := syscall.Flock(int(f.Fd()), LOCK_EX)
if err != nil {
return err
}
return nil
}
func unlockFile(f *os.File) error {
return syscall.Flock(int(f.Fd()), LOCK_UN)
}
-128
View File
@@ -1,128 +0,0 @@
package chroot
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strings"
)
// StepAttachVolume attaches the previously created volume to an
// available device location.
//
// Produces:
// device string - The location where the volume was attached.
// attach_cleanup CleanupFunc
type StepAttachVolume struct {
attached bool
volumeId string
}
func (s *StepAttachVolume) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
device := state.Get("device").(string)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
volumeId := state.Get("volume_id").(string)
// For the API call, it expects "sd" prefixed devices.
attachVolume := strings.Replace(device, "/xvd", "/sd", 1)
ui.Say(fmt.Sprintf("Attaching the root volume to %s", attachVolume))
_, err := ec2conn.AttachVolume(volumeId, instance.InstanceId, attachVolume)
if err != nil {
err := fmt.Errorf("Error attaching volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Mark that we attached it so we can detach it later
s.attached = true
s.volumeId = volumeId
// Wait for the volume to become attached
stateChange := awscommon.StateChangeConf{
Pending: []string{"attaching"},
StepState: state,
Target: "attached",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.Volumes([]string{volumeId}, ec2.NewFilter())
if err != nil {
return nil, "", err
}
if len(resp.Volumes[0].Attachments) == 0 {
return nil, "", errors.New("No attachments on volume.")
}
a := resp.Volumes[0].Attachments[0]
return a, a.Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("attach_cleanup", s)
return multistep.ActionContinue
}
func (s *StepAttachVolume) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
if !s.attached {
return nil
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Detaching EBS volume...")
_, err := ec2conn.DetachVolume(s.volumeId)
if err != nil {
return fmt.Errorf("Error detaching EBS volume: %s", err)
}
s.attached = false
// Wait for the volume to detach
stateChange := awscommon.StateChangeConf{
Pending: []string{"attaching", "attached", "detaching"},
StepState: state,
Target: "detached",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.Volumes([]string{s.volumeId}, ec2.NewFilter())
if err != nil {
return nil, "", err
}
v := resp.Volumes[0]
if len(v.Attachments) > 0 {
return v, v.Attachments[0].Status, nil
} else {
return v, "detached", nil
}
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
return fmt.Errorf("Error waiting for volume: %s", err)
}
return nil
}
@@ -1,11 +0,0 @@
package chroot
import "testing"
func TestAttachVolumeCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
var raw interface{}
raw = new(StepAttachVolume)
if _, ok := raw.(Cleanup); !ok {
t.Fatalf("cleanup func should be a CleanupFunc")
}
}
@@ -1,31 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepCheckRootDevice makes sure the root device on the AMI is EBS-backed.
type StepCheckRootDevice struct{}
func (s *StepCheckRootDevice) Run(state multistep.StateBag) multistep.StepAction {
image := state.Get("ec2").(*ec2.Image)
ui := state.Get("ui").(packer.Ui)
ui.Say("Checking the root device on source AMI...")
// It must be EBS-backed otherwise the build won't work
if image.RootDeviceType != "ebs" {
err := fmt.Errorf("The root device of the source AMI must be EBS-backed.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepCheckRootDevice) Cleanup(multistep.StateBag) {}
@@ -1,36 +0,0 @@
package chroot
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// StepChrootProvision provisions the instance within a chroot.
type StepChrootProvision struct {
mounts []string
}
func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction {
hook := state.Get("hook").(packer.Hook)
mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
// Create our communicator
comm := &Communicator{
Chroot: mountPath,
CmdWrapper: wrappedCommand,
}
// Provision
log.Println("Running the provision hook")
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepChrootProvision) Cleanup(state multistep.StateBag) {}
-89
View File
@@ -1,89 +0,0 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"path/filepath"
)
// StepCopyFiles copies some files from the host into the chroot environment.
//
// Produces:
// copy_files_cleanup CleanupFunc - A function to clean up the copied files
// early.
type StepCopyFiles struct {
files []string
}
func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
stderr := new(bytes.Buffer)
s.files = make([]string, 0, len(config.CopyFiles))
if len(config.CopyFiles) > 0 {
ui.Say("Copying files from host to chroot...")
for _, path := range config.CopyFiles {
ui.Message(path)
chrootPath := filepath.Join(mountPath, path)
log.Printf("Copying '%s' to '%s'", path, chrootPath)
cmdText, err := wrappedCommand(fmt.Sprintf("cp --remove-destination %s %s", path, chrootPath))
if err != nil {
err := fmt.Errorf("Error building copy command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
stderr.Reset()
cmd := ShellCommand(cmdText)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
err := fmt.Errorf(
"Error copying file: %s\nnStderr: %s", err, stderr.String())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.files = append(s.files, chrootPath)
}
}
state.Put("copy_files_cleanup", s)
return multistep.ActionContinue
}
func (s *StepCopyFiles) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepCopyFiles) CleanupFunc(state multistep.StateBag) error {
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
if s.files != nil {
for _, file := range s.files {
log.Printf("Removing: %s", file)
localCmdText, err := wrappedCommand(fmt.Sprintf("rm -f %s", file))
if err != nil {
return err
}
localCmd := ShellCommand(localCmdText)
if err := localCmd.Run(); err != nil {
return err
}
}
}
s.files = nil
return nil
}
@@ -1,11 +0,0 @@
package chroot
import "testing"
func TestCopyFilesCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
var raw interface{}
raw = new(StepCopyFiles)
if _, ok := raw.(Cleanup); !ok {
t.Fatalf("cleanup func should be a CleanupFunc")
}
}
-107
View File
@@ -1,107 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"log"
)
// StepCreateVolume creates a new volume from the snapshot of the root
// device of the AMI.
//
// Produces:
// volume_id string - The ID of the created volume
type StepCreateVolume struct {
volumeId string
}
func (s *StepCreateVolume) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
image := state.Get("source_image").(*ec2.Image)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
// Determine the root device snapshot
log.Printf("Searching for root device of the image (%s)", image.RootDeviceName)
var rootDevice *ec2.BlockDeviceMapping
for _, device := range image.BlockDevices {
if device.DeviceName == image.RootDeviceName {
rootDevice = &device
break
}
}
if rootDevice == nil {
err := fmt.Errorf("Couldn't find root device!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating the root volume...")
createVolume := &ec2.CreateVolume{
AvailZone: instance.AvailZone,
Size: rootDevice.VolumeSize,
SnapshotId: rootDevice.SnapshotId,
VolumeType: rootDevice.VolumeType,
IOPS: rootDevice.IOPS,
}
log.Printf("Create args: %#v", createVolume)
createVolumeResp, err := ec2conn.CreateVolume(createVolume)
if err != nil {
err := fmt.Errorf("Error creating root volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the volume ID so we remember to delete it later
s.volumeId = createVolumeResp.VolumeId
log.Printf("Volume ID: %s", s.volumeId)
// Wait for the volume to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"creating"},
StepState: state,
Target: "available",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.Volumes([]string{s.volumeId}, ec2.NewFilter())
if err != nil {
return nil, "", err
}
v := resp.Volumes[0]
return v, v.Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("volume_id", s.volumeId)
return multistep.ActionContinue
}
func (s *StepCreateVolume) Cleanup(state multistep.StateBag) {
if s.volumeId == "" {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting the created EBS volume...")
_, err := ec2conn.DeleteVolume(s.volumeId)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting EBS volume: %s", err))
}
}
@@ -1,37 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// StepEarlyCleanup performs some of the cleanup steps early in order to
// prepare for snapshotting and creating an AMI.
type StepEarlyCleanup struct{}
func (s *StepEarlyCleanup) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
cleanupKeys := []string{
"copy_files_cleanup",
"mount_extra_cleanup",
"mount_device_cleanup",
"attach_cleanup",
}
for _, key := range cleanupKeys {
c := state.Get(key).(Cleanup)
log.Printf("Running cleanup func: %s", key)
if err := c.CleanupFunc(state); err != nil {
err := fmt.Errorf("Error cleaning up: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepEarlyCleanup) Cleanup(state multistep.StateBag) {}
@@ -1,28 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// StepEarlyUnflock unlocks the flock.
type StepEarlyUnflock struct{}
func (s *StepEarlyUnflock) Run(state multistep.StateBag) multistep.StepAction {
cleanup := state.Get("flock_cleanup").(Cleanup)
ui := state.Get("ui").(packer.Ui)
log.Println("Unlocking file lock...")
if err := cleanup.CleanupFunc(state); err != nil {
err := fmt.Errorf("Error unlocking file lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepEarlyUnflock) Cleanup(state multistep.StateBag) {}
-72
View File
@@ -1,72 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"path/filepath"
)
// StepFlock provisions the instance within a chroot.
//
// Produces:
// flock_cleanup Cleanup - To perform early cleanup
type StepFlock struct {
fh *os.File
}
func (s *StepFlock) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
lockfile := "/var/lock/packer-chroot/lock"
if err := os.MkdirAll(filepath.Dir(lockfile), 0755); err != nil {
err := fmt.Errorf("Error creating lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Obtaining lock: %s", lockfile)
f, err := os.Create(lockfile)
if err != nil {
err := fmt.Errorf("Error creating lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// LOCK!
if err := lockFile(f); err != nil {
err := fmt.Errorf("Error obtaining lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the file handle, we can't close it because we need to hold
// the lock.
s.fh = f
state.Put("flock_cleanup", s)
return multistep.ActionContinue
}
func (s *StepFlock) Cleanup(state multistep.StateBag) {
s.CleanupFunc(state)
}
func (s *StepFlock) CleanupFunc(state multistep.StateBag) error {
if s.fh == nil {
return nil
}
log.Printf("Unlocking: %s", s.fh.Name())
if err := unlockFile(s.fh); err != nil {
return err
}
s.fh = nil
return nil
}
-11
View File
@@ -1,11 +0,0 @@
package chroot
import "testing"
func TestFlockCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
var raw interface{}
raw = new(StepFlock)
if _, ok := raw.(Cleanup); !ok {
t.Fatalf("cleanup func should be a CleanupFunc")
}
}
@@ -1,57 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// StepInstanceInfo verifies that this builder is running on an EC2 instance.
type StepInstanceInfo struct{}
func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
// Get our own instance ID
ui.Say("Gathering information about this EC2 instance...")
instanceIdBytes, err := aws.GetMetaData("instance-id")
if err != nil {
log.Printf("Error: %s", err)
err := fmt.Errorf(
"Error retrieving the ID of the instance Packer is running on.\n" +
"Please verify Packer is running on a proper AWS EC2 instance.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceId := string(instanceIdBytes)
log.Printf("Instance ID: %s", instanceId)
// Query the entire instance metadata
instancesResp, err := ec2conn.Instances([]string{instanceId}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error getting instance data: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(instancesResp.Reservations) == 0 {
err := fmt.Errorf("Error getting instance data: no instance found.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instance := &instancesResp.Reservations[0].Instances[0]
state.Put("instance", instance)
return multistep.ActionContinue
}
func (s *StepInstanceInfo) Cleanup(multistep.StateBag) {}
-117
View File
@@ -1,117 +0,0 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"path/filepath"
)
type mountPathData struct {
Device string
}
// StepMountDevice mounts the attached device.
//
// Produces:
// mount_path string - The location where the volume was mounted.
// mount_device_cleanup CleanupFunc - To perform early cleanup
type StepMountDevice struct {
mountPath string
}
func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
device := state.Get("device").(string)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
mountPath, err := config.tpl.Process(config.MountPath, &mountPathData{
Device: filepath.Base(device),
})
if err != nil {
err := fmt.Errorf("Error preparing mount directory: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
mountPath, err = filepath.Abs(mountPath)
if err != nil {
err := fmt.Errorf("Error preparing mount directory: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Mount path: %s", mountPath)
if err := os.MkdirAll(mountPath, 0755); err != nil {
err := fmt.Errorf("Error creating mount directory: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Mounting the root device...")
stderr := new(bytes.Buffer)
mountCommand, err := wrappedCommand(
fmt.Sprintf("mount %s %s", device, mountPath))
if err != nil {
err := fmt.Errorf("Error creating mount command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
cmd := ShellCommand(mountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
err := fmt.Errorf(
"Error mounting root volume: %s\nStderr: %s", err, stderr.String())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the mount path so we remember to unmount it later
s.mountPath = mountPath
state.Put("mount_path", s.mountPath)
state.Put("mount_device_cleanup", s)
return multistep.ActionContinue
}
func (s *StepMountDevice) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error {
if s.mountPath == "" {
return nil
}
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
ui.Say("Unmounting the root device...")
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", s.mountPath))
if err != nil {
return fmt.Errorf("Error creating unmount command: %s", err)
}
cmd := ShellCommand(unmountCommand)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Error unmounting root device: %s", err)
}
s.mountPath = ""
return nil
}
@@ -1,11 +0,0 @@
package chroot
import "testing"
func TestMountDeviceCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
var raw interface{}
raw = new(StepMountDevice)
if _, ok := raw.(Cleanup); !ok {
t.Fatalf("cleanup func should be a CleanupFunc")
}
}
-109
View File
@@ -1,109 +0,0 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
// StepMountExtra mounts the attached device.
//
// Produces:
// mount_extra_cleanup CleanupFunc - To perform early cleanup
type StepMountExtra struct {
mounts []string
}
func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
s.mounts = make([]string, 0, len(config.ChrootMounts))
ui.Say("Mounting additional paths within the chroot...")
for _, mountInfo := range config.ChrootMounts {
innerPath := mountPath + mountInfo[2]
if err := os.MkdirAll(innerPath, 0755); err != nil {
err := fmt.Errorf("Error creating mount directory: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
flags := "-t " + mountInfo[0]
if mountInfo[0] == "bind" {
flags = "--bind"
}
ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2]))
stderr := new(bytes.Buffer)
mountCommand, err := wrappedCommand(fmt.Sprintf(
"mount %s %s %s",
flags,
mountInfo[1],
innerPath))
if err != nil {
err := fmt.Errorf("Error creating mount command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
cmd := ShellCommand(mountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
err := fmt.Errorf(
"Error mounting: %s\nStderr: %s", err, stderr.String())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.mounts = append(s.mounts, innerPath)
}
state.Put("mount_extra_cleanup", s)
return multistep.ActionContinue
}
func (s *StepMountExtra) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
return
}
}
func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error {
if s.mounts == nil {
return nil
}
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
for len(s.mounts) > 0 {
var path string
lastIndex := len(s.mounts) - 1
path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex]
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", path))
if err != nil {
return fmt.Errorf("Error creating unmount command: %s", err)
}
stderr := new(bytes.Buffer)
cmd := ShellCommand(unmountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf(
"Error unmounting device: %s\nStderr: %s", err, stderr.String())
}
}
s.mounts = nil
return nil
}
@@ -1,11 +0,0 @@
package chroot
import "testing"
func TestMountExtraCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
var raw interface{}
raw = new(StepMountExtra)
if _, ok := raw.(Cleanup); !ok {
t.Fatalf("cleanup func should be a CleanupFunc")
}
}
@@ -1,45 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
)
// StepPrepareDevice finds an available device and sets it.
type StepPrepareDevice struct {
mounts []string
}
func (s *StepPrepareDevice) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
device := config.DevicePath
if device == "" {
var err error
log.Println("Device path not specified, searching for available device...")
device, err = AvailableDevice()
if err != nil {
err := fmt.Errorf("Error finding available device: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, err := os.Stat(device); err == nil {
err := fmt.Errorf("Device is in use: %s", device)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Device: %s", device)
state.Put("device", device)
return multistep.ActionContinue
}
func (s *StepPrepareDevice) Cleanup(state multistep.StateBag) {}
@@ -1,79 +0,0 @@
package chroot
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
// StepRegisterAMI creates the AMI.
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
image := state.Get("source_image").(*ec2.Image)
snapshotId := state.Get("snapshot_id").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Registering the AMI...")
blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))
for i, device := range image.BlockDevices {
newDevice := device
if newDevice.DeviceName == image.RootDeviceName {
newDevice.SnapshotId = snapshotId
}
blockDevices[i] = newDevice
}
registerOpts := &ec2.RegisterImage{
Name: config.AMIName,
Architecture: image.Architecture,
KernelId: image.KernelId,
RamdiskId: image.RamdiskId,
RootDeviceName: image.RootDeviceName,
BlockDevices: blockDevices,
}
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
if config.AMIEnhancedNetworking {
registerOpts.SriovNetSupport = "simple"
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state.Put("error", fmt.Errorf("Error registering AMI: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
// Set the AMI ID in the state
ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId))
amis := make(map[string]string)
amis[ec2conn.Region.Name] = registerResp.ImageId
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, registerResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) {}
-87
View File
@@ -1,87 +0,0 @@
package chroot
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
// StepSnapshot creates a snapshot of the created volume.
//
// Produces:
// snapshot_id string - ID of the created snapshot
type StepSnapshot struct {
snapshotId string
}
func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
volumeId := state.Get("volume_id").(string)
ui.Say("Creating snapshot...")
createSnapResp, err := ec2conn.CreateSnapshot(volumeId, "")
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the snapshot ID so we can delete it later
s.snapshotId = createSnapResp.Id
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
// Wait for the snapshot to be ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
StepState: state,
Target: "completed",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.Snapshots([]string{s.snapshotId}, ec2.NewFilter())
if err != nil {
return nil, "", err
}
if len(resp.Snapshots) == 0 {
return nil, "", errors.New("No snapshots found.")
}
s := resp.Snapshots[0]
return s, s.Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("snapshot_id", s.snapshotId)
return multistep.ActionContinue
}
func (s *StepSnapshot) Cleanup(state multistep.StateBag) {
if s.snapshotId == "" {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Removing snapshot since we cancelled or halted...")
_, err := ec2conn.DeleteSnapshots([]string{s.snapshotId})
if err != nil {
ui.Error(fmt.Sprintf("Error: %s", err))
}
}
}
-83
View File
@@ -1,83 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/packer/packer"
"strings"
"unicode"
)
// AccessConfig is for common configuration related to AWS access
type AccessConfig struct {
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
RawRegion string `mapstructure:"region"`
}
// Auth returns a valid aws.Auth object for access to AWS services, or
// an error if the authentication couldn't be resolved.
func (c *AccessConfig) Auth() (aws.Auth, error) {
auth, err := aws.GetAuth(c.AccessKey, c.SecretKey)
if err == nil {
// Store the accesskey and secret that we got...
c.AccessKey = auth.AccessKey
c.SecretKey = auth.SecretKey
}
return auth, err
}
// Region returns the aws.Region object for access to AWS services, requesting
// the region from the instance metadata if possible.
func (c *AccessConfig) Region() (aws.Region, error) {
if c.RawRegion != "" {
return aws.Regions[c.RawRegion], nil
}
md, err := aws.GetMetaData("placement/availability-zone")
if err != nil {
return aws.Region{}, err
}
region := strings.TrimRightFunc(string(md), unicode.IsLetter)
return aws.Regions[region], nil
}
func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
templates := map[string]*string{
"access_key": &c.AccessKey,
"secret_key": &c.SecretKey,
"region": &c.RawRegion,
}
errs := make([]error, 0)
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if c.RawRegion != "" {
if _, ok := aws.Regions[c.RawRegion]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", c.RawRegion))
}
}
if len(errs) > 0 {
return errs
}
return nil
}
@@ -1,27 +0,0 @@
package common
import (
"testing"
)
func testAccessConfig() *AccessConfig {
return &AccessConfig{}
}
func TestAccessConfigPrepare_Region(t *testing.T) {
c := testAccessConfig()
c.RawRegion = ""
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.RawRegion = "us-east-12"
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
c.RawRegion = "us-east-1"
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
-121
View File
@@ -1,121 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/packer/packer"
)
// AMIConfig is for common configuration related to creating AMIs.
type AMIConfig struct {
AMIName string `mapstructure:"ami_name"`
AMIDescription string `mapstructure:"ami_description"`
AMIVirtType string `mapstructure:"ami_virtualization_type"`
AMIUsers []string `mapstructure:"ami_users"`
AMIGroups []string `mapstructure:"ami_groups"`
AMIProductCodes []string `mapstructure:"ami_product_codes"`
AMIRegions []string `mapstructure:"ami_regions"`
AMITags map[string]string `mapstructure:"tags"`
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
}
func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
templates := map[string]*string{
"ami_name": &c.AMIName,
"ami_description": &c.AMIDescription,
"ami_virtualization_type": &c.AMIVirtType,
}
errs := make([]error, 0)
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
sliceTemplates := map[string][]string{
"ami_users": c.AMIUsers,
"ami_groups": c.AMIGroups,
"ami_product_codes": c.AMIProductCodes,
"ami_regions": c.AMIRegions,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = t.Process(elem, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
if c.AMIName == "" {
errs = append(errs, fmt.Errorf("ami_name must be specified"))
}
if len(c.AMIRegions) > 0 {
regionSet := make(map[string]struct{})
regions := make([]string, 0, len(c.AMIRegions))
for _, region := range c.AMIRegions {
// If we already saw the region, then don't look again
if _, ok := regionSet[region]; ok {
continue
}
// Mark that we saw the region
regionSet[region] = struct{}{}
// Verify the region is real
if _, ok := aws.Regions[region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", region))
continue
}
regions = append(regions, region)
}
c.AMIRegions = regions
}
newTags := make(map[string]string)
for k, v := range c.AMITags {
k, err := t.Process(k, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag key %s: %s", k, err))
continue
}
v, err := t.Process(v, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag value '%s': %s", v, err))
continue
}
newTags[k] = v
}
c.AMITags = newTags
if len(errs) > 0 {
return errs
}
return nil
}
-47
View File
@@ -1,47 +0,0 @@
package common
import (
"reflect"
"testing"
)
func testAMIConfig() *AMIConfig {
return &AMIConfig{
AMIName: "foo",
}
}
func TestAMIConfigPrepare_name(t *testing.T) {
c := testAMIConfig()
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.AMIName = ""
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
}
func TestAMIConfigPrepare_regions(t *testing.T) {
c := testAMIConfig()
c.AMIRegions = nil
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.AMIRegions = []string{"foo"}
if err := c.Prepare(nil); err == nil {
t.Fatal("should have error")
}
c.AMIRegions = []string{"us-east-1", "us-west-1", "us-east-1"}
if err := c.Prepare(nil); err != nil {
t.Fatalf("bad: %s", err)
}
expected := []string{"us-east-1", "us-west-1"}
if !reflect.DeepEqual(c.AMIRegions, expected) {
t.Fatalf("bad: %#v", c.AMIRegions)
}
}
-76
View File
@@ -1,76 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/packer"
"log"
"sort"
"strings"
)
// Artifact is an artifact implementation that contains built AMIs.
type Artifact struct {
// A map of regions to AMI IDs.
Amis map[string]string
// BuilderId is the unique ID for the builder that created this AMI
BuilderIdValue string
// EC2 connection for performing API stuff.
Conn *ec2.EC2
}
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*Artifact) Files() []string {
// We have no files
return nil
}
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 {
amiStrings := make([]string, 0, len(a.Amis))
for region, id := range a.Amis {
single := fmt.Sprintf("%s: %s", region, id)
amiStrings = append(amiStrings, single)
}
sort.Sort(sort.StringSlice(amiStrings))
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *Artifact) Destroy() error {
errors := make([]error, 0)
for region, imageId := range a.Amis {
log.Printf("Deregistering image ID (%s) from region (%s)", imageId, region)
regionconn := ec2.New(a.Conn.Auth, aws.Regions[region])
if _, err := regionconn.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
}
-50
View File
@@ -1,50 +0,0 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
)
// BlockDevice
type BlockDevice struct {
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
DeviceName string `mapstructure:"device_name"`
Encrypted bool `mapstructure:"encrypted"`
IOPS int64 `mapstructure:"iops"`
NoDevice bool `mapstructure:"no_device"`
SnapshotId string `mapstructure:"snapshot_id"`
VirtualName string `mapstructure:"virtual_name"`
VolumeType string `mapstructure:"volume_type"`
VolumeSize int64 `mapstructure:"volume_size"`
}
type BlockDevices struct {
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings"`
}
func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
var blockDevices []ec2.BlockDeviceMapping
for _, blockDevice := range b {
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: blockDevice.DeviceName,
VirtualName: blockDevice.VirtualName,
SnapshotId: blockDevice.SnapshotId,
VolumeType: blockDevice.VolumeType,
VolumeSize: blockDevice.VolumeSize,
DeleteOnTermination: blockDevice.DeleteOnTermination,
IOPS: blockDevice.IOPS,
NoDevice: blockDevice.NoDevice,
Encrypted: blockDevice.Encrypted,
})
}
return blockDevices
}
func (b *BlockDevices) BuildAMIDevices() []ec2.BlockDeviceMapping {
return buildBlockDevices(b.AMIMappings)
}
func (b *BlockDevices) BuildLaunchDevices() []ec2.BlockDeviceMapping {
return buildBlockDevices(b.LaunchMappings)
}
@@ -1,44 +0,0 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
"reflect"
"testing"
)
func TestBlockDevice(t *testing.T) {
ec2Mapping := []ec2.BlockDeviceMapping{
ec2.BlockDeviceMapping{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
}
blockDevice := BlockDevice{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
}
blockDevices := BlockDevices{
AMIMappings: []BlockDevice{blockDevice},
LaunchMappings: []BlockDevice{blockDevice},
}
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildAMIDevices()) {
t.Fatalf("bad: %#v", ec2Mapping)
}
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildLaunchDevices()) {
t.Fatalf("bad: %#v", ec2Mapping)
}
}
-160
View File
@@ -1,160 +0,0 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
"os"
"time"
)
// RunConfig contains configuration for running an instance from a source
// AMI and details on how to access that launched image.
type RunConfig struct {
AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"`
AvailabilityZone string `mapstructure:"availability_zone"`
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
InstanceType string `mapstructure:"instance_type"`
RunTags map[string]string `mapstructure:"run_tags"`
SourceAmi string `mapstructure:"source_ami"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"`
SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.TemporaryKeyPairName == "" {
c.TemporaryKeyPairName = "packer {{uuid}}"
}
// Validation
var err error
errs := make([]error, 0)
if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if c.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {
if _, err := os.Stat(c.UserDataFile); err != nil {
errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
}
}
if c.SecurityGroupId != "" {
if len(c.SecurityGroupIds) > 0 {
errs = append(errs, fmt.Errorf("Only one of security_group_id or security_group_ids can be specified."))
} else {
c.SecurityGroupIds = []string{c.SecurityGroupId}
c.SecurityGroupId = ""
}
}
templates := map[string]*string{
"iam_instance_profile": &c.IamInstanceProfile,
"instance_type": &c.InstanceType,
"ssh_timeout": &c.RawSSHTimeout,
"ssh_username": &c.SSHUsername,
"ssh_private_key_file": &c.SSHPrivateKeyFile,
"source_ami": &c.SourceAmi,
"subnet_id": &c.SubnetId,
"temporary_key_pair_name": &c.TemporaryKeyPairName,
"vpc_id": &c.VpcId,
"availability_zone": &c.AvailabilityZone,
"user_data": &c.UserData,
"user_data_file": &c.UserDataFile,
}
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
sliceTemplates := map[string][]string{
"security_group_ids": c.SecurityGroupIds,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = t.Process(elem, nil)
if err != nil {
errs = append(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
newTags := make(map[string]string)
for k, v := range c.RunTags {
k, err := t.Process(k, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag key %s: %s", k, err))
continue
}
v, err := t.Process(v, nil)
if err != nil {
errs = append(errs,
fmt.Errorf("Error processing tag value '%s': %s", v, err))
continue
}
newTags[k] = v
}
c.RunTags = newTags
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
-140
View File
@@ -1,140 +0,0 @@
package common
import (
"io/ioutil"
"os"
"testing"
)
func init() {
// Clear out the AWS access key env vars so they don't
// affect our tests.
os.Setenv("AWS_ACCESS_KEY_ID", "")
os.Setenv("AWS_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_KEY", "")
}
func testConfig() *RunConfig {
return &RunConfig{
SourceAmi: "abcd",
InstanceType: "m1.small",
SSHUsername: "root",
}
}
func TestRunConfigPrepare(t *testing.T) {
c := testConfig()
err := c.Prepare(nil)
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
c.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_UserData(t *testing.T) {
c := testConfig()
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
c.UserData = "foo"
c.UserDataFile = tf.Name()
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_UserDataFile(t *testing.T) {
c := testConfig()
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.UserDataFile = "idontexistidontthink"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
c.UserDataFile = tf.Name()
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_TemporaryKeyPairName(t *testing.T) {
c := testConfig()
c.TemporaryKeyPairName = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.TemporaryKeyPairName == "" {
t.Fatal("keypair empty")
}
}
-69
View File
@@ -1,69 +0,0 @@
package common
import (
"code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"time"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(e *ec2.EC2, port int) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
for j := 0; j < 2; j++ {
var host string
i := state.Get("instance").(*ec2.Instance)
if i.DNSName != "" {
host = i.DNSName
} else if i.VpcId != "" {
if i.PublicIpAddress != "" {
host = i.PublicIpAddress
} else {
host = i.PrivateIpAddress
}
}
if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil
}
r, err := e.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return "", err
}
if len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
return "", fmt.Errorf("instance not found: %s", i.InstanceId)
}
state.Put("instance", &r.Reservations[0].Instances[0])
time.Sleep(1 * time.Second)
}
return "", errors.New("couldn't determine IP address for instance")
}
}
// SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the instance created over SSH using the generated
// private key.
func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, error) {
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
privateKey := state.Get("privateKey").(string)
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
}, nil
}
}
-137
View File
@@ -1,137 +0,0 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"log"
"time"
)
// StateRefreshFunc is a function type used for StateChangeConf that is
// responsible for refreshing the item being watched for a state change.
//
// It returns three results. `result` is any object that will be returned
// as the final object after waiting for state change. This allows you to
// return the final updated object, for example an EC2 instance after refreshing
// it.
//
// `state` is the latest state of that object. And `err` is any error that
// may have happened while refreshing the state.
type StateRefreshFunc func() (result interface{}, state string, err error)
// StateChangeConf is the configuration struct used for `WaitForState`.
type StateChangeConf struct {
Pending []string
Refresh StateRefreshFunc
StepState multistep.StateBag
Target string
}
// AMIStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an AMI for state changes.
func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.Images([]string{imageId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else {
log.Printf("Error on AMIStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil || len(resp.Images) == 0 {
// Sometimes AWS has consistency issues and doesn't see the
// AMI. Return an empty state.
return nil, "", nil
}
i := resp.Images[0]
return i, i.State, nil
}
}
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else {
log.Printf("Error on InstanceStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
i = &resp.Reservations[0].Instances[0]
return i, i.State.Name, nil
}
}
// WaitForState watches an object and waits for it to achieve a certain
// state.
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
notfoundTick := 0
for {
var currentState string
i, currentState, err = conf.Refresh()
if err != nil {
return
}
if i == nil {
// If we didn't find the resource, check if we have been
// not finding it for awhile, and if so, report an error.
notfoundTick += 1
if notfoundTick > 20 {
return nil, errors.New("couldn't find resource")
}
} else {
// Reset the counter for when a resource isn't found
notfoundTick = 0
if currentState == conf.Target {
return
}
if conf.StepState != nil {
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if currentState == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
return
}
}
time.Sleep(2 * time.Second)
}
return
}
@@ -1,98 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"sync"
)
type StepAMIRegionCopy struct {
Regions []string
}
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
amis := state.Get("amis").(map[string]string)
ami := amis[ec2conn.Region.Name]
if len(s.Regions) == 0 {
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Copying AMI (%s) to other regions...", ami))
var lock sync.Mutex
var wg sync.WaitGroup
errs := new(packer.MultiError)
for _, region := range s.Regions {
wg.Add(1)
ui.Message(fmt.Sprintf("Copying to: %s", region))
go func(region string) {
defer wg.Done()
id, err := amiRegionCopy(state, ec2conn.Auth, ami,
aws.Regions[region], ec2conn.Region)
lock.Lock()
defer lock.Unlock()
amis[region] = id
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
}
}(region)
}
// TODO(mitchellh): Wait but also allow for cancels to go through...
ui.Message("Waiting for all copies to complete...")
wg.Wait()
// If there were errors, show them
if len(errs.Errors) > 0 {
state.Put("error", errs)
ui.Error(errs.Error())
return multistep.ActionHalt
}
state.Put("amis", amis)
return multistep.ActionContinue
}
func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
// No cleanup...
}
// amiRegionCopy does a copy for the given AMI to the target region and
// returns the resulting ID or error.
func amiRegionCopy(state multistep.StateBag, auth aws.Auth, imageId string,
target aws.Region, source aws.Region) (string, error) {
// Connect to the region where the AMI will be copied to
regionconn := ec2.New(auth, target)
resp, err := regionconn.CopyImage(&ec2.CopyImage{
SourceRegion: source.Name,
SourceImageId: imageId,
})
if err != nil {
return "", fmt.Errorf("Error Copying AMI (%s) to region (%s): %s",
imageId, target, err)
}
stateChange := StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: AMIStateRefreshFunc(regionconn, resp.ImageId),
StepState: state,
}
if _, err := WaitForState(&stateChange); err != nil {
return "", fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s",
resp.ImageId, target, err)
}
return resp.ImageId, nil
}
-46
View File
@@ -1,46 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepCreateTags struct {
Tags map[string]string
}
func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
amis := state.Get("amis").(map[string]string)
if len(s.Tags) > 0 {
for region, ami := range amis {
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
var ec2Tags []ec2.Tag
for key, value := range s.Tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
}
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
_, err := regionconn.CreateTags([]string{ami}, ec2Tags)
if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepCreateTags) Cleanup(state multistep.StateBag) {
// No cleanup...
}
-99
View File
@@ -1,99 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"runtime"
)
type StepKeyPair struct {
Debug bool
DebugKeyPath string
KeyPairName string
PrivateKeyFile string
keyName string
}
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
if s.PrivateKeyFile != "" {
s.keyName = ""
privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile)
if err != nil {
state.Put("error", fmt.Errorf("Error loading configured private key file: %s", err))
return multistep.ActionHalt
}
state.Put("keyPair", "")
state.Put("privateKey", string(privateKeyBytes))
return multistep.ActionContinue
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say(fmt.Sprintf("Creating temporary keypair: %s", s.KeyPairName))
keyResp, err := ec2conn.CreateKeyPair(s.KeyPairName)
if err != nil {
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
return multistep.ActionHalt
}
// Set the keyname so we know to delete it later
s.keyName = s.KeyPairName
// Set some state data for use in future steps
state.Put("keyPair", s.keyName)
state.Put("privateKey", keyResp.KeyMaterial)
// If we're in debug mode, output the private key to the working
// directory.
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
defer f.Close()
// Write the key out
if _, err := f.Write([]byte(keyResp.KeyMaterial)); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
// Chmod it so that it is SSH ready
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
// If no key name is set, then we never created it, so just return
if s.keyName == "" {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting temporary keypair...")
_, err := ec2conn.DeleteKeyPair(s.keyName)
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
}
}
@@ -1,82 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepModifyAMIAttributes struct {
Users []string
Groups []string
ProductCodes []string
Description string
}
func (s *StepModifyAMIAttributes) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
amis := state.Get("amis").(map[string]string)
// Determine if there is any work to do.
valid := false
valid = valid || s.Description != ""
valid = valid || (s.Users != nil && len(s.Users) > 0)
valid = valid || (s.Groups != nil && len(s.Groups) > 0)
valid = valid || (s.ProductCodes != nil && len(s.ProductCodes) > 0)
if !valid {
return multistep.ActionContinue
}
// Construct the modify image attribute requests we're going to make.
// We need to make each separately since the EC2 API only allows changing
// one type at a kind currently.
options := make(map[string]*ec2.ModifyImageAttribute)
if s.Description != "" {
options["description"] = &ec2.ModifyImageAttribute{
Description: s.Description,
}
}
if len(s.Groups) > 0 {
options["groups"] = &ec2.ModifyImageAttribute{
AddGroups: s.Groups,
}
}
if len(s.Users) > 0 {
options["users"] = &ec2.ModifyImageAttribute{
AddUsers: s.Users,
}
}
if len(s.ProductCodes) > 0 {
options["product codes"] = &ec2.ModifyImageAttribute{
ProductCodes: s.ProductCodes,
}
}
for region, ami := range amis {
ui.Say(fmt.Sprintf("Modifying attributes on AMI (%s)...", ami))
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
for name, opts := range options {
ui.Message(fmt.Sprintf("Modifying: %s", name))
_, err := regionconn.ModifyImageAttribute(ami, opts)
if err != nil {
err := fmt.Errorf("Error modify AMI attributes: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepModifyAMIAttributes) Cleanup(state multistep.StateBag) {
// No cleanup...
}
@@ -1,165 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
)
type StepRunSourceInstance struct {
AssociatePublicIpAddress bool
AvailabilityZone string
BlockDevices BlockDevices
Debug bool
ExpectedRootDevice string
InstanceType string
IamInstanceProfile string
SourceAMI string
SubnetId string
Tags map[string]string
UserData string
UserDataFile string
instance *ec2.Instance
}
func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
keyName := state.Get("keyPair").(string)
securityGroupIds := state.Get("securityGroupIds").([]string)
ui := state.Get("ui").(packer.Ui)
userData := s.UserData
if s.UserDataFile != "" {
contents, err := ioutil.ReadFile(s.UserDataFile)
if err != nil {
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
return multistep.ActionHalt
}
userData = string(contents)
}
securityGroups := make([]ec2.SecurityGroup, len(securityGroupIds))
for n, securityGroupId := range securityGroupIds {
securityGroups[n] = ec2.SecurityGroup{Id: securityGroupId}
}
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
UserData: []byte(userData),
MinCount: 0,
MaxCount: 0,
SecurityGroups: securityGroups,
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
AssociatePublicIpAddress: s.AssociatePublicIpAddress,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
AvailZone: s.AvailabilityZone,
}
ui.Say("Launching a source AWS instance...")
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
if err != nil {
state.Put("error", fmt.Errorf("There was a problem with the source AMI: %s", err))
return multistep.ActionHalt
}
if len(imageResp.Images) != 1 {
state.Put("error", fmt.Errorf("The source AMI '%s' could not be found.", s.SourceAMI))
return multistep.ActionHalt
}
if s.ExpectedRootDevice != "" && imageResp.Images[0].RootDeviceType != s.ExpectedRootDevice {
state.Put("error", fmt.Errorf(
"The provided source AMI has an invalid root device type.\n"+
"Expected '%s', got '%s'.",
s.ExpectedRootDevice, imageResp.Images[0].RootDeviceType))
return multistep.ActionHalt
}
runResp, err := ec2conn.RunInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = &runResp.Instances[0]
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
ec2Tags := make([]ec2.Tag, 1, len(s.Tags)+1)
ec2Tags[0] = ec2.Tag{"Name", "Packer Builder"}
for k, v := range s.Tags {
ec2Tags = append(ec2Tags, ec2.Tag{k, v})
}
_, err = ec2conn.CreateTags([]string{s.instance.InstanceId}, ec2Tags)
if err != nil {
ui.Message(
fmt.Sprintf("Failed to tag a Name on the builder instance: %s", err))
}
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
stateChange := StateChangeConf{
Pending: []string{"pending"},
Target: "running",
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
StepState: state,
}
latestInstance, err := WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = latestInstance.(*ec2.Instance)
if s.Debug {
if s.instance.DNSName != "" {
ui.Message(fmt.Sprintf("Public DNS: %s", s.instance.DNSName))
}
if s.instance.PublicIpAddress != "" {
ui.Message(fmt.Sprintf("Public IP: %s", s.instance.PublicIpAddress))
}
if s.instance.PrivateIpAddress != "" {
ui.Message(fmt.Sprintf("Private IP: %s", s.instance.PrivateIpAddress))
}
}
state.Put("instance", s.instance)
return multistep.ActionContinue
}
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
if s.instance == nil {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
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
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Target: "terminated",
}
WaitForState(&stateChange)
}
@@ -1,115 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type StepSecurityGroup struct {
SecurityGroupIds []string
SSHPort int
VpcId string
createdGroupId string
}
func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
if len(s.SecurityGroupIds) > 0 {
log.Printf("Using specified security groups: %v", s.SecurityGroupIds)
state.Put("securityGroupIds", s.SecurityGroupIds)
return multistep.ActionContinue
}
if s.SSHPort == 0 {
panic("SSHPort must be set to a non-zero value.")
}
// Create the group
ui.Say("Creating temporary security group for this instance...")
groupName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
log.Printf("Temporary group name: %s", groupName)
group := ec2.SecurityGroup{
Name: groupName,
Description: "Temporary group for Packer",
VpcId: s.VpcId,
}
groupResp, err := ec2conn.CreateSecurityGroup(group)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the group ID so we can delete it later
s.createdGroupId = groupResp.Id
// Authorize the SSH access
perms := []ec2.IPPerm{
ec2.IPPerm{
Protocol: "tcp",
FromPort: s.SSHPort,
ToPort: s.SSHPort,
SourceIPs: []string{"0.0.0.0/0"},
},
}
// We loop and retry this a few times because sometimes the security
// group isn't available immediately because AWS resources are eventaully
// consistent.
ui.Say("Authorizing SSH access on the temporary security group...")
for i := 0; i < 5; i++ {
_, err = ec2conn.AuthorizeSecurityGroup(groupResp.SecurityGroup, perms)
if err == nil {
break
}
log.Printf("Error authorizing. Will sleep and retry. %s", err)
time.Sleep((time.Duration(i) * time.Second) + 1)
}
if err != nil {
err := fmt.Errorf("Error creating temporary security group: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set some state data for use in future steps
state.Put("securityGroupIds", []string{s.createdGroupId})
return multistep.ActionContinue
}
func (s *StepSecurityGroup) Cleanup(state multistep.StateBag) {
if s.createdGroupId == "" {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting temporary security group...")
var err error
for i := 0; i < 5; i++ {
_, err = ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
if err == nil {
break
}
log.Printf("Error deleting security group: %s", err)
time.Sleep(5 * time.Second)
}
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
}
}
@@ -1,56 +0,0 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepSourceAMIInfo extracts critical information from the source AMI
// that is used throughout the AMI creation process.
//
// Produces:
// source_image *ec2.Image - the source AMI info
type StepSourceAMIInfo struct {
SourceAmi string
EnhancedNetworking bool
}
func (s *StepSourceAMIInfo) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Inspecting the source AMI...")
imageResp, err := ec2conn.Images([]string{s.SourceAmi}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error querying AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(imageResp.Images) == 0 {
err := fmt.Errorf("Source AMI '%s' was not found!", s.SourceAmi)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
image := &imageResp.Images[0]
// Enhanced Networking (SriovNetSupport) can only be enabled on HVM AMIs.
// See http://goo.gl/icuXh5
if s.EnhancedNetworking && image.VirtualizationType != "hvm" {
err := fmt.Errorf("Cannot enable enhanced networking, source AMI '%s' is not HVM", s.SourceAmi)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("source_image", image)
return multistep.ActionContinue
}
func (s *StepSourceAMIInfo) Cleanup(multistep.StateBag) {}
-38
View File
@@ -1,38 +0,0 @@
package common
import (
"bytes"
"text/template"
)
func isalphanumeric(b byte) bool {
if '0' <= b && b <= '9' {
return true
}
if 'a' <= b && b <= 'z' {
return true
}
if 'A' <= b && b <= 'Z' {
return true
}
return false
}
// Clean up AMI name by replacing invalid characters with "-"
func templateCleanAMIName(s string) string {
allowed := []byte{'(', ')', ',', '/', '-', '_'}
b := []byte(s)
newb := make([]byte, len(b))
for i, c := range b {
if isalphanumeric(c) || bytes.IndexByte(allowed, c) != -1 {
newb[i] = c
} else {
newb[i] = '-'
}
}
return string(newb[:])
}
var TemplateFuncs = template.FuncMap{
"clean_ami_name": templateCleanAMIName,
}
@@ -1,16 +0,0 @@
package common
import (
"testing"
)
func TestAMITemplatePrepare_clean(t *testing.T) {
origName := "AMZamz09(),/-_:&^$%"
expected := "AMZamz09(),/-_-----"
name := templateCleanAMIName(origName)
if name != expected {
t.Fatalf("template names do not match: expected %s got %s\n", expected, name)
}
}
-174
View File
@@ -1,174 +0,0 @@
// The amazonebs package contains a packer.Builder implementation that
// builds AMIs for Amazon EC2.
//
// In general, there are two types of AMIs that can be created: ebs-backed or
// instance-store. This builder _only_ builds ebs-backed images.
package ebs
import (
"fmt"
"log"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazonebs"
type config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
tpl *packer.ConfigTemplate
}
type Builder struct {
config config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return nil, err
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
b.config.tpl.UserVars = b.config.PackerUserVars
b.config.tpl.Funcs(awscommon.TemplateFuncs)
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, err := b.config.Region()
if err != nil {
return nil, err
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("ec2", ec2conn)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
},
&awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
Debug: b.config.PackerDebug,
ExpectedRootDevice: "ebs",
InstanceType: b.config.InstanceType,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
SourceAMI: b.config.SourceAmi,
IamInstanceProfile: b.config.IamInstanceProfile,
SubnetId: b.config.SubnetId,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
AvailabilityZone: b.config.AvailabilityZone,
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
&stepModifyInstance{},
&stepCreateAMI{},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state.GetOk("amis"); !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state.Get("amis").(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
-93
View File
@@ -1,93 +0,0 @@
package ebs
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
"secret_key": "bar",
"source_ami": "foo",
"instance_type": "foo",
"region": "us-east-1",
"ssh_username": "root",
"ami_name": "foo",
}
}
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{}{
"access_key": []string{},
}
warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
-62
View File
@@ -1,62 +0,0 @@
package ebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
type stepCreateAMI struct{}
func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config)
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
// Create the image
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
createOpts := &ec2.CreateImage{
InstanceId: instance.InstanceId,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
}
createResp, err := ec2conn.CreateImage(createOpts)
if err != nil {
err := fmt.Errorf("Error creating AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the AMI ID in the state
ui.Message(fmt.Sprintf("AMI: %s", createResp.ImageId))
amis := make(map[string]string)
amis[ec2conn.Region.Name] = createResp.ImageId
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, createResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCreateAMI) Cleanup(multistep.StateBag) {
// No cleanup...
}
@@ -1,39 +0,0 @@
package ebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepModifyInstance struct{}
func (s *stepModifyInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config)
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
if config.AMIEnhancedNetworking {
ui.Say("Enabling Enhanced Networking...")
_, err := ec2conn.ModifyInstance(
instance.InstanceId,
&ec2.ModifyInstance{SriovNetSupport: true},
)
if err != nil {
err := fmt.Errorf("Error enabling Enhanced Networking on %s: %s", instance.InstanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepModifyInstance) Cleanup(state multistep.StateBag) {
// No cleanup...
}
-280
View File
@@ -1,280 +0,0 @@
// The instance package contains a packer.Builder implementation that builds
// AMIs for Amazon EC2 backed by instance storage, as opposed to EBS storage.
package instance
import (
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazon.instance"
// Config is the configuration that is chained through the steps and
// settable from the template.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
AccountId string `mapstructure:"account_id"`
BundleDestination string `mapstructure:"bundle_destination"`
BundlePrefix string `mapstructure:"bundle_prefix"`
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
tpl *packer.ConfigTemplate
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return nil, err
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
b.config.tpl.UserVars = b.config.PackerUserVars
b.config.tpl.Funcs(awscommon.TemplateFuncs)
if b.config.BundleDestination == "" {
b.config.BundleDestination = "/tmp"
}
if b.config.BundlePrefix == "" {
b.config.BundlePrefix = "image-{{timestamp}}"
}
if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--url {{.S3Endpoint}} " +
"--retry"
}
if b.config.BundleVolCommand == "" {
b.config.BundleVolCommand = "sudo -n ec2-bundle-vol " +
"-k {{.KeyPath}} " +
"-u {{.AccountId}} " +
"-c {{.CertPath}} " +
"-r {{.Architecture}} " +
"-e {{.PrivatePath}}/* " +
"-d {{.Destination}} " +
"-p {{.Prefix}} " +
"--batch"
}
if b.config.X509UploadPath == "" {
b.config.X509UploadPath = "/tmp"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
validates := map[string]*string{
"bundle_upload_command": &b.config.BundleUploadCommand,
"bundle_vol_command": &b.config.BundleVolCommand,
}
for n, ptr := range validates {
if err := b.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
templates := map[string]*string{
"account_id": &b.config.AccountId,
"ami_name": &b.config.AMIName,
"bundle_destination": &b.config.BundleDestination,
"bundle_prefix": &b.config.BundlePrefix,
"s3_bucket": &b.config.S3Bucket,
"x509_cert_path": &b.config.X509CertPath,
"x509_key_path": &b.config.X509KeyPath,
"x509_upload_path": &b.config.X509UploadPath,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
if b.config.AccountId == "" {
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
} else {
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
}
if b.config.S3Bucket == "" {
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
}
if b.config.X509CertPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_cert_path is required"))
} else if _, err := os.Stat(b.config.X509CertPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_cert_path points to bad file: %s", err))
}
if b.config.X509KeyPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_key_path is required"))
} else if _, err := os.Stat(b.config.X509KeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_key_path points to bad file: %s", err))
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, err := b.config.Region()
if err != nil {
return nil, err
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("ec2", ec2conn)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
},
&awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
Debug: b.config.PackerDebug,
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
IamInstanceProfile: b.config.IamInstanceProfile,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
AvailabilityZone: b.config.AvailabilityZone,
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&StepUploadX509Cert{},
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,
Users: b.config.AMIUsers,
Groups: b.config.AMIGroups,
ProductCodes: b.config.AMIProductCodes,
},
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state.GetOk("amis"); !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state.Get("amis").(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
-273
View File
@@ -1,273 +0,0 @@
package instance
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
func testConfig() map[string]interface{} {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
panic(err)
}
return map[string]interface{}{
"account_id": "foo",
"ami_name": "foo",
"instance_type": "m1.small",
"region": "us-east-1",
"s3_bucket": "foo",
"source_ami": "foo",
"ssh_username": "bob",
"x509_cert_path": tf.Name(),
"x509_key_path": tf.Name(),
"x509_upload_path": "/foo",
}
}
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 TestBuilderPrepare_AccountId(t *testing.T) {
b := &Builder{}
config := testConfig()
config["account_id"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
config["account_id"] = "foo"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Errorf("err: %s", err)
}
config["account_id"] = "0123-0456-7890"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.AccountId != "012304567890" {
t.Errorf("should strip hyphens: %s", b.config.AccountId)
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_BundleDestination(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_destination"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundleDestination != "/tmp" {
t.Fatalf("bad: %s", b.config.BundleDestination)
}
}
func TestBuilderPrepare_BundlePrefix(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_prefix"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundlePrefix == "" {
t.Fatalf("bad: %s", b.config.BundlePrefix)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_S3Bucket(t *testing.T) {
b := &Builder{}
config := testConfig()
config["s3_bucket"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
config["s3_bucket"] = "foo"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_X509CertPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_cert_path"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
config["x509_cert_path"] = "i/am/a/file/that/doesnt/exist"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_cert_path"] = tf.Name()
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509KeyPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_key_path"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
config["x509_key_path"] = "i/am/a/file/that/doesnt/exist"
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_key_path"] = tf.Name()
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509UploadPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_upload_path"] = ""
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
@@ -1,74 +0,0 @@
package instance
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type bundleCmdData struct {
AccountId string
Architecture string
CertPath string
Destination string
KeyPath string
Prefix string
PrivatePath string
}
type StepBundleVolume struct{}
func (s *StepBundleVolume) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
x509RemoteCertPath := state.Get("x509RemoteCertPath").(string)
x509RemoteKeyPath := state.Get("x509RemoteKeyPath").(string)
// Bundle the volume
var err error
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
AccountId: config.AccountId,
Architecture: instance.Architecture,
CertPath: x509RemoteCertPath,
Destination: config.BundleDestination,
KeyPath: x509RemoteKeyPath,
Prefix: config.BundlePrefix,
PrivatePath: config.X509UploadPath,
})
if err != nil {
err := fmt.Errorf("Error processing bundle volume command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Bundling the volume...")
cmd := new(packer.RemoteCmd)
cmd.Command = config.BundleVolCommand
if err := cmd.StartWithUi(comm, ui); err != nil {
state.Put("error", fmt.Errorf("Error bundling volume: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state.Put("error", fmt.Errorf(
"Volume bundling failed. Please see the output above for more\n"+
"details on what went wrong."))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
// Store the manifest path
manifestName := config.BundlePrefix + ".manifest.xml"
state.Put("manifest_name", manifestName)
state.Put("manifest_path", fmt.Sprintf(
"%s/%s", config.BundleDestination, manifestName))
return multistep.ActionContinue
}
func (s *StepBundleVolume) Cleanup(multistep.StateBag) {}
@@ -1,65 +0,0 @@
package instance
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
manifestPath := state.Get("remote_manifest_path").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Registering the AMI...")
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: config.AMIName,
BlockDevices: config.BlockDevices.BuildAMIDevices(),
VirtType: config.AMIVirtType,
}
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
if config.AMIEnhancedNetworking {
registerOpts.SriovNetSupport = "simple"
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state.Put("error", fmt.Errorf("Error registering AMI: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
// Set the AMI ID in the state
ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId))
amis := make(map[string]string)
amis[ec2conn.Region.Name] = registerResp.ImageId
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, registerResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(multistep.StateBag) {}
@@ -1,72 +0,0 @@
package instance
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type uploadCmdData struct {
AccessKey string
BucketName string
BundleDirectory string
ManifestPath string
S3Endpoint string
SecretKey string
}
type StepUploadBundle struct{}
func (s *StepUploadBundle) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
manifestName := state.Get("manifest_name").(string)
manifestPath := state.Get("manifest_path").(string)
ui := state.Get("ui").(packer.Ui)
region, err := config.Region()
if err != nil {
err := fmt.Errorf("Error retrieving region: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
config.BundleUploadCommand, err = config.tpl.Process(config.BundleUploadCommand, uploadCmdData{
AccessKey: config.AccessKey,
BucketName: config.S3Bucket,
BundleDirectory: config.BundleDestination,
ManifestPath: manifestPath,
S3Endpoint: region.S3Endpoint,
SecretKey: config.SecretKey,
})
if err != nil {
err := fmt.Errorf("Error processing bundle upload command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Uploading the bundle...")
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
if err := cmd.StartWithUi(comm, ui); err != nil {
state.Put("error", fmt.Errorf("Error uploading volume: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state.Put("error", fmt.Errorf(
"Bundle upload failed. Please see the output above for more\n"+
"details on what went wrong."))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
state.Put("remote_manifest_path", fmt.Sprintf(
"%s/%s", config.S3Bucket, manifestName))
return multistep.ActionContinue
}
func (s *StepUploadBundle) Cleanup(state multistep.StateBag) {}
@@ -1,49 +0,0 @@
package instance
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
type StepUploadX509Cert struct{}
func (s *StepUploadX509Cert) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
x509RemoteCertPath := config.X509UploadPath + "/cert.pem"
x509RemoteKeyPath := config.X509UploadPath + "/key.pem"
ui.Say("Uploading X509 Certificate...")
if err := s.uploadSingle(comm, x509RemoteCertPath, config.X509CertPath); err != nil {
state.Put("error", fmt.Errorf("Error uploading X509 cert: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
if err := s.uploadSingle(comm, x509RemoteKeyPath, config.X509KeyPath); err != nil {
state.Put("error", fmt.Errorf("Error uploading X509 cert: %s", err))
ui.Error(state.Get("error").(error).Error())
return multistep.ActionHalt
}
state.Put("x509RemoteCertPath", x509RemoteCertPath)
state.Put("x509RemoteKeyPath", x509RemoteKeyPath)
return multistep.ActionContinue
}
func (s *StepUploadX509Cert) Cleanup(multistep.StateBag) {}
func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
return comm.Upload(dst, f)
}
+68
View File
@@ -0,0 +1,68 @@
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 {
return BuilderId
}
func (*artifact) Files() []string {
// We have no files
return nil
}
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 {
amiStrings := make([]string, 0, len(a.amis))
for region, id := range a.amis {
single := fmt.Sprintf("%s: %s", region, id)
amiStrings = append(amiStrings, single)
}
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
}
@@ -1,32 +1,35 @@
package common
package amazonebs
import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer"
"testing"
)
func TestArtifact_Impl(t *testing.T) {
var _ packer.Artifact = new(Artifact)
assert := asserts.NewTestingAsserts(t, true)
var actual packer.Artifact
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
expected := `east:foo,west:bar`
amis := make(map[string]string)
amis["east"] = "foo"
amis["west"] = "bar"
a := &Artifact{
Amis: amis,
}
a := &artifact{amis, nil}
result := a.Id()
if result != expected {
t.Fatalf("bad: %s", result)
}
assert.Equal(result, expected, "should match output")
}
func TestArtifactString(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
expected := `AMIs were created:
east: foo
@@ -36,9 +39,7 @@ west: bar`
amis["east"] = "foo"
amis["west"] = "bar"
a := &Artifact{Amis: amis}
a := &artifact{amis, nil}
result := a.String()
if result != expected {
t.Fatalf("bad: %s", result)
}
assert.Equal(result, expected, "should match output")
}
+182
View File
@@ -0,0 +1,182 @@
// The amazonebs package contains a packer.Builder implementation that
// builds AMIs for Amazon EC2.
//
// In general, there are two types of AMIs that can be created: ebs-backed or
// instance-store. This builder _only_ builds ebs-backed images.
package amazonebs
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
"time"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazonebs"
type config struct {
// Access information
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
// Information for the source instance
Region string
SourceAmi string `mapstructure:"source_ami"`
InstanceType string `mapstructure:"instance_type"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SSHTimeout time.Duration
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
}
type Builder struct {
config config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
var err error
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
b.config.RawSSHTimeout = "1m"
}
// Accumulate any errors
errs := make([]error, 0)
if b.config.AccessKey == "" {
errs = append(errs, errors.New("An access_key must be specified"))
}
if b.config.SecretKey == "" {
errs = append(errs, errors.New("A secret_key must be specified"))
}
if b.config.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if b.config.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if b.config.Region == "" {
errs = append(errs, errors.New("A region must be specified"))
} else if _, ok := aws.Regions[b.config.Region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", b.config.Region))
}
if b.config.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
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}
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, ok := aws.Regions[b.config.Region]
if !ok {
panic("region not found")
}
auth := aws.Auth{b.config.AccessKey, b.config.SecretKey}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&stepKeyPair{},
&stepSecurityGroup{},
&stepRunSourceInstance{},
&stepConnectSSH{},
&stepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &artifact{
amis: state["amis"].(map[string]string),
conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
+264
View File
@@ -0,0 +1,264 @@
package amazonebs
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
"secret_key": "bar",
"source_ami": "foo",
"instance_type": "foo",
"region": "us-east-1",
"ssh_username": "root",
"ami_name": "foo",
}
}
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{}{
"access_key": []string{},
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_AccessKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["access_key"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.AccessKey != "foo" {
t.Errorf("access key invalid: %s", b.config.AccessKey)
}
// Test bad
delete(config, "access_key")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
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()
// Test good
config["instance_type"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.InstanceType != "foo" {
t.Errorf("invalid: %s", b.config.InstanceType)
}
// Test bad
delete(config, "instance_type")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["region"] = "us-east-1"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != "us-east-1" {
t.Errorf("invalid: %s", b.config.Region)
}
// Test bad
delete(config, "region")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test invalid
config["region"] = "i-am-not-real"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SecretKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["secret_key"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SecretKey != "foo" {
t.Errorf("secret key invalid: %s", b.config.SecretKey)
}
// Test bad
delete(config, "secret_key")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SourceAmi(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["source_ami"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SourceAmi != "foo" {
t.Errorf("invalid: %s", b.config.SourceAmi)
}
// Test bad
delete(config, "source_ami")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SSHPort(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.SSHPort != 22 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
// Test set
config["ssh_port"] = 35
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 35 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
}
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
config["ssh_timeout"] = "this is not good"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["ssh_timeout"] = "5s"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHUsername(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ssh_username"] = "foo"
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)
}
// Test bad
delete(config, "ssh_username")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
+4
View File
@@ -0,0 +1,4 @@
package amazonebs
// This hook is fired prior to launching the EC2 instance.
const HookPreLaunch = "amazonebs_pre_launch"
+39
View File
@@ -0,0 +1,39 @@
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, pending []string, target string) (i *ec2.Instance, err error) {
log.Printf("Waiting for instance state to become: %s", target)
i = originalInstance
for i.State.Name != target {
found := false
for _, allowed := range pending {
if i.State.Name == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, target)
return
}
var resp *ec2.InstancesResp
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return
}
i = &resp.Reservations[0].Instances[0]
time.Sleep(2 * time.Second)
}
return
}
+125
View File
@@ -0,0 +1,125 @@
package amazonebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"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)
instance := state["instance"].(*ec2.Instance)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
// 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 bool, 1)
connectQuit := make(chan bool, 1)
defer func() {
connectQuit <- true
}()
go func() {
var err error
ui.Say("Connecting to the instance via SSH...")
attempts := 0
for {
select {
case <-connectQuit:
return
default:
}
attempts += 1
log.Printf(
"Opening TCP conn for SSH to %s:%d (attempt %d)",
instance.DNSName, config.SSHPort, attempts)
s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort))
if err == nil {
break
}
// A brief sleep so we're not being overly zealous attempting
// to connect to the instance.
time.Sleep(500 * time.Millisecond)
}
connected <- true
}()
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
timeout := time.After(config.SSHTimeout)
ConnectWaitLoop:
for {
select {
case <-connected:
// 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
}
}
}
var comm packer.Communicator
if err == nil {
comm, err = ssh.New(s.conn, sshConfig)
}
if err != nil {
err := fmt.Errorf("Error connecting to SSH: %s", err)
state["error"] = err
ui.Error(err.Error())
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()
}
}
+78
View File
@@ -0,0 +1,78 @@
package amazonebs
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type stepCreateAMI struct{}
type amiNameData struct {
CreateTime string
}
func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ec2conn := state["ec2"].(*ec2.EC2)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
// Create the image
ui.Say(fmt.Sprintf("Creating the AMI: %s", amiName))
createOpts := &ec2.CreateImage{
InstanceId: instance.InstanceId,
Name: amiName,
}
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
}
// Set the AMI ID in the state
ui.Say(fmt.Sprintf("AMI: %s", createResp.ImageId))
amis := make(map[string]string)
amis[config.Region] = createResp.ImageId
state["amis"] = amis
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
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
}
if imageResp.Images[0].State == "available" {
break
}
}
return multistep.ActionContinue
}
func (s *stepCreateAMI) Cleanup(map[string]interface{}) {
// No cleanup...
}
+57
View File
@@ -0,0 +1,57 @@
package amazonebs
import (
"cgl.tideland.biz/identifier"
"encoding/hex"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepKeyPair struct {
keyName string
}
func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
ui.Say("Creating temporary keypair for this instance...")
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := 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
}
// Set the keyname so we know to delete it later
s.keyName = keyName
// Set some state data for use in future steps
state["keyPair"] = keyName
state["privateKey"] = keyResp.KeyMaterial
return multistep.ActionContinue
}
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
// If no key name is set, then we never created it, so just return
if s.keyName == "" {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary keypair...")
_, err := ec2conn.DeleteKeyPair(s.keyName)
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
}
}
+25
View File
@@ -0,0 +1,25 @@
package amazonebs
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{}) {}
@@ -0,0 +1,73 @@
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepRunSourceInstance struct {
instance *ec2.Instance
}
func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ec2conn := state["ec2"].(*ec2.EC2)
keyName := state["keyPair"].(string)
securityGroupId := state["securityGroupId"].(string)
ui := state["ui"].(packer.Ui)
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: config.SourceAmi,
InstanceType: config.InstanceType,
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
}
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
}
s.instance = &runResp.Instances[0]
log.Printf("instance id: %s", s.instance.InstanceId)
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
}
state["instance"] = s.instance
return multistep.ActionContinue
}
func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
if s.instance == nil {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
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")
}
+74
View File
@@ -0,0 +1,74 @@
package amazonebs
import (
"cgl.tideland.biz/identifier"
"encoding/hex"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepSecurityGroup struct {
groupId string
}
func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
// Create the group
ui.Say("Creating temporary security group for this instance...")
groupName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("Temporary group name: %s", groupName)
groupResp, err := ec2conn.CreateSecurityGroup(groupName, "Temporary group for Packer")
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the group ID so we can delete it later
s.groupId = groupResp.Id
// Authorize the SSH access
perms := []ec2.IPPerm{
ec2.IPPerm{
Protocol: "tcp",
FromPort: config.SSHPort,
ToPort: config.SSHPort,
SourceIPs: []string{"0.0.0.0/0"},
},
}
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
}
// Set some state data for use in future steps
state["securityGroupId"] = s.groupId
return multistep.ActionContinue
}
func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.groupId == "" {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
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))
}
}
@@ -1,42 +1,35 @@
package ebs
package amazonebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
type stepStopInstance struct{}
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
// Stop the instance so we can create an AMI from it
ui.Say("Stopping the source instance...")
_, err := ec2conn.StopInstances(instance.InstanceId)
if err != nil {
err := fmt.Errorf("Error stopping instance: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the instance to actual stop
ui.Say("Waiting for the instance to stop...")
stateChange := awscommon.StateChangeConf{
Pending: []string{"running", "stopping"},
Target: "stopped",
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance),
StepState: state,
}
_, err = awscommon.WaitForState(&stateChange)
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -44,6 +37,6 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue
}
func (s *stepStopInstance) Cleanup(multistep.StateBag) {
func (s *stepStopInstance) Cleanup(map[string]interface{}) {
// No cleanup...
}
@@ -2,20 +2,14 @@ package common
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"log"
"net/http"
"net/url"
"os"
"runtime"
)
// DownloadConfig is the configuration given to instantiate a new
@@ -43,10 +37,6 @@ type DownloadConfig struct {
// for the downloader will be used to verify with this checksum after
// it is downloaded.
Checksum []byte
// What to use for the user agent for HTTP requests. If set to "", use the
// default user agent provided by Go.
UserAgent string
}
// A DownloadClient helps download, verify checksums, etc.
@@ -55,30 +45,12 @@ type DownloadClient struct {
downloader Downloader
}
// HashForType returns the Hash implementation for the given string
// type, or nil if the type is not supported.
func HashForType(t string) hash.Hash {
switch t {
case "md5":
return md5.New()
case "sha1":
return sha1.New()
case "sha256":
return sha256.New()
case "sha512":
return sha512.New()
default:
return nil
}
}
// NewDownloadClient returns a new DownloadClient for the given
// configuration.
func NewDownloadClient(c *DownloadConfig) *DownloadClient {
if c.DownloaderMap == nil {
c.DownloaderMap = map[string]Downloader{
"http": &HTTPDownloader{userAgent: c.UserAgent},
"https": &HTTPDownloader{userAgent: c.UserAgent},
"http": new(HTTPDownloader),
}
}
@@ -101,7 +73,6 @@ func (d *DownloadClient) Cancel() {
func (d *DownloadClient) Get() (string, error) {
// If we already have the file and it matches, then just return the target path.
if verify, _ := d.VerifyChecksum(d.config.TargetPath); verify {
log.Println("Initial checksum matched, no download needed.")
return d.config.TargetPath, nil
}
@@ -110,17 +81,10 @@ func (d *DownloadClient) Get() (string, error) {
return "", err
}
log.Printf("Parsed URL: %#v", url)
// Files when we don't copy the file are special cased.
var finalPath string
if url.Scheme == "file" && !d.config.CopyFile {
finalPath = url.Path
// Remove forward slash on absolute Windows file URLs before processing
if runtime.GOOS == "windows" && finalPath[0] == '/' {
finalPath = finalPath[1:len(finalPath)]
}
} else {
finalPath = d.config.TargetPath
@@ -137,11 +101,7 @@ func (d *DownloadClient) Get() (string, error) {
}
defer f.Close()
log.Printf("Downloading: %s", url.String())
err = d.downloader.Download(f, url)
if err != nil {
return "", err
}
}
if d.config.Hash != nil {
@@ -156,12 +116,8 @@ func (d *DownloadClient) Get() (string, error) {
}
// PercentProgress returns the download progress as a percentage.
func (d *DownloadClient) PercentProgress() int {
if d.downloader == nil {
return -1
}
return int((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
func (d *DownloadClient) PercentProgress() uint {
return uint((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
}
// VerifyChecksum tests that the path matches the checksum for the
@@ -177,7 +133,6 @@ func (d *DownloadClient) VerifyChecksum(path string) (bool, error) {
}
defer f.Close()
log.Printf("Verifying checksum of %s", path)
d.config.Hash.Reset()
io.Copy(d.config.Hash, f)
return bytes.Compare(d.config.Hash.Sum(nil), d.config.Checksum) == 0, nil
@@ -186,9 +141,8 @@ func (d *DownloadClient) VerifyChecksum(path string) (bool, error) {
// HTTPDownloader is an implementation of Downloader that downloads
// files over HTTP.
type HTTPDownloader struct {
progress uint
total uint
userAgent string
progress uint
total uint
}
func (*HTTPDownloader) Cancel() {
@@ -196,37 +150,11 @@ func (*HTTPDownloader) Cancel() {
}
func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error {
log.Printf("Starting download: %s", src.String())
req, err := http.NewRequest("GET", src.String(), nil)
resp, err := http.Get(src.String())
if err != nil {
return err
}
if d.userAgent != "" {
req.Header.Set("User-Agent", d.userAgent)
}
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
log.Printf(
"Non-200 status code: %d. Getting error body.", resp.StatusCode)
errorBody := new(bytes.Buffer)
io.Copy(errorBody, resp.Body)
return fmt.Errorf("HTTP error '%d'! Remote side responded:\n%s",
resp.StatusCode, errorBody.String())
}
d.progress = 0
d.total = uint(resp.ContentLength)
+42
View File
@@ -0,0 +1,42 @@
package common
import (
"crypto/md5"
"encoding/hex"
"io/ioutil"
"os"
"testing"
)
func TestDownloadClient_VerifyChecksum(t *testing.T) {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("tempfile error: %s", err)
}
defer os.Remove(tf.Name())
// "foo"
checksum, err := hex.DecodeString("acbd18db4cc2f85cedef654fccc4a4d8")
if err != nil {
t.Fatalf("decode err: %s", err)
}
// Write the file
tf.Write([]byte("foo"))
tf.Close()
config := &DownloadConfig{
Hash: md5.New(),
Checksum: checksum,
}
d := NewDownloadClient(config)
result, err := d.VerifyChecksum(tf.Name())
if err != nil {
t.Fatalf("Verify err: %s", err)
}
if !result {
t.Fatal("didn't verify")
}
}
@@ -11,7 +11,7 @@ import (
// MultistepDebugFn will return a proper multistep.DebugPauseFn to
// use for debugging if you're using multistep in your builder.
func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
return func(loc multistep.DebugLocation, name string, state multistep.StateBag) {
return func(loc multistep.DebugLocation, name string, state map[string]interface{}) {
var locationString string
switch loc {
case multistep.DebugLocationAfterRun:
@@ -23,7 +23,7 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
}
message := fmt.Sprintf(
"Pausing %s step '%s'. Press enter to continue.",
"Pausing %s step '%s'. Press any key to continue.",
locationString, name)
result := make(chan string, 1)
@@ -41,7 +41,7 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
case <-result:
return
case <-time.After(100 * time.Millisecond):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
if _, ok := state[multistep.StateCancelled]; ok {
return
}
}
+49 -250
View File
@@ -13,9 +13,7 @@ import (
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
@@ -23,7 +21,6 @@ const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
type Image struct {
Id uint
Name string
Slug string
Distribution string
}
@@ -31,26 +28,6 @@ type ImagesResp struct {
Images []Image
}
type Region struct {
Id uint
Name string
Slug string
}
type RegionsResp struct {
Regions []Region
}
type Size struct {
Id uint
Name string
Slug string
}
type SizesResp struct {
Sizes []Size
}
type DigitalOceanClient struct {
// The http client for communicating
client *http.Client
@@ -66,11 +43,7 @@ type DigitalOceanClient struct {
// Creates a new client for communicating with DO
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
c := &DigitalOceanClient{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
client: http.DefaultClient,
BaseURL: DIGITALOCEAN_API_URL,
ClientID: client,
APIKey: key,
@@ -80,9 +53,10 @@ func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
// Creates an SSH Key and returns it's id
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
// 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 {
@@ -98,35 +72,15 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
// Destroys an SSH key
func (d DigitalOceanClient) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequest(d, path, url.Values{})
_, err := NewRequest(d, path, "")
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClient) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
params := url.Values{}
params.Set("name", name)
found_size, err := d.Size(size)
if err != nil {
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
}
found_image, err := d.Image(image)
if err != nil {
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
}
found_region, err := d.Region(region)
if err != nil {
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
}
params.Set("size_slug", found_size.Slug)
params.Set("image_slug", found_image.Slug)
params.Set("region_slug", found_region.Slug)
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
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 {
@@ -142,7 +96,7 @@ func (d DigitalOceanClient) CreateDroplet(name string, size string, image string
// Destroys a droplet
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequest(d, path, url.Values{})
_, err := NewRequest(d, path, "")
return err
}
@@ -150,16 +104,7 @@ func (d DigitalOceanClient) DestroyDroplet(id uint) error {
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequest(d, path, url.Values{})
_, err := NewRequest(d, path, "")
return err
}
@@ -167,9 +112,7 @@ func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
params := url.Values{}
params.Set("name", name)
params := fmt.Sprintf("name=%v", name)
_, err := NewRequest(d, path, params)
@@ -178,7 +121,7 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
// Returns all available images.
func (d DigitalOceanClient) Images() ([]Image, error) {
resp, err := NewRequest(d, "images", url.Values{})
resp, err := NewRequest(d, "images", "")
if err != nil {
return nil, err
}
@@ -194,7 +137,7 @@ func (d DigitalOceanClient) Images() ([]Image, error) {
// Destroys an image by its ID.
func (d DigitalOceanClient) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequest(d, path, url.Values{})
_, err := NewRequest(d, path, "")
return err
}
@@ -202,7 +145,7 @@ func (d DigitalOceanClient) DestroyImage(id uint) error {
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequest(d, path, url.Values{})
body, err := NewRequest(d, path, "")
if err != nil {
return "", "", err
}
@@ -222,198 +165,54 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
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)
// Add the authentication parameters
params.Set("client_id", d.ClientID)
params.Set("api_key", d.APIKey)
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
var decodedResponse map[string]interface{}
// 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)
var lastErr error
for attempts := 1; attempts < 10; attempts++ {
resp, err := client.Get(url)
if err != nil {
return nil, err
}
resp, err := client.Get(url)
if err != nil {
return decodedResponse, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
log.Printf("response from digitalocean: %s", body)
resp.Body.Close()
if err != nil {
return decodedResponse, err
}
var decodedResponse map[string]interface{}
err = json.Unmarshal(body, &decodedResponse)
if err != nil {
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
resp.StatusCode, body))
return decodedResponse, err
}
err = json.Unmarshal(body, &decodedResponse)
// Check for errors sent by digitalocean
status := decodedResponse["status"].(string)
if status == "OK" {
return decodedResponse, nil
}
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" {
statusRaw, ok := decodedResponse["error_message"]
if ok {
status = statusRaw.(string)
} else {
status = fmt.Sprintf(
"Unknown error. Full response body: %s", body)
}
status = decodedResponse["error_message"]
}
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
resp.StatusCode, status))
log.Println(lastErr)
if strings.Contains(status, "a pending event") {
// Retry, DigitalOcean sends these dumb "pending event"
// errors all the time.
time.Sleep(5 * time.Second)
continue
}
// Some other kind of error. Just return.
return decodedResponse, lastErr
err = errors.New(fmt.Sprintf("Received bad status from DigitalOcean: %v", status))
return decodedResponse, err
}
return nil, lastErr
}
func (d DigitalOceanClient) Image(slug_or_name_or_id string) (Image, error) {
images, err := d.Images()
if err != nil {
return Image{}, err
}
for _, image := range images {
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
if strings.EqualFold(image.Name, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if image.Id == uint(id) {
return image, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
return Image{}, err
}
// Returns all available regions.
func (d DigitalOceanClient) Regions() ([]Region, error) {
resp, err := NewRequest(d, "regions", url.Values{})
if err != nil {
return nil, err
}
var result RegionsResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Regions, nil
}
func (d DigitalOceanClient) Region(slug_or_name_or_id string) (Region, error) {
regions, err := d.Regions()
if err != nil {
return Region{}, err
}
for _, region := range regions {
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
if strings.EqualFold(region.Name, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if region.Id == uint(id) {
return region, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
return Region{}, err
}
// Returns all available sizes.
func (d DigitalOceanClient) Sizes() ([]Size, error) {
resp, err := NewRequest(d, "sizes", url.Values{})
if err != nil {
return nil, err
}
var result SizesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Sizes, nil
}
func (d DigitalOceanClient) Size(slug_or_name_or_id string) (Size, error) {
sizes, err := d.Sizes()
if err != nil {
return Size{}, err
}
for _, size := range sizes {
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
if strings.EqualFold(size.Name, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if size.Id == uint(id) {
return size, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
return Size{}, err
return decodedResponse, nil
}
+3 -7
View File
@@ -12,9 +12,6 @@ type Artifact struct {
// The ID of the image
snapshotId uint
// The name of the region
regionName string
// The client for making API calls
client *DigitalOceanClient
}
@@ -29,15 +26,14 @@ func (*Artifact) Files() []string {
}
func (a *Artifact) Id() string {
// mimicing the aws builder
return fmt.Sprintf("%s:%s", a.regionName, a.snapshotName)
return a.snapshotName
}
func (a *Artifact) String() string {
return fmt.Sprintf("A snapshot was created: '%v' in region '%v'", a.snapshotName, a.regionName)
return fmt.Sprintf("A snapshot was created: %v", a.snapshotName)
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
log.Printf("Destroying image: %d", a.snapshotId)
return a.client.DestroyImage(a.snapshotId)
}
+2 -2
View File
@@ -14,8 +14,8 @@ func TestArtifact_Impl(t *testing.T) {
}
func TestArtifactString(t *testing.T) {
a := &Artifact{"packer-foobar", 42, "San Francisco", nil}
expected := "A snapshot was created: 'packer-foobar' in region 'San Francisco'"
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)
+88 -148
View File
@@ -4,63 +4,49 @@
package digitalocean
import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strconv"
"text/template"
"time"
)
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
// name="Ubuntu 12.04.4 x64", id=3101045,
const DefaultImage = "ubuntu-12-04-x64"
// see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key]
// name="New York", id=1
const DefaultRegion = "nyc1"
// see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key]
// name="512MB", id=66 (the smallest droplet size)
const DefaultSize = "512mb"
// 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 {
common.PackerConfig `mapstructure:",squash"`
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"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
SnapshotName string
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
SSHTimeout time.Duration
EventDelay time.Duration
StateTimeout time.Duration
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
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"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
tpl *packer.ConfigTemplate
}
type Builder struct {
@@ -68,64 +54,29 @@ type Builder struct {
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return nil, err
func (b *Builder) Prepare(raws ...interface{}) error {
for _, raw := range raws {
err := mapstructure.Decode(raw, &b.config)
if err != nil {
return err
}
}
b.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
b.config.tpl.UserVars = b.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
// Optional configuration with defaults
if b.config.APIKey == "" {
// Default to environment variable for api_key, if it exists
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
//
if b.config.RegionID == 0 {
// Default to Region "New York"
b.config.RegionID = 1
}
if b.config.ClientID == "" {
// Default to environment variable for client_id, if it exists
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
if b.config.SizeID == 0 {
// Default to 512mb, the smallest droplet size
b.config.SizeID = 66
}
if b.config.Region == "" {
if b.config.RegionID != 0 {
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
} else {
b.config.Region = DefaultRegion
}
}
if b.config.Size == "" {
if b.config.SizeID != 0 {
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
} else {
b.config.Size = DefaultSize
}
}
if b.config.Image == "" {
if b.config.ImageID != 0 {
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
} else {
b.config.Image = DefaultImage
}
}
if b.config.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.SnapshotName = "packer-{{timestamp}}"
}
if b.config.DropletName == "" {
// Default to packer-[time-ordered-uuid]
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
if b.config.ImageID == 0 {
// Default to base image "Ubuntu 12.04 x64 Server (id: 284203)"
b.config.ImageID = 284203
}
if b.config.SSHUsername == "" {
@@ -139,67 +90,78 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.SSHPort = 22
}
if b.config.RawSnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
}
if b.config.RawSSHTimeout == "" {
// Default to 1 minute timeouts
b.config.RawSSHTimeout = "1m"
}
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"
}
templates := map[string]*string{
"client_id": &b.config.ClientID,
"api_key": &b.config.APIKey,
"snapshot_name": &b.config.SnapshotName,
"droplet_name": &b.config.DropletName,
"ssh_username": &b.config.SSHUsername,
"ssh_timeout": &b.config.RawSSHTimeout,
"state_timeout": &b.config.RawStateTimeout,
}
for n, ptr := range templates {
var err error
*ptr, err = b.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// 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 = packer.MultiErrorAppend(
errs, errors.New("a client_id must be specified"))
errs = append(errs, errors.New("a client_id must be specified"))
}
if b.config.APIKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("an api_key must be specified"))
errs = append(errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
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 = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
b.config.StateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
// 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()
}
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
return nil, nil
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) {
@@ -207,24 +169,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
// Set up the state
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("client", client)
state.Put("hook", hook)
state.Put("ui", ui)
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),
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
},
new(common.StepProvision),
new(stepShutdown),
new(stepConnectSSH),
new(stepProvision),
new(stepPowerOff),
new(stepSnapshot),
}
@@ -242,35 +199,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
if _, ok := state.GetOk("snapshot_name"); !ok {
if _, ok := state["snapshot_name"]; !ok {
log.Println("Failed to find snapshot_name in state. Bug?")
return nil, nil
}
sregion := state.Get("region")
var region string
if sregion != nil {
region = sregion.(string)
} else {
region = fmt.Sprintf("%v", state.Get("region_id").(uint))
}
found_region, err := client.Region(region)
if err != nil {
return nil, err
}
artifact := &Artifact{
snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint),
regionName: found_region.Name,
snapshotName: state["snapshot_name"].(string),
snapshotId: state["snapshot_image_id"].(uint),
client: client,
}
+74 -248
View File
@@ -2,17 +2,10 @@ package digitalocean
import (
"github.com/mitchellh/packer/packer"
"os"
"strconv"
"testing"
)
func init() {
// Clear out the credential env vars
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
func testConfig() map[string]interface{} {
return map[string]interface{}{
"client_id": "foo",
@@ -34,10 +27,7 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
"api_key": []string{},
}
warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(c)
if err == nil {
t.Fatalf("prepare should fail")
}
@@ -49,10 +39,7 @@ func TestBuilderPrepare_APIKey(t *testing.T) {
// Test good
config["api_key"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -64,25 +51,10 @@ func TestBuilderPrepare_APIKey(t *testing.T) {
// Test bad
delete(config, "api_key")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "api_key")
os.Setenv("DIGITALOCEAN_API_KEY", "foo")
defer os.Setenv("DIGITALOCEAN_API_KEY", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ClientID(t *testing.T) {
@@ -91,10 +63,7 @@ func TestBuilderPrepare_ClientID(t *testing.T) {
// Test good
config["client_id"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -106,147 +75,90 @@ func TestBuilderPrepare_ClientID(t *testing.T) {
// Test bad
delete(config, "client_id")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "client_id")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo")
defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
func TestBuilderPrepare_RegionID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != DefaultRegion {
t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
if b.config.RegionID != 1 {
t.Errorf("invalid: %d", b.config.RegionID)
}
expected := "sfo1"
// Test set
config["region_id"] = 0
config["region"] = expected
config["region_id"] = 2
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != expected {
t.Errorf("found %s, expected %s", b.config.Region, expected)
if b.config.RegionID != 2 {
t.Errorf("invalid: %d", b.config.RegionID)
}
}
func TestBuilderPrepare_Size(t *testing.T) {
func TestBuilderPrepare_SizeID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Size != DefaultSize {
t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
if b.config.SizeID != 66 {
t.Errorf("invalid: %d", b.config.SizeID)
}
expected := "1024mb"
// Test set
config["size_id"] = 0
config["size"] = expected
config["size_id"] = 67
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Size != expected {
t.Errorf("found %s, expected %s", b.config.Size, expected)
if b.config.SizeID != 67 {
t.Errorf("invalid: %d", b.config.SizeID)
}
}
func TestBuilderPrepare_Image(t *testing.T) {
func TestBuilderPrepare_ImageID(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Image != DefaultImage {
t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
if b.config.SizeID != 66 {
t.Errorf("invalid: %d", b.config.SizeID)
}
expected := "ubuntu-14-04-x64"
// Test set
config["image_id"] = 0
config["image"] = expected
config["size_id"] = 2
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Image != expected {
t.Errorf("found %s, expected %s", b.config.Image, expected)
if b.config.SizeID != 2 {
t.Errorf("invalid: %d", b.config.SizeID)
}
}
@@ -255,10 +167,7 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -270,10 +179,7 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
// Test set
config["ssh_username"] = "foo"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -288,10 +194,7 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) {
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -303,10 +206,7 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) {
// Test set
config["ssh_timeout"] = "30s"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -314,10 +214,39 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) {
// Test bad
config["ssh_timeout"] = "tubes"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
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")
}
@@ -329,10 +258,7 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -344,10 +270,7 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
// Test set
config["state_timeout"] = "5m"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -355,84 +278,39 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
// Test bad
config["state_timeout"] = "tubes"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_PrivateNetworking(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.PrivateNetworking != false {
t.Errorf("invalid: %s", b.config.PrivateNetworking)
}
// Test set
config["private_networking"] = true
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.PrivateNetworking != true {
t.Errorf("invalid: %s", b.config.PrivateNetworking)
}
}
func TestBuilderPrepare_SnapshotName(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SnapshotName == "" {
t.Errorf("invalid: %s", b.config.SnapshotName)
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
t.Errorf("invalid: %d", b.config.RawSnapshotName)
}
// Test set
config["snapshot_name"] = "foobarbaz"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test set with template
config["snapshot_name"] = "{{timestamp}}"
config["snapshot_name"] = "{{.CreateTime}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@@ -443,55 +321,3 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
}
}
func TestBuilderPrepare_DropletName(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.DropletName == "" {
t.Errorf("invalid: %s", b.config.DropletName)
}
// Test normal set
config["droplet_name"] = "foobar"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with template
config["droplet_name"] = "foobar-{{timestamp}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with bad template
config["droplet_name"] = "foobar-{{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
-30
View File
@@ -1,30 +0,0 @@
package digitalocean
import (
"code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/multistep"
)
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(config)
ipAddress := state.Get("droplet_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
}
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
config := state.Get("config").(config)
privateKey := state.Get("privateKey").(string)
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &ssh.ClientConfig{
User: config.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
}, nil
}
+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()
}
}
+30 -17
View File
@@ -1,29 +1,35 @@
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 multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
sshKeyId := state.Get("ssh_key_id").(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...")
// Create the droplet based on configuration
dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking)
// 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.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -32,29 +38,36 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
s.dropletId = dropletId
// Store the droplet id for later
state.Put("droplet_id", dropletId)
state["droplet_id"] = dropletId
return multistep.ActionContinue
}
func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
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.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
// Destroy the droplet we just created
ui.Say("Destroying droplet...")
err := client.DestroyDroplet(s.dropletId)
if err != nil {
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
DIGITALOCEAN_API_URL, s.dropletId, c.ClientID, c.APIKey)
// 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))
}
+16 -16
View File
@@ -1,14 +1,15 @@
package digitalocean
import (
"code.google.com/p/gosshold/ssh"
"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/common/uuid"
"github.com/mitchellh/packer/packer"
"log"
)
@@ -17,9 +18,9 @@ type stepCreateSSHKey struct {
keyId uint
}
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
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...")
@@ -34,21 +35,20 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
}
// Set the private key in the statebag for later
state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk)))
state["privateKey"] = string(pem.EncodeToMemory(&priv_blk))
// Marshal the public key into SSH compatible format
// TODO properly handle the public key error
pub, _ := ssh.NewPublicKey(&priv.PublicKey)
pub_sshformat := string(ssh.MarshalAuthorizedKey(pub))
pub := priv.PublicKey
pub_sshformat := string(ssh.MarshalAuthorizedKey(&pub))
// The name of the public key on DO
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
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.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -59,20 +59,20 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future
state.Put("ssh_key_id", keyId)
state["ssh_key_id"] = keyId
return multistep.ActionContinue
}
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
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.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
client := state["client"].(*DigitalOceanClient)
ui := state["ui"].(packer.Ui)
c := state["config"].(config)
ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId)
+10 -10
View File
@@ -8,18 +8,18 @@ import (
type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
dropletId := state.Get("droplet_id").(uint)
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.stateTimeout)
err := waitForDropletState("active", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -28,16 +28,16 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
ip, _, err := client.DropletStatus(dropletId)
if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("droplet_ip", ip)
state["droplet_ip"] = ip
return multistep.ActionContinue
}
func (s *stepDropletInfo) Cleanup(state multistep.StateBag) {
func (s *stepDropletInfo) Cleanup(state map[string]interface{}) {
// no cleanup
}
+20 -24
View File
@@ -5,43 +5,39 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
c := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)
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)
_, status, err := client.DropletStatus(dropletId)
if err != nil {
err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// 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)
if status == "off" {
// Droplet is already off, don't do anything
return multistep.ActionContinue
}
// Poweroff the droplet so it can be snapshot
err := client.PowerOffDroplet(dropletId)
// Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...")
err = client.PowerOffDroplet(dropletId)
if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Println("Waiting for poweroff event to complete...")
err = waitForDropletState("off", dropletId, client, c.stateTimeout)
ui.Say("Waiting for droplet to power off...")
err = waitForDropletState("off", dropletId, client, c)
if err != nil {
state.Put("error", err)
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -49,6 +45,6 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue
}
func (s *stepPowerOff) Cleanup(state multistep.StateBag) {
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{}) {}
-74
View File
@@ -1,74 +0,0 @@
package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)
// Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...")
err := client.ShutdownDroplet(dropletId)
if err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// A channel we use as a flag to end our goroutines
done := make(chan struct{})
shutdownRetryDone := make(chan struct{})
// Make sure we wait for the shutdown retry goroutine to end
// before moving on.
defer func() {
close(done)
<-shutdownRetryDone
}()
// Start a goroutine that just keeps trying to shut down the
// droplet.
go func() {
defer close(shutdownRetryDone)
for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts)
err := client.ShutdownDroplet(dropletId)
if err != nil {
log.Printf("Shutdown retry error: %s", err)
}
select {
case <-done:
return
case <-time.After(20 * time.Second):
// Retry!
}
}
}()
err = waitForDropletState("off", dropletId, client, 2*time.Minute)
if err != nil {
log.Printf("Error waiting for graceful off: %s", err)
}
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state multistep.StateBag) {
// no cleanup
}
+13 -14
View File
@@ -10,26 +10,26 @@ import (
type stepSnapshot struct{}
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
dropletId := state.Get("droplet_id").(uint)
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.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout)
err = waitForDropletState("active", dropletId, client, c)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -38,7 +38,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
images, err := client.Images()
if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
@@ -53,20 +53,19 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
if imageId == 0 {
err := errors.New("Couldn't find snapshot to get the image ID. Bug?")
state.Put("error", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region)
state["snapshot_image_id"] = imageId
state["snapshot_name"] = c.SnapshotName
return multistep.ActionContinue
}
func (s *stepSnapshot) Cleanup(state multistep.StateBag) {
func (s *stepSnapshot) Cleanup(state map[string]interface{}) {
// no cleanup
}
+26 -25
View File
@@ -1,55 +1,56 @@
package digitalocean
import (
"fmt"
"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, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error {
active := make(chan bool, 1)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId)
if err != nil {
result <- err
return
log.Println(err)
break
}
if status == desiredState {
result <- nil
return
break
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
active <- true
}()
log.Printf("Waiting for up to %d seconds for droplet to become %s", timeout/time.Second, desiredState)
select {
case err := <-result:
return err
case <-time.After(timeout):
err := fmt.Errorf("Timeout while waiting to for droplet to become '%s'", desiredState)
return err
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
}
-32
View File
@@ -1,32 +0,0 @@
package docker
import (
"fmt"
"os"
)
// ExportArtifact is an Artifact implementation for when a container is
// exported from docker into a single flat file.
type ExportArtifact struct {
path string
}
func (*ExportArtifact) BuilderId() string {
return BuilderId
}
func (a *ExportArtifact) Files() []string {
return []string{a.path}
}
func (*ExportArtifact) Id() string {
return "Container"
}
func (a *ExportArtifact) String() string {
return fmt.Sprintf("Exported Docker file: %s", a.path)
}
func (a *ExportArtifact) Destroy() error {
return os.Remove(a.path)
}
-10
View File
@@ -1,10 +0,0 @@
package docker
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestExportArtifact_impl(t *testing.T) {
var _ packer.Artifact = new(ExportArtifact)
}

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