Compare commits

...

235 Commits

Author SHA1 Message Date
Mitchell Hashimoto 995b4ffc80 v0.2.2 2013-08-01 10:29:19 -07:00
Mitchell Hashimoto 648c59ba54 website: update works with image for other vmware products 2013-08-01 10:25:38 -07:00
Mitchell Hashimoto fbc4a255d0 provisioner/salt-masterless: remove Ui
/cc @rgarcia Should just pass it in as a param
2013-08-01 10:06:38 -07:00
Mitchell Hashimoto caf04a572e website: update platforms to note instance-store AMI [GH-230] 2013-08-01 09:57:12 -07:00
Mitchell Hashimoto bfa4a055f5 website: better salt-masterless docs 2013-07-31 23:21:16 -07:00
Mitchell Hashimoto 4c1db34ac9 provisioner/salt-masterless: verify local_state_tree exists
/cc @rgarcia
2013-07-31 23:11:08 -07:00
Mitchell Hashimoto 116692212c provisioner/salt-masterless: use Messages for minor steps
/cc @rgarcia - "Message" in Ui is the non-bold colored messages and
are meant for sub-messages or steps part of a larger message. "Say"
is used for the bold stuff. This is more in line wit how Packer works.
2013-07-31 23:07:15 -07:00
Mitchell Hashimoto 00921966ee provisioner/salt-masterless: don't capitalize Ui
/cc @rgarcia - Not sure why this was capitalized to begin with
2013-07-31 23:06:22 -07:00
Mitchell Hashimoto 5642fa01c8 provisioner/salt-masterless: use common lib for config decoding
/cc @rgarcia - Since you opened the pull request, common config
decoding stuff has been pulled out into the common Package. This
simplifies a lot of code.
2013-07-31 23:05:15 -07:00
Mitchell Hashimoto 53ba20041c plugin/provisioner-salt-masterless: compile again 2013-07-31 23:02:56 -07:00
Mitchell Hashimoto 14393b91b7 plugin/provisioner-salt-masterless: add empty test file
/cc @rgarcia - This just forces the tests to make sure the package
even builds. Better than nothing.
2013-07-31 23:01:59 -07:00
Mitchell Hashimoto dc615ff317 provisoner/salt-masterless: lowercase package name
/cc @rgarcia - I don't know of any Go packages that follow this naming
convention so I just lowercased it.
2013-07-31 23:01:03 -07:00
Mitchell Hashimoto d728ca24f9 Update CHANGELOG 2013-07-31 23:00:13 -07:00
Mitchell Hashimoto ff366ca685 Merge pull request #220 from rgarcia/salt-provisioner
provisioner/salt-masterless: masterless salt provisioner
2013-07-31 22:57:42 -07:00
Mitchell Hashimoto 744c52f685 builder/vmware: slightly nicer error messages 2013-07-31 22:52:36 -07:00
Mitchell Hashimoto e6c94f449f website: note support VMware player on Linux 2013-07-31 22:18:35 -07:00
Mitchell Hashimoto 844fde01a7 builder/vmware: error message tweaking for Player driver 2013-07-31 22:15:13 -07:00
Mitchell Hashimoto 7411087a5c Update CHANGELOG 2013-07-31 22:11:51 -07:00
Julian Phillips 4b6bbfd723 builder/vmware: We only need one runAndLog function
We shouldn't need a different one for each driver ...

Conflicts:
	builder/vmware/driver_workstation9.go
2013-07-31 22:11:07 -07:00
Julian Phillips 898e468edd builder/vmware: Add Support for VMware Player 5 (Linux)
We have two choices for VMware on Linux.  We already support
Workstation, add support for Player too.

VMware Player doesn't come with vmware-vdiskmanager, so fall back to
using qemu-img if we can find that.

Conflicts:
	builder/vmware/driver.go
2013-07-31 22:09:38 -07:00
Mitchell Hashimoto 5de3a948fa builder/vmware: sleep before cleaning files on Windows 2013-07-31 15:36:22 -07:00
Mitchell Hashimoto b1ccdd4e11 fmt 2013-07-31 15:29:03 -07:00
Mitchell Hashimoto 5a267ef73f Merge pull request #228 from markpeek/markpeek-images
builder/amazon/common: handle missing source AMI error
2013-07-31 15:28:37 -07:00
Mitchell Hashimoto 8f6b2e4d32 builder/virtualbox,vmware: retry removing output dir if fails 2013-07-31 15:16:39 -07:00
Mitchell Hashimoto ce4db65fa0 builder/vmware: compile host_ip on every platform 2013-07-31 15:10:28 -07:00
Mitchell Hashimoto 1ff52979fe builder/vmware: redo host IP stuff for Windows 2013-07-31 14:46:12 -07:00
Mitchell Hashimoto 5343bc42a0 packer: fmt 2013-07-31 14:43:34 -07:00
Mark Peek f99797eba3 Handle missing source AMI error
Specifying a non-existent source AMI will result in an empty array of
images in the response which would cause an "index out of range" panic.
This change handles this case more gracefully with an error message.
2013-07-31 14:20:25 -07:00
Mitchell Hashimoto 27a07cc9aa packer: don't output colors if Windows and not Cygwin 2013-07-31 14:06:01 -07:00
Mitchell Hashimoto e11b6617f3 builder/vmware: more logs for Workstation 2013-07-31 14:02:00 -07:00
Mitchell Hashimoto 5fd00cfd3a builder/vmware: convert line endings in workstation to Unix-style 2013-07-31 13:58:57 -07:00
Mitchell Hashimoto f5b0b4f9d7 builder/vmware: improved logging for various workstation steps 2013-07-31 13:50:49 -07:00
Mitchell Hashimoto 2f15d0e14f Update CHANGELOG 2013-07-31 12:40:55 -07:00
Mitchell Hashimoto f6245ae128 builder/vmware: verify various paths exist for Workstation 2013-07-31 12:39:49 -07:00
Mitchell Hashimoto d0a2dac8f0 builder/vmware: refactor the platform-specific stuff out
/cc @rasa - This is more idiomatic Go: to share the platform-independent
stuff and make the platform-specific stuff unexported functions
2013-07-31 12:36:17 -07:00
Mitchell Hashimoto aa6a9cc2e8 scripts: make dist executable 2013-07-31 12:36:05 -07:00
Mitchell Hashimoto 62207912d2 Merge branch 'rasa-master'
This adds support for Windows VMware Workstation
2013-07-31 11:53:00 -07:00
Mitchell Hashimoto 720a72dc30 Merge branch 'master' of https://github.com/rasa/packer into rasa-master 2013-07-31 11:52:20 -07:00
Mitchell Hashimoto 52b310aeda website: document iam instance profile 2013-07-31 11:43:52 -07:00
Mitchell Hashimoto 6f8076ca1d Update CHANGELOG 2013-07-31 11:42:05 -07:00
Mitchell Hashimoto 18f34173cf fmt 2013-07-31 11:41:07 -07:00
James Massara 4bd3afb3ee Added IAM Instance Profile support
Conflicts:
	builder/amazon/common/run_config.go
2013-07-31 11:40:59 -07:00
Mitchell Hashimoto 0820da6039 builder/amazon/common: wait for terminated state 2013-07-31 09:37:43 -07:00
Mitchell Hashimoto 02c20af35e packer/plugin: trim whitespace on right of log data from plugin 2013-07-31 09:37:03 -07:00
Mitchell Hashimoto 8cf245eaa5 builder/amazon/ebs: don't need this variable 2013-07-31 09:35:26 -07:00
Mitchell Hashimoto 6740f99f26 builder/amazon/common: prefer public DNS if possible for SSH 2013-07-31 09:30:52 -07:00
Mitchell Hashimoto fd3f9e9e4c builder/common: channels instead of unsafe read/write for communicate 2013-07-31 09:23:04 -07:00
Mitchell Hashimoto 838e60cf08 Update CHANGELOG 2013-07-30 22:42:40 -07:00
Mitchell Hashimoto ab73e65a59 Merge branch 'builder-amazon-ebs-chroot'
This adds "no-launch" EBS-backed AMI creation, which allows the creation
of these AMIs without launching a new EC2 instance. Instead this builder
is meant to be run on an existing EC2 instance and uses that instance as
a host to attach root EBS volumes, provision in a chroot, etc.
2013-07-30 22:40:38 -07:00
Mitchell Hashimoto 377493db4f builder/amazon/chroot: use set AMI name 2013-07-30 22:33:41 -07:00
Mitchell Hashimoto ffe1e5f57b builder/amazon/chroot: default volumes dir is relative 2013-07-30 22:31:07 -07:00
Mitchell Hashimoto 005c485bfb builder/amazon/chroot: validate that chroot_mounts are 3 elements 2013-07-30 22:29:27 -07:00
Mitchell Hashimoto f0a0816736 builder/amazon/chroot: let go of flock earlier for parallelism 2013-07-30 22:25:33 -07:00
Mitchell Hashimoto 6f8c915ef4 website: docs for amazon-chroot 2013-07-30 22:17:58 -07:00
Mitchell Hashimoto cccf3ddc79 builder/amazon/chroot: fix compilaton on Windows 2013-07-30 21:50:59 -07:00
Mitchell Hashimoto 167bdd9a46 builder/amazon/chroot: flock so that device searching is safe 2013-07-30 21:48:37 -07:00
Mitchell Hashimoto 997b81da21 builder/amazon/chroot: find available device 2013-07-30 21:19:57 -07:00
Mitchell Hashimoto 6b7f59216e builder/amazon/chroot: get rid of AttachedDevicePath 2013-07-30 20:48:07 -07:00
Mitchell Hashimoto 089d284067 builder/amazon/chroot: register AMI 2013-07-30 18:28:21 -07:00
Mitchell Hashimoto 6f4db32418 builder/amazon/chroot: switch func type to interface
Was getting weird behavior... see
https://groups.google.com/d/msg/golang-nuts/a1kymwSVt2M/FwcCuBl1_48
2013-07-30 17:56:42 -07:00
Mitchell Hashimoto 21002e04a1 builder/amazon/common: correct the log statement 2013-07-30 17:32:41 -07:00
Mitchell Hashimoto df4c844493 builder/amazon/chroot: log the exit code for the chroot communicator 2013-07-30 17:23:37 -07:00
Mitchell Hashimoto 613322d98b communicator/ssh: log when remote exits 2013-07-30 17:06:22 -07:00
Mitchell Hashimoto 7ab4490733 builder/amazon/chroot: step to snapshot the root image 2013-07-30 16:58:58 -07:00
Mitchell Hashimoto 36be9a9bf6 builder/amazon/chroot: initial len should be 0 so we don't have empty 2013-07-30 16:45:49 -07:00
Mitchell Hashimoto 44c6103fd0 builder/amazon/chroot: perform early cleanup 2013-07-30 16:41:29 -07:00
Mitchell Hashimoto f79f113bcc plugin/builder-amazon-chroot: add test file 2013-07-30 16:08:45 -07:00
Mitchell Hashimoto f3a4d44066 builder/amazon/chroot: make sure to remove files before copy 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 998712250c builder/amazon/chroot: copyfiles support 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 759ff1ace1 builder/amazon/chroot: upload should os.Create, not os.Open 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto a3a2ace843 builder/amazon/chroot: enable the chroot provisioner 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 9f2399516a builder/amazon/chroot: provision within the chroot 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 778657e995 builder/amazon/chroot: unmount in reverse 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 9dc55ee56c builder/amazon/chroot: special case bind fstype 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto bec7b26836 builder/amazon/chroot: /bin/sh 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 462e48cac4 builder/amazon/chroot: mount extra paths 2013-07-30 16:07:50 -07:00
Mitchell Hashimoto 9bb9f02b99 builder/amazon/chroot: process MountPath template 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 710b6a41ec builder/amazon/chroot: use the attached device path 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 713f466670 builder/amazon/chroot: use mountcommand configs 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 22aad9c87c builder/amazon/chroot: mount the root device 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto df85c67e4a builder/amazon/chroot: more settings, validation 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 1e9cc89f73 builder/amazon/chroot: improved logging about what volume created 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 4358830953 builder/amazon/chroot: fix types 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 121e9791a3 builder/amazon/chroot: only let it run on Linux 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto af492621de builder/amazon/chroot: put device in state bag 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 5a70c82e25 builder/amazon/chroot: more valid transition states while detaching 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 702f299343 builder/amazon/chroot: Attach volume 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 7c04d634f9 builder/amazon/chroot: wait for volume to beecome ready 2013-07-30 16:07:49 -07:00
Mitchell Hashimoto 3f0c4b0e19 builder/amazon/common: generic wait for state to wait for any state 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto 3f49f1a6d0 builder/amazon/chroot: delete the EBS volume after we're done 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto e5f0cbe298 builder/amazon/chroot: step to gather instance info 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto 726c4a68ef builder/amazon/chroot: create the volume 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto 90a27bc57b builder/amazon/chroot: extract the source AMI info 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto c189c7ed12 builder/amazon/chroot: verify we're on an EC2 instance 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto b329323bb2 builder/amazon/chroot: rename to builder-amazon-chroot 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto c6fde36b5e builder/amazon/chroot: boilerplate 2013-07-30 16:07:48 -07:00
Mitchell Hashimoto 8046d2635a add tests files to everything to test compilation 2013-07-30 16:05:20 -07:00
Mitchell Hashimoto 8c341b0c78 packer/plugin: flush os.Stderr whenever a plugin ends 2013-07-30 10:48:59 -07:00
Mitchell Hashimoto ce0322662d packer/plugin: use chan struct{} for condition variable behavior 2013-07-30 10:43:43 -07:00
Mitchell Hashimoto 31731414be packer/plugin: clean up stderr 2013-07-30 10:41:02 -07:00
Mitchell Hashimoto 467474910c packer/plugin: more stable stderr logging 2013-07-30 10:38:48 -07:00
Mitchell Hashimoto 6110079401 packer: Use ch for condition variable
Thanks @titanous I didnt know this worked.
2013-07-29 13:20:57 -07:00
Mitchell Hashimoto 1812efe3b5 communicator/ssh: use SetExited 2013-07-29 12:12:42 -07:00
Mitchell Hashimoto 1400645d69 packer/rpc: Use the SetExited for remote communicators 2013-07-29 12:07:37 -07:00
Mitchell Hashimoto 6a940d9ea8 packer: use locks/conds to avoid races on RemoteCmd.Exited [GH-42] 2013-07-29 12:04:58 -07:00
Ross Smith II d1cdd9150b Updated website for vmware workstation support, fixes #221/#222 2013-07-29 08:20:48 -07:00
Mitchell Hashimoto 0bff938880 builder/virtualbox,vmware: use DownloadableURL 2013-07-29 00:13:30 -07:00
Mitchell Hashimoto 85ec53e76c builder/common: Don't prepend slash to URL path if path is empty 2013-07-29 00:13:30 -07:00
Mitchell Hashimoto ac8548d998 builder/common: make rel path absolute in DownloadableURL [GH-215] 2013-07-29 00:13:30 -07:00
Mitchell Hashimoto 1ebb1b7ef6 builder/common: add DownloadableURL to remove duplication parsing URLs 2013-07-29 00:13:30 -07:00
Rafael Garcia 728f9ecdbd provisioner/salt-masterless: rename 2013-07-28 23:30:03 -07:00
Ross Smith II b8f1d484be added missing fallthru in case statement for linux option, fixes #221, #222 2013-07-28 20:41:09 -07:00
Mitchell Hashimoto 9a37de618d Merge pull request #224 from zyegfryed/patch-1
website: typo
2013-07-28 11:42:46 -07:00
Sébastien Fievet a5054cf314 Typo 2013-07-28 16:49:03 +02:00
Rafael Garcia 880dd75675 provisioner/salt: use cmd.StartWithUi 2013-07-27 18:12:18 -07:00
Ross Smith II 2e4585bc9c gofmt fixes, improved/added log messages, fixes #221/#222 2013-07-27 14:59:23 -07:00
Ross Smith II 1cc36f11bc fixes #221 Add support for VMWare Workstation in Windows 2013-07-27 13:00:21 -07:00
Rafael Garcia 1b0d316456 provisioner/salt: simple tests 2013-07-27 00:14:32 -07:00
Rafael Garcia 8fe0733676 provisioner/salt: put state tree temporarily in /tmp/salt by default 2013-07-27 00:14:32 -07:00
Rafael Garcia e5f5ba67c4 provisioner/salt: document 3c1f142 2013-07-27 00:14:32 -07:00
Rafael Garcia b883abe1e2 provisioner/salt: copy local state tree to remote, run highstate 2013-07-27 00:14:32 -07:00
Rafael Garcia af3e922b26 provisioner/salt: docs 2013-07-27 00:14:32 -07:00
Rafael Garcia 3654a9a9f5 provisioner/salt: pass args to bootstrap 2013-07-27 00:14:32 -07:00
Rafael Garcia 9bdd5a927a provisioner/salt: install salt 2013-07-27 00:14:32 -07:00
Mitchell Hashimoto beb97af6ce Up version for dev 2013-07-26 10:15:35 -07:00
Mitchell Hashimoto 801a13dd38 v0.2.1 2013-07-26 09:54:05 -07:00
Mitchell Hashimoto e5b14ac450 builder/amazon/common: wait for AMI to be available [GH-207] 2013-07-26 09:38:16 -07:00
Mitchell Hashimoto e6557c53c2 ctrl-c closes stdin for plugins so that they are unblocked 2013-07-25 23:27:13 -07:00
Mitchell Hashimoto b455d70346 Remove TODO, it was from pre-0.1 2013-07-25 23:14:56 -07:00
Mitchell Hashimoto 48b08fe71a update comments in signal.go 2013-07-25 21:36:46 -05:00
Mitchell Hashimoto 74ef543d5c packer/plugin: connect stdin to parent stdin 2013-07-25 21:24:49 -05:00
Mitchell Hashimoto 9f38f4cb47 fmt 2013-07-25 20:49:39 -05:00
Mitchell Hashimoto 7a00b240c1 builder/amazon/*: sigint while waiting for state change works 2013-07-25 20:49:15 -05:00
Mitchell Hashimoto f5f4266371 Update CHANGELOG 2013-07-25 15:20:20 -05:00
Mitchell Hashimoto 1ec2eacb92 Merge pull request #218 from jstamerj/master
builder/virtualbox, communicator/ssh: fixes for running on windows
2013-07-25 13:19:08 -07:00
Jeff Stamerjohn 88dcb43a3f Merge pull request #1 from jstamerj/jstamerj/windows
Jstamerj/windows
2013-07-25 09:32:13 -07:00
Jeff Stamerjohn c5aad085f3 Merge branch 'master' into jstamerj/windows 2013-07-25 09:31:02 -07:00
Jeff Stamerjohn 41df9fa2e8 Windows fixes
Strip carriage returns from end of lines in virtualbox/driver.go, fixing wait for virtualbox host shutdown.
Fix target upload directory path in communicator.go to use forward slashes.  (When running on windows path/filepath returns backslashes...which does not work when the target host is unix).
2013-07-25 09:28:04 -07:00
Mitchell Hashimoto 029dabaa49 Update CHANGELOG 2013-07-25 10:55:25 -05:00
Mitchell Hashimoto 4857faa363 Merge branch 'builder-amazon-instance'
This adds the "amazon-instance" builder to Packer which can create
instance-storage backed AMIs. The instance-storage backed AMIs are built
by launching another instance-store backed instance, provisioning it,
and then using EC2 AMI tools to bundle and register a new AMI.
2013-07-25 10:52:16 -05:00
Mitchell Hashimoto 0130b1310c builder/amazon/instance: default x509_upload_path 2013-07-25 10:51:38 -05:00
Mitchell Hashimoto 30a4bf7687 website: document the amazon-instance builder 2013-07-25 10:51:21 -05:00
Mitchell Hashimoto 8c3281405d builder/amazon/*: wait for AMI to be ready in common, use it instance 2013-07-25 00:56:37 -05:00
Mitchell Hashimoto 072d7a647a builder/amazon/instance: register AMI using API 2013-07-25 00:45:55 -05:00
Mitchell Hashimoto 77b92cd048 builder/amazon/instance: register the AMI 2013-07-25 00:19:04 -05:00
Mitchell Hashimoto 17f6914fc1 builder/amazon/instance: prefix has CreateTime support 2013-07-24 23:51:46 -05:00
Mitchell Hashimoto 5e6695264a builder/amazon/instance: upload bundle 2013-07-24 23:29:21 -05:00
Mitchell Hashimoto ad1015f35c builder/amazon/instance: bundle volume and keep track of dir 2013-07-24 23:22:16 -05:00
Mitchell Hashimoto 801541714e builder/amazon/instance: run ec2-bundle-vol in batch mode 2013-07-24 22:51:59 -05:00
Mitchell Hashimoto 3f543dddaa builder/amazon/instance: sudo the ec2-bundle-vol by default 2013-07-24 16:07:33 -05:00
Mitchell Hashimoto b5552c00f3 builder/amazon/instance: detect error bundling 2013-07-24 15:44:58 -05:00
Mitchell Hashimoto c6358b0475 builder/amazon/instance: ec2-bundle-vol is called 2013-07-24 15:43:35 -05:00
Mitchell Hashimoto 544b4b1429 builder/amazon/instance: check for the ami tools 2013-07-23 23:19:44 -05:00
Mitchell Hashimoto 3efdb009d7 builder/amazon/common: SSH into private IP if in VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b9b5de5684 fmt 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto a5546fa0f3 builder/amazon/*: Fix failing tests from rebase of VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b11b60746b builder/amazon/instance: boilerplate for bundle volume 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto c39299c21f builder/amazon/instance: after upload x509, should continue 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto d11cabf893 builder/amazon/instance: upload x509 cert 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto caaa6667ed plugin/builder-amazon-instance: add the plugin 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto b91fb5e1a0 fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto a780d0e0dd builder/amazon/common: extract Artifact 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5a4f5c3f6f builder/amazon: provision on instance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5c7c0a6ee2 builder/amazon: extract SSH connect funcs 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 51f7db35f4 builder/amazon: extract StepRunSourceInstance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 579eaaabc5 builder/amazon/common: panic if SSHPot is 0 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto f8319f8881 builder/amazon: extract StepSecurityGroup 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto e67e4cfa16 builder/amazon: extract StepKeyPair for both 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5921492c8e fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto d489f99aad builder/amazon/*: clean up tests 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto ae864b3efa builder/amazon/common: tests for runconfig 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 9fb778c012 builder/common: common config struct for Packer configs 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 45d50279f3 builder/amazon/ebs: validate access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 6fdf237c15 builder/amazon/common: RunConfig for launch info 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 8f649af165 builder/amazon/common: AccessConfig for standard access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 9348f481dd builder/amazon/instance: boilerplate 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 17acee653b packer: In the case of an error, close stdout/stderr writers 2013-07-23 22:46:51 -05:00
Mitchell Hashimoto 1c5e9848b5 provisioner/shell: use StartWithUi 2013-07-23 22:44:32 -05:00
Mitchell Hashimoto 479855a4f1 builder/virtualbox: shutdown messages go to Ui 2013-07-23 22:42:39 -05:00
Mitchell Hashimoto 3641c1a708 packer: Preserve original stdout/stderr on StartWithUi
/cc @sit
2013-07-23 22:38:03 -05:00
Mitchell Hashimoto 2e82358d2a fmt 2013-07-23 22:36:09 -05:00
Mitchell Hashimoto 32c84c17b1 packer: RemoteCmd.StartWithUi thanks to @sit
I ended up working on this on my own outside your PR, sorry, but it is
basically the same as yours.
2013-07-23 22:35:05 -05:00
Mitchell Hashimoto bd1fe21718 packer/rpc: remove unused test file 2013-07-23 13:45:35 -07:00
Mitchell Hashimoto 6422fabd19 command/build: if any builds fail, exit with 1 exit status 2013-07-23 13:36:07 -07:00
Mitchell Hashimoto 54fb31b7a3 Update CHANGELOG 2013-07-21 22:40:02 -07:00
Mitchell Hashimoto 51b63efe50 Merge pull request #210 from rgarcia/vpc-launch
builder/amazon: support launching in VPC
2013-07-21 22:39:01 -07:00
Rafael Garcia 282554b2b0 builder/amazon/ebs: support launching in VPC
adds `vpc_id` and `subnet_id` to builder config

depends on https://github.com/mitchellh/goamz/pull/4
2013-07-21 00:44:14 -07:00
Mitchell Hashimoto 38ae1a0ba9 packer: Properly handle ? in URLs in cache keys
/cc @sit - Found another edge case
2013-07-20 19:20:29 -07:00
Mitchell Hashimoto e3478c38ef builder/vmware: more flexible ISO removing
/cc @timsutton - This is a bit more flexible.
2013-07-20 19:08:20 -07:00
Mitchell Hashimoto 7cab4c4058 Update CHANGELOG 2013-07-20 17:00:36 -07:00
Mitchell Hashimoto 9be941ec3e Merge pull request #203 from timsutton/vmware-eject-isos
builder/vmware: Remove ISO from ide1 in stepCleanVMX
2013-07-20 16:59:55 -07:00
Mitchell Hashimoto e51e9cd56e Merge pull request #204 from sit/builder-output-directory-error
builder/v*: Include path to output dir in error
2013-07-20 16:58:39 -07:00
Mitchell Hashimoto 9c393742df Update CHANGELOG 2013-07-20 16:55:22 -07:00
Mitchell Hashimoto f64fd019e5 packer: Extension works properly with URLs too
/cc @sit
2013-07-20 16:53:55 -07:00
Mitchell Hashimoto cf9bc2c819 packer: Use strings operations, faster than regexp and easy
/cc @sit
2013-07-20 16:50:18 -07:00
Mitchell Hashimoto 9349c81631 Merge pull request #205 from sit/preserve-cache-suffix
Preserve suffix of files put in the cache.
2013-07-20 16:40:31 -07:00
Mitchell Hashimoto f97148bc22 Update changelog 2013-07-20 16:37:48 -07:00
Mitchell Hashimoto cff38c6aea Merge pull request #206 from psi/add-more-special-keys
builder/vmware, builder/virtualbox: add special keys for backspace, delete and F1-F12
2013-07-20 16:36:30 -07:00
JD Harrington 6028a3c3de Add special keys for backspace, delete and F1-F12 2013-07-20 15:37:01 -04:00
Mitchell Hashimoto d179d1ad00 website: sleep in getting started 2013-07-20 12:35:51 -07:00
Emil Sit 20d3946eae builder/v*: Include path to output dir in error 2013-07-20 00:36:59 -04:00
Emil Sit bc4b2d079d Revert "builder/virtualbox: Copy ISO because VirtualBox can't recognize"
This reverts commit 688be43811007d33b9c70dec93b43782a4f71c06; instead
of copying the ISO (which can be huge), we have adjusted the cache path
to include the extension of the source file.

Conflicts:

	builder/virtualbox/step_download_iso.go
2013-07-20 00:26:46 -04:00
Emil Sit 27da543a41 packer/cache: Preserve any extension found on keys
This allows us to hand cache paths to any programs that may want to
interpret file extensions in order to behave differently. For
example, VirtualBox may want ISO images to end with .iso.
2013-07-20 00:19:05 -04:00
Emil Sit fd1ec31a1d packer/cache: Extract cache path calculation to method 2013-07-20 00:15:14 -04:00
Timothy Sutton 7e6e699593 Remove ISO from ide1 in stepCleanVMX 2013-07-19 20:46:26 -04:00
Mitchell Hashimoto 7d1f799ced builder/*: switch to MultiErrorAppend 2013-07-19 19:08:25 -04:00
Mitchell Hashimoto 40ec63d349 packer: MultiErrorAppend helper method. 2013-07-19 18:14:50 -04:00
Mitchell Hashimoto 5dbae2efde builder/common: CheckUnusedConfig 2013-07-19 15:09:13 -04:00
Mitchell Hashimoto d1ecd89635 builder/*: extract to common the config decoding 2013-07-19 15:00:32 -04:00
Mitchell Hashimoto 6f6a27b008 builder/common: extract out decode config 2013-07-19 14:59:04 -04:00
Mitchell Hashimoto 5b8c372fdc communicator/ssh: style
/cc @markpeek - The main thing is that the style for variables/methods
in Go is to use CamelCase (with the first letter generally downcased
unless an exported thing).
2013-07-19 14:16:39 -04:00
Mitchell Hashimoto c80db71361 packer: fix failing tests 2013-07-19 14:13:02 -04:00
Mitchell Hashimoto abf4cfecb2 communicator/ssh: remove complete TODO 2013-07-19 14:09:20 -04:00
Mitchell Hashimoto 27fc4da5f9 Update CHANGELOG 2013-07-19 14:08:59 -04:00
Mitchell Hashimoto 0784f7961b Merge pull request #195 from markpeek/markpeek-file-error
communicator/ssh: check scp codes and report errors
2013-07-19 11:07:03 -07:00
Mitchell Hashimoto 71c0b2c1d5 Update CHANGELOG 2013-07-19 14:02:54 -04:00
Mitchell Hashimoto 9f93ca7d3b website: powered by bintray 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto bf3e137a5a website: remove the ?direct in downloads to use CDN 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto f226bd13af Merge pull request #199 from qur/builder-name
packer: builder name should be removed from rawConfig
2013-07-19 11:02:05 -07:00
Julian Phillips dca4fabd1b packer/template: Remove name from builder rawConfig
This prevents the builder from rejecting the name (which it doesn't
know about) when validating the config.
2013-07-19 15:36:13 +01:00
Julian Phillips a31fd29b8b packer/template: Test that builder.rawConfig excludes name
The name isn't actually part of the builder config, so it should be
removed during parsing.
2013-07-19 15:35:05 +01:00
Radu Voicilas 0f1bc9ee6f Add a clean target to the Makefile 2013-07-18 10:40:23 -04:00
Mark Peek ed75091bd4 Log an upload error closer to when it occurs
When there is an error returned from the Upload it is propagated back
to the main loop and may have additional log messages before the actual
error is displayed. This displays a log message close to where the error
actually occurred.
2013-07-17 18:17:46 -07:00
Mark Peek a8a53fb0b4 Return errors from remote scp command
Currently file provisioners will silently fail when there is an error
with the remote side. This commit checks the scp error code and will
return the error message from the remote.
2013-07-17 18:15:42 -07:00
Mitchell Hashimoto 104178080c Merge pull request #192 from dougm/fusion-leases-path
builder/vmware: fix Fusion DhcpLeasesPath
2013-07-17 16:23:28 -07:00
Doug MacEachern 923a541d8c fix Fusion DhcpLeasesPath 2013-07-17 13:03:05 -07:00
Mitchell Hashimoto bca05ddd5b builder/vmware: look for license for WS9 2013-07-16 14:58:16 -07:00
Mitchell Hashimoto 04b8ff89a6 builder/vmware: workstation 9 show stderr if command fails 2013-07-16 14:46:34 -07:00
Mitchell Hashimoto b9f0b81821 scripts: Use XC_ARCH/XC_OS to limit what arch/os combo is built 2013-07-16 14:36:54 -07:00
Mitchell Hashimoto 56402f9432 Update CHANGELOG 2013-07-16 14:24:56 -07:00
Mitchell Hashimoto cbddd413f1 builder/vmware: wording 2013-07-16 14:23:52 -07:00
Mitchell Hashimoto 229011def4 builder/vmware: split drivers out into individual files 2013-07-16 14:23:01 -07:00
Julian Phillips d7e5a3bb1d builder/vmware: Add support for Workstation 9 (Linux).
Now the VMware builder should automatically pick between Fusion 5 and
Workstation 9, based on which one is installed.
2013-07-16 14:11:59 -07:00
Mitchell Hashimoto 8cb035cc65 Up version for dev 2013-07-16 11:44:15 -07:00
Mitchell Hashimoto e6dcb739d7 website: fix download parsing 2013-07-16 11:42:11 -07:00
146 changed files with 5650 additions and 1209 deletions
+54
View File
@@ -1,3 +1,57 @@
## 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:
+3
View File
@@ -10,6 +10,9 @@ all:
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./scripts/build.sh
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
format:
go fmt ./...
-7
View File
@@ -1,7 +0,0 @@
* 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
+199
View File
@@ -0,0 +1,199 @@
// 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"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"runtime"
"text/template"
)
// 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"`
AMIName string `mapstructure:"ami_name"`
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
MountCommand string `mapstructure:"mount_command"`
MountPath string `mapstructure:"mount_path"`
SourceAmi string `mapstructure:"source_ami"`
UnmountCommand string `mapstructure:"unmount_command"`
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
// 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.MountCommand == "" {
b.config.MountCommand = "mount"
}
if b.config.MountPath == "" {
b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}"
}
if b.config.UnmountCommand == "" {
b.config.UnmountCommand = "umount"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
for _, mounts := range b.config.ChrootMounts {
if len(mounts) != 3 {
errs = packer.MultiErrorAppend(
errs, errors.New("Each chroot_mounts entry should be three elements."))
break
}
}
if b.config.SourceAmi == "" {
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
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)
// 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{
&StepInstanceInfo{},
&StepSourceAMIInfo{},
&StepFlock{},
&StepPrepareDevice{},
&StepCreateVolume{},
&StepAttachVolume{},
&StepEarlyUnflock{},
&StepMountDevice{},
&StepMountExtra{},
&StepCopyFiles{},
&StepChrootProvision{},
&StepEarlyCleanup{},
&StepSnapshot{},
&StepRegisterAMI{},
}
// 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 := &awscommon.Artifact{
Amis: state["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()
}
}
+84
View File
@@ -0,0 +1,84 @@
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"
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_ChrootMounts(t *testing.T) {
b := &Builder{}
config := testConfig()
config["chroot_mounts"] = nil
err := b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
config["chroot_mounts"] = [][]string{
[]string{"bad"},
}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SourceAmi(t *testing.T) {
b := &Builder{}
config := testConfig()
config["source_ami"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["source_ami"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
}
+6
View File
@@ -0,0 +1,6 @@
package chroot
// Cleanup is an interface that some steps implement for early cleanup.
type Cleanup interface {
CleanupFunc(map[string]interface{}) error
}
+87
View File
@@ -0,0 +1,87 @@
package chroot
import (
"github.com/mitchellh/packer/packer"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
)
// Communicator is a special communicator that works by executing
// commands locally but within a chroot.
type Communicator struct {
Chroot string
}
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
chrootCmdPath, err := exec.LookPath("chroot")
if err != nil {
return err
}
localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.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 executation ended 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)
f, err := os.Create(dst)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, r); err != nil {
return err
}
return nil
}
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
}
@@ -0,0 +1,14 @@
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")
}
}
+61
View File
@@ -0,0 +1,61 @@
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 {
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
@@ -0,0 +1,16 @@
// +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
}
+32
View File
@@ -0,0 +1,32 @@
// +build !windows
package chroot
import (
"errors"
"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|LOCK_NB)
if err != nil {
errno, ok := err.(syscall.Errno)
if ok && errno == syscall.EWOULDBLOCK {
return errors.New("file already locked")
}
return err
}
return nil
}
func unlockFile(f *os.File) error {
return syscall.Flock(int(f.Fd()), LOCK_UN)
}
+129
View File
@@ -0,0 +1,129 @@
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 map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
device := state["device"].(string)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
volumeId := state["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["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{
Conn: ec2conn,
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.")
}
return nil, resp.Volumes[0].Attachments[0].Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["attach_cleanup"] = s
return multistep.ActionContinue
}
func (s *StepAttachVolume) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepAttachVolume) CleanupFunc(state map[string]interface{}) error {
if !s.attached {
return nil
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["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{
Conn: ec2conn,
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
}
state := "detached"
if len(resp.Volumes[0].Attachments) > 0 {
state = resp.Volumes[0].Attachments[0].Status
}
return nil, state, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
return fmt.Errorf("Error waiting for volume: %s", err)
}
return nil
}
@@ -0,0 +1,11 @@
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")
}
}
@@ -0,0 +1,34 @@
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 map[string]interface{}) multistep.StepAction {
hook := state["hook"].(packer.Hook)
mountPath := state["mount_path"].(string)
ui := state["ui"].(packer.Ui)
// Create our communicator
comm := &Communicator{
Chroot: mountPath,
}
// Provision
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 (s *StepChrootProvision) Cleanup(state map[string]interface{}) {}
+107
View File
@@ -0,0 +1,107 @@
package chroot
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io"
"log"
"os"
"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 map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
mountPath := state["mount_path"].(string)
ui := state["ui"].(packer.Ui)
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)
if err := s.copySingle(chrootPath, path); err != nil {
err := fmt.Errorf("Error copying file: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.files = append(s.files, chrootPath)
}
}
state["copy_files_cleanup"] = s
return multistep.ActionContinue
}
func (s *StepCopyFiles) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepCopyFiles) CleanupFunc(map[string]interface{}) error {
if s.files != nil {
for _, file := range s.files {
log.Printf("Removing: %s", file)
if err := os.Remove(file); err != nil {
return err
}
}
}
s.files = nil
return nil
}
func (s *StepCopyFiles) copySingle(dst, src string) error {
// Stat the src file so we can copy the mode later
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
// Remove any existing destination file
if err := os.Remove(dst); err != nil {
return err
}
// Copy the files
srcF, err := os.Open(src)
if err != nil {
return err
}
defer srcF.Close()
dstF, err := os.Create(dst)
if err != nil {
return err
}
defer dstF.Close()
if _, err := io.Copy(dstF, srcF); err != nil {
return err
}
dstF.Close()
// Match the mode
if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
return err
}
return nil
}
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,107 @@
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 map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
image := state["source_image"].(*ec2.Image)
instance := state["instance"].(*ec2.Instance)
ui := state["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["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["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{
Conn: ec2conn,
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
}
return nil, resp.Volumes[0].Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["volume_id"] = s.volumeId
return multistep.ActionContinue
}
func (s *StepCreateVolume) Cleanup(state map[string]interface{}) {
if s.volumeId == "" {
return
}
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["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))
}
}
@@ -0,0 +1,37 @@
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 map[string]interface{}) multistep.StepAction {
ui := state["ui"].(packer.Ui)
cleanupKeys := []string{
"copy_files_cleanup",
"mount_extra_cleanup",
"mount_device_cleanup",
"attach_cleanup",
}
for _, key := range cleanupKeys {
c := state[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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepEarlyCleanup) Cleanup(state map[string]interface{}) {}
@@ -0,0 +1,28 @@
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 map[string]interface{}) multistep.StepAction {
cleanup := state["flock_cleanup"].(Cleanup)
ui := state["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepEarlyUnflock) Cleanup(state map[string]interface{}) {}
+72
View File
@@ -0,0 +1,72 @@
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 map[string]interface{}) multistep.StepAction {
ui := state["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["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// LOCK!
if err := lockFile(f); err != nil {
err := fmt.Errorf("Error creating lock: %s", err)
state["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["flock_cleanup"] = s
return multistep.ActionContinue
}
func (s *StepFlock) Cleanup(state map[string]interface{}) {
s.CleanupFunc(state)
}
func (s *StepFlock) CleanupFunc(state map[string]interface{}) 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
@@ -0,0 +1,11 @@
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")
}
}
@@ -0,0 +1,57 @@
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 map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["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["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["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
instance := &instancesResp.Reservations[0].Instances[0]
state["instance"] = instance
return multistep.ActionContinue
}
func (s *StepInstanceInfo) Cleanup(map[string]interface{}) {}
+103
View File
@@ -0,0 +1,103 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"os/exec"
"path/filepath"
"text/template"
)
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 map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ui := state["ui"].(packer.Ui)
device := state["device"].(string)
mountPathRaw := new(bytes.Buffer)
t := template.Must(template.New("mountPath").Parse(config.MountPath))
t.Execute(mountPathRaw, &mountPathData{
Device: filepath.Base(device),
})
var err error
mountPath := mountPathRaw.String()
mountPath, err = filepath.Abs(mountPath)
if err != nil {
err := fmt.Errorf("Error preparing mount directory: %s", err)
state["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Mounting the root device...")
stderr := new(bytes.Buffer)
mountCommand := fmt.Sprintf("%s %s %s", config.MountCommand, device, mountPath)
cmd := exec.Command("/bin/sh", "-c", mountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
err := fmt.Errorf(
"Error mounting root volume: %s\nStderr: %s", err, stderr.String())
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the mount path so we remember to unmount it later
s.mountPath = mountPath
state["mount_path"] = s.mountPath
state["mount_device_cleanup"] = s
return multistep.ActionContinue
}
func (s *StepMountDevice) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepMountDevice) CleanupFunc(state map[string]interface{}) error {
if s.mountPath == "" {
return nil
}
config := state["config"].(*Config)
ui := state["ui"].(packer.Ui)
ui.Say("Unmounting the root device...")
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath)
cmd := exec.Command("/bin/sh", "-c", unmountCommand)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Error unmounting root device: %s", err)
}
s.mountPath = ""
return nil
}
@@ -0,0 +1,11 @@
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")
}
}
+100
View File
@@ -0,0 +1,100 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
"os/exec"
)
// StepMountExtra mounts the attached device.
//
// Produces:
// mount_extra_cleanup CleanupFunc - To perform early cleanup
type StepMountExtra struct {
mounts []string
}
func (s *StepMountExtra) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
mountPath := state["mount_path"].(string)
ui := state["ui"].(packer.Ui)
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["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 := fmt.Sprintf(
"%s %s %s %s",
config.MountCommand,
flags,
mountInfo[1],
innerPath)
cmd := exec.Command("/bin/sh", "-c", mountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
err := fmt.Errorf(
"Error mounting: %s\nStderr: %s", err, stderr.String())
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.mounts = append(s.mounts, innerPath)
}
state["mount_extra_cleanup"] = s
return multistep.ActionContinue
}
func (s *StepMountExtra) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
return
}
}
func (s *StepMountExtra) CleanupFunc(state map[string]interface{}) error {
if s.mounts == nil {
return nil
}
config := state["config"].(*Config)
for len(s.mounts) > 0 {
var path string
lastIndex := len(s.mounts) - 1
path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex]
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path)
stderr := new(bytes.Buffer)
cmd := exec.Command("/bin/sh", "-c", 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
}
@@ -0,0 +1,11 @@
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")
}
}
@@ -0,0 +1,45 @@
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 map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ui := state["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["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Device: %s", device)
state["device"] = device
return multistep.ActionContinue
}
func (s *StepPrepareDevice) Cleanup(state map[string]interface{}) {}
@@ -0,0 +1,84 @@
package chroot
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
// StepRegisterAMI creates the AMI.
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ec2conn := state["ec2"].(*ec2.EC2)
image := state["source_image"].(*ec2.Image)
snapshotId := state["snapshot_id"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))
for i, device := range image.BlockDevices {
newDevice := device
if newDevice.DeviceName == image.RootDeviceName {
newDevice.SnapshotId = snapshotId
}
blockDevices[i] = newDevice
}
registerOpts := &ec2.RegisterImage{
Name: amiName,
Architecture: image.Architecture,
KernelId: image.KernelId,
RamdiskId: image.RamdiskId,
RootDeviceName: image.RootDeviceName,
BlockDevices: blockDevices,
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state["error"] = fmt.Errorf("Error registering AMI: %s", err)
ui.Error(state["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["amis"] = amis
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
if err := awscommon.WaitForAMI(ec2conn, registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(state map[string]interface{}) {}
+87
View File
@@ -0,0 +1,87 @@
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 map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
volumeId := state["volume_id"].(string)
ui.Say("Creating snapshot...")
createSnapResp, err := ec2conn.CreateSnapshot(volumeId, "")
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state["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{
Conn: ec2conn,
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.")
}
return nil, resp.Snapshots[0].Status, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["snapshot_id"] = s.snapshotId
return multistep.ActionContinue
}
func (s *StepSnapshot) Cleanup(state map[string]interface{}) {
if s.snapshotId == "" {
return
}
_, cancelled := state[multistep.StateCancelled]
_, halted := state[multistep.StateHalted]
if cancelled || halted {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["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))
}
}
}
@@ -0,0 +1,52 @@
package chroot
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{}
func (s *StepSourceAMIInfo) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
ui.Say("Inspecting the source AMI...")
imageResp, err := ec2conn.Images([]string{config.SourceAmi}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error querying AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(imageResp.Images) == 0 {
err := fmt.Errorf("Source AMI '%s' was not found!", config.SourceAmi)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
image := &imageResp.Images[0]
// 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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
state["source_image"] = image
return multistep.ActionContinue
}
func (s *StepSourceAMIInfo) Cleanup(map[string]interface{}) {}
+47
View File
@@ -0,0 +1,47 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"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) {
return aws.GetAuth(c.AccessKey, c.SecretKey)
}
// 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() []error {
if c.RawRegion != "" {
if _, ok := aws.Regions[c.RawRegion]; !ok {
return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)}
}
}
return nil
}
@@ -0,0 +1,27 @@
package common
import (
"testing"
)
func testAccessConfig() *AccessConfig {
return &AccessConfig{}
}
func TestAccessConfigPrepare_Region(t *testing.T) {
c := testAccessConfig()
c.RawRegion = ""
if err := c.Prepare(); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
c.RawRegion = "us-east-12"
if err := c.Prepare(); err == nil {
t.Fatal("should have error")
}
c.RawRegion = "us-east-1"
if err := c.Prepare(); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
+30
View File
@@ -0,0 +1,30 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
// WaitForAMI waits for the given AMI ID to become ready.
func WaitForAMI(c *ec2.EC2, imageId string) error {
for {
imageResp, err := c.Images([]string{imageId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
log.Println("AMI not found, probably state issues on AWS side. Trying again.")
continue
}
return err
}
if imageResp.Images[0].State == "available" {
return nil
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
}
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"fmt"
@@ -8,35 +8,39 @@ import (
"strings"
)
type artifact struct {
// Artifact is an artifact implementation that contains built AMIs.
type Artifact struct {
// A map of regions to AMI IDs.
amis map[string]string
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
Conn *ec2.EC2
}
func (*artifact) BuilderId() string {
return BuilderId
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*artifact) Files() []string {
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 {
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 {
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)
}
@@ -44,12 +48,12 @@ func (a *artifact) String() string {
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *artifact) Destroy() error {
func (a *Artifact) Destroy() error {
errors := make([]error, 0)
for _, imageId := range a.amis {
for _, imageId := range a.Amis {
log.Printf("Deregistering image ID: %s", imageId)
if _, err := a.conn.DeregisterImage(imageId); err != nil {
if _, err := a.Conn.DeregisterImage(imageId); err != nil {
errors = append(errors, err)
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/asserts"
@@ -10,7 +10,7 @@ func TestArtifact_Impl(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
var actual packer.Artifact
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
assert.Implementor(&Artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
@@ -22,7 +22,10 @@ func TestArtifactId(t *testing.T) {
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{
Amis: amis,
}
result := a.Id()
assert.Equal(result, expected, "should match output")
}
@@ -39,7 +42,7 @@ west: bar`
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{Amis: amis}
result := a.String()
assert.Equal(result, expected, "should match output")
}
+69
View File
@@ -0,0 +1,69 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"log"
"time"
)
type StateChangeConf struct {
Conn *ec2.EC2
Pending []string
Refresh func() (interface{}, string, error)
StepState map[string]interface{}
Target string
}
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{}, string, error) {
return func() (interface{}, string, error) {
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return nil, "", err
}
i = &resp.Reservations[0].Instances[0]
return i, i.State.Name, nil
}
}
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
for {
var currentState string
i, currentState, err = conf.Refresh()
if err != nil {
return
}
if currentState == conf.Target {
return
}
if conf.StepState != nil {
if _, ok := conf.StepState[multistep.StateCancelled]; ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if currentState == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
return
}
time.Sleep(2 * time.Second)
}
return
}
+61
View File
@@ -0,0 +1,61 @@
package common
import (
"errors"
"fmt"
"time"
)
// RunConfig contains configuration for running an instance from a source
// AMI and details on how to access that launched image.
type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"`
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
InstanceType string `mapstructure:"instance_type"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
SubnetId string `mapstructure:"subnet_id"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare() []error {
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "1m"
}
// 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"))
}
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
}
+89
View File
@@ -0,0 +1,89 @@
package common
import (
"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()
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
if err := c.Prepare(); 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(); 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(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
+48
View File
@@ -0,0 +1,48 @@
package common
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
var host string
instance := state["instance"].(*ec2.Instance)
if instance.DNSName != "" {
host = instance.DNSName
} else if instance.VpcId == "" {
host = instance.PrivateIpAddress
} else {
return "", errors.New("couldn't determine IP address for instance")
}
return fmt.Sprintf("%s:%d", host, port), nil
}
}
// SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the instance created over SSH using the generated
// private key.
func SSHConfig(username string) func(map[string]interface{}) (*gossh.ClientConfig, error) {
return func(state map[string]interface{}) (*gossh.ClientConfig, error) {
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: username,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,11 +10,11 @@ import (
"log"
)
type stepKeyPair struct {
type StepKeyPair struct {
keyName string
}
func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
@@ -23,9 +23,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := ec2conn.CreateKeyPair(keyName)
if err != nil {
err := fmt.Errorf("Error creating temporary keypair: %s", err)
state["error"] = err
ui.Error(err.Error())
state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err)
return multistep.ActionHalt
}
@@ -39,7 +37,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionContinue
}
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
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
@@ -0,0 +1,113 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type StepRunSourceInstance struct {
ExpectedRootDevice string
InstanceType string
SourceAMI string
IamInstanceProfile string
SubnetId string
instance *ec2.Instance
}
func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
keyName := state["keyPair"].(string)
securityGroupId := state["securityGroupId"].(string)
ui := state["ui"].(packer.Ui)
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
}
ui.Say("Launching a source AWS instance...")
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
if err != nil {
state["error"] = fmt.Errorf("There was a problem with the source AMI: %s", err)
return multistep.ActionHalt
}
if len(imageResp.Images) != 1 {
state["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["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["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = &runResp.Instances[0]
log.Printf("instance id: %s", s.instance.InstanceId)
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
stateChange := StateChangeConf{
Conn: ec2conn,
Pending: []string{"pending"},
Target: "running",
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
StepState: state,
}
latestInstance, err := WaitForState(&stateChange)
s.instance = latestInstance.(*ec2.Instance)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
state["error"] = err
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
}
stateChange := StateChangeConf{
Conn: ec2conn,
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Target: "terminated",
}
WaitForState(&stateChange)
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,40 +10,52 @@ import (
"log"
)
type stepSecurityGroup struct {
groupId string
type StepSecurityGroup struct {
SecurityGroupId string
SSHPort int
VpcId string
createdGroupId string
}
func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
func (s *StepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
if config.SecurityGroupId != "" {
log.Printf("Using specified security group: %s", config.SecurityGroupId)
state["securityGroupId"] = config.SecurityGroupId
if s.SecurityGroupId != "" {
log.Printf("Using specified security group: %s", s.SecurityGroupId)
state["securityGroupId"] = s.SecurityGroupId
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", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("Temporary group name: %s", groupName)
groupResp, err := ec2conn.CreateSecurityGroup(groupName, "Temporary group for Packer")
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.groupId = groupResp.Id
s.createdGroupId = groupResp.Id
// Authorize the SSH access
perms := []ec2.IPPerm{
ec2.IPPerm{
Protocol: "tcp",
FromPort: config.SSHPort,
ToPort: config.SSHPort,
FromPort: s.SSHPort,
ToPort: s.SSHPort,
SourceIPs: []string{"0.0.0.0/0"},
},
}
@@ -57,13 +69,13 @@ func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepActi
}
// Set some state data for use in future steps
state["securityGroupId"] = s.groupId
state["securityGroupId"] = s.createdGroupId
return multistep.ActionContinue
}
func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.groupId == "" {
func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.createdGroupId == "" {
return
}
@@ -71,10 +83,10 @@ func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary security group...")
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
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))
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
}
}
+39 -97
View File
@@ -8,43 +8,25 @@ package ebs
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"sort"
"strings"
"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"`
SecurityGroupId string `mapstructure:"security_group_id"`
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
type Builder struct {
@@ -53,81 +35,30 @@ type Builder struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
b.config.RawSSHTimeout = "1m"
}
// Accumulate any errors
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"))
errs = packer.MultiErrorAppend(
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))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
@@ -135,12 +66,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
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")
region, err := b.config.Region()
if err != nil {
return nil, err
}
auth, err := aws.GetAuth(b.config.AccessKey, b.config.SecretKey)
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
@@ -156,13 +87,23 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&stepKeyPair{},
&stepSecurityGroup{},
&stepRunSourceInstance{},
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "ebs",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
IamInstanceProfile: b.config.IamInstanceProfile,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshTimeout,
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
@@ -192,9 +133,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Build the artifact and return it
artifact := &artifact{
amis: state["amis"].(map[string]string),
conn: ec2conn,
artifact := &awscommon.Artifact{
Amis: state["amis"].(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
-160
View File
@@ -2,19 +2,9 @@ package ebs
import (
"github.com/mitchellh/packer/packer"
"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() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
@@ -75,30 +65,6 @@ func TestBuilderPrepare_AMIName(t *testing.T) {
}
}
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_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
@@ -110,129 +76,3 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
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_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")
}
}
-39
View File
@@ -1,39 +0,0 @@
package ebs
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
}
-31
View File
@@ -1,31 +0,0 @@
package ebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(config)
instance := state["instance"].(*ec2.Instance)
return fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
+7 -19
View File
@@ -5,8 +5,8 @@ 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"
"strconv"
"text/template"
"time"
@@ -52,28 +52,16 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
// 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
amis[ec2conn.Region.Name] = 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
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
if err := awscommon.WaitForAMI(ec2conn, createResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
@@ -1,86 +0,0 @@
package ebs
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...")
imageResp, err := ec2conn.Images([]string{config.SourceAmi}, ec2.NewFilter())
if err != nil {
state["error"] = fmt.Errorf("There was a problem with the source AMI: %s", err)
return multistep.ActionHalt
}
if imageResp.Images[0].RootDeviceType != "ebs" {
state["error"] = fmt.Errorf(
"The provided source AMI is instance-store based. The\n" +
"amazon-ebs bundler can only work with EBS based AMIs.")
return multistep.ActionHalt
}
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(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, 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")
}
+9 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
@@ -26,7 +27,14 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio
// Wait for the instance to actual stop
ui.Say("Waiting for the instance to stop...")
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
stateChange := awscommon.StateChangeConf{
Conn: ec2conn,
Pending: []string{"running", "stopping"},
Target: "stopped",
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance),
StepState: state,
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
state["error"] = err
+218
View File
@@ -0,0 +1,218 @@
// 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"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
"text/template"
)
// 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.RunConfig `mapstructure:",squash"`
AccountId string `mapstructure:"account_id"`
AMIName string `mapstructure:"ami_name"`
BundleDestination string `mapstructure:"bundle_destination"`
BundlePrefix string `mapstructure:"bundle_prefix"`
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
if b.config.BundleDestination == "" {
b.config.BundleDestination = "/tmp"
}
if b.config.BundlePrefix == "" {
b.config.BundlePrefix = "image-{{.CreateTime}}"
}
if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--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()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
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.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if b.config.S3Bucket == "" {
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
}
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 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, 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 := make(map[string]interface{})
state["config"] = &b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&StepUploadX509Cert{},
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
}
// 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 := &awscommon.Artifact{
Amis: state["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()
}
}
+219
View File
@@ -0,0 +1,219 @@
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"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["account_id"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
config["account_id"] = "0123-0456-7890"
err = b.Prepare(config)
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"
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_BundleDestination(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_destination"] = ""
err := b.Prepare(config)
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"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
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
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_S3Bucket(t *testing.T) {
b := &Builder{}
config := testConfig()
config["s3_bucket"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["s3_bucket"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_X509CertPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_cert_path"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_cert_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
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()
err = b.Prepare(config)
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"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_key_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
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()
err = b.Prepare(config)
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"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
@@ -0,0 +1,104 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type bundleCmdData struct {
AccountId string
Architecture string
CertPath string
Destination string
KeyPath string
Prefix string
PrivatePath string
}
type bundlePrefixData struct {
CreateTime string
}
type StepBundleVolume struct{}
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
x509RemoteCertPath := state["x509RemoteCertPath"].(string)
x509RemoteKeyPath := state["x509RemoteKeyPath"].(string)
// Verify the AMI tools are available
ui.Say("Checking for EC2 AMI tools...")
cmd := &packer.RemoteCmd{Command: "ec2-ami-tools-version"}
if err := comm.Start(cmd); err != nil {
state["error"] = fmt.Errorf("Error checking for AMI tools: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
cmd.Wait()
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"The EC2 AMI tools could not be detected. These must be manually\n" +
"via a provisioner or some other means and are required for Packer\n" +
"to create an instance-store AMI.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Bundle the volume
var bundlePrefix bytes.Buffer
prefixTData := bundlePrefixData{
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
t.Execute(&bundlePrefix, prefixTData)
var bundleCmd bytes.Buffer
tData := bundleCmdData{
AccountId: config.AccountId,
Architecture: instance.Architecture,
CertPath: x509RemoteCertPath,
Destination: config.BundleDestination,
KeyPath: x509RemoteKeyPath,
Prefix: bundlePrefix.String(),
PrivatePath: config.X509UploadPath,
}
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
t.Execute(&bundleCmd, tData)
ui.Say("Bundling the volume...")
cmd = new(packer.RemoteCmd)
cmd.Command = bundleCmd.String()
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Volume bundling failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Store the manifest path
manifestName := bundlePrefix.String() + ".manifest.xml"
state["manifest_name"] = manifestName
state["manifest_path"] = fmt.Sprintf(
"%s/%s", config.BundleDestination, manifestName)
return multistep.ActionContinue
}
func (s *StepBundleVolume) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,68 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ec2conn := state["ec2"].(*ec2.EC2)
manifestPath := state["remote_manifest_path"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: amiName,
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state["error"] = fmt.Errorf("Error registering AMI: %s", err)
ui.Error(state["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["amis"] = amis
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
if err := awscommon.WaitForAMI(ec2conn, registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,61 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"text/template"
)
type uploadCmdData struct {
AccessKey string
BucketName string
BundleDirectory string
ManifestPath string
SecretKey string
}
type StepUploadBundle struct{}
func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
manifestName := state["manifest_name"].(string)
manifestPath := state["manifest_path"].(string)
ui := state["ui"].(packer.Ui)
var uploadCmd bytes.Buffer
tData := uploadCmdData{
AccessKey: config.AccessKey,
BucketName: config.S3Bucket,
BundleDirectory: config.BundleDestination,
ManifestPath: manifestPath,
SecretKey: config.SecretKey,
}
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
t.Execute(&uploadCmd, tData)
ui.Say("Uploading the bundle...")
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Bundle upload failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["remote_manifest_path"] = fmt.Sprintf(
"%s/%s", config.S3Bucket, manifestName)
return multistep.ActionContinue
}
func (s *StepUploadBundle) Cleanup(state map[string]interface{}) {}
@@ -0,0 +1,49 @@
package instance
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
type StepUploadX509Cert struct{}
func (s *StepUploadX509Cert) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
ui := state["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["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if err := s.uploadSingle(comm, x509RemoteKeyPath, config.X509KeyPath); err != nil {
state["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["x509RemoteCertPath"] = x509RemoteCertPath
state["x509RemoteKeyPath"] = x509RemoteKeyPath
return multistep.ActionContinue
}
func (s *StepUploadX509Cert) Cleanup(map[string]interface{}) {}
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)
}
+119
View File
@@ -0,0 +1,119 @@
package common
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/packer"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
)
// CheckUnusedConfig is a helper that makes sure that the there are no
// unused configuration keys, properly ignoring keys that don't matter.
func CheckUnusedConfig(md *mapstructure.Metadata) *packer.MultiError {
errs := make([]error, 0)
if md.Unused != nil && len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
return nil
}
// DecodeConfig is a helper that handles decoding raw configuration using
// mapstructure. It returns the metadata and any errors that may happen.
// If you need extra configuration for mapstructure, you should configure
// it manually and not use this helper function.
func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metadata, error) {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: target,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
if err != nil {
return nil, err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return nil, err
}
}
return &md, nil
}
// DownloadableURL processes a URL that may also be a file path and returns
// a completely valid URL. For example, the original URL might be "local/file.iso"
// which isn't a valid URL. DownloadableURL will return "file:///local/file.iso"
func DownloadableURL(original string) (string, error) {
url, err := url.Parse(original)
if err != nil {
return "", err
}
if url.Scheme == "" {
url.Scheme = "file"
}
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
return "", err
}
url.Path, err = filepath.Abs(url.Path)
if err != nil {
return "", err
}
url.Path, err = filepath.EvalSymlinks(url.Path)
if err != nil {
return "", err
}
url.Path = filepath.Clean(url.Path)
}
// Make sure it is lowercased
url.Scheme = strings.ToLower(url.Scheme)
// This is to work around issue #5927. This can safely be removed once
// we distribute with a version of Go that fixes that bug.
//
// See: https://code.google.com/p/go/issues/detail?id=5927
if url.Path != "" && url.Path[0] != '/' {
url.Path = "/" + url.Path
}
// Verify that the scheme is something we support in our common downloader.
supported := []string{"file", "http", "https"}
found := false
for _, s := range supported {
if url.Scheme == s {
found = true
break
}
}
if !found {
return "", fmt.Errorf("Unsupported URL scheme: %s", url.Scheme)
}
return url.String(), nil
}
+161
View File
@@ -0,0 +1,161 @@
package common
import (
"fmt"
"github.com/mitchellh/mapstructure"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestCheckUnusedConfig(t *testing.T) {
md := &mapstructure.Metadata{
Unused: make([]string, 0),
}
err := CheckUnusedConfig(md)
if err != nil {
t.Fatalf("err: %s", err)
}
md.Unused = []string{"foo", "bar"}
err = CheckUnusedConfig(md)
if err == nil {
t.Fatal("should have error")
}
}
func TestDecodeConfig(t *testing.T) {
type Local struct {
Foo string
Bar string
}
raws := []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"bar": "baz",
"baz": "what",
},
}
var result Local
md, err := DecodeConfig(&result, raws...)
if err != nil {
t.Fatalf("err: %s", err)
}
if result.Foo != "bar" {
t.Fatalf("invalid: %#v", result.Foo)
}
if result.Bar != "baz" {
t.Fatalf("invalid: %#v", result.Bar)
}
if md == nil {
t.Fatal("metadata should not be nil")
}
if !reflect.DeepEqual(md.Unused, []string{"baz"}) {
t.Fatalf("unused: %#v", md.Unused)
}
}
func TestDownloadableURL(t *testing.T) {
// Invalid URL: has hex code in host
_, err := DownloadableURL("http://what%20.com")
if err == nil {
t.Fatal("expected err")
}
// Invalid: unsupported scheme
_, err = DownloadableURL("ftp://host.com/path")
if err == nil {
t.Fatal("expected err")
}
// Valid: http
u, err := DownloadableURL("HTTP://packer.io/path")
if err != nil {
t.Fatalf("err: %s", err)
}
if u != "http://packer.io/path" {
t.Fatalf("bad: %s", u)
}
// No path
u, err = DownloadableURL("HTTP://packer.io")
if err != nil {
t.Fatalf("err: %s", err)
}
if u != "http://packer.io" {
t.Fatalf("bad: %s", u)
}
}
func TestDownloadableURL_FilePaths(t *testing.T) {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("tempfile err: %s", err)
}
defer os.Remove(tf.Name())
tf.Close()
tfPath, err := filepath.EvalSymlinks(tf.Name())
if err != nil {
t.Fatalf("tempfile err: %s", err)
}
tfPath = filepath.Clean(tfPath)
// Relative filepath. We run this test in a func so that
// the defers run right away.
func() {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd err: %s", err)
}
err = os.Chdir(filepath.Dir(tfPath))
if err != nil {
t.Fatalf("chdir err: %s", err)
}
defer os.Chdir(wd)
filename := filepath.Base(tfPath)
u, err := DownloadableURL(filename)
if err != nil {
t.Fatalf("err: %s", err)
}
if u != fmt.Sprintf("file://%s", tfPath) {
t.Fatalf("unexpected: %s", u)
}
}()
// Test some cases with and without a schema prefix
for _, prefix := range []string{"", "file://"} {
// Nonexistent file
_, err = DownloadableURL(prefix + "i/dont/exist")
if err == nil {
t.Fatal("expected err")
}
// Good file
u, err := DownloadableURL(prefix + tfPath)
if err != nil {
t.Fatalf("err: %s", err)
}
if u != fmt.Sprintf("file://%s", tfPath) {
t.Fatalf("unexpected: %s", u)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
package common
// PackerConfig is a struct that contains the configuration keys that
// are sent by packer, properly tagged already so mapstructure can load
// them. Embed this structure into your configuration class to get it.
type PackerConfig struct {
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
}
+10 -9
View File
@@ -34,8 +34,7 @@ type StepConnectSSH struct {
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
cancel bool
comm packer.Communicator
comm packer.Communicator
}
func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
@@ -44,10 +43,11 @@ func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction
var comm packer.Communicator
var err error
cancel := make(chan struct{})
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for SSH to become available...")
comm, err = s.waitForSSH(state)
comm, err = s.waitForSSH(state, cancel)
waitDone <- true
}()
@@ -70,13 +70,13 @@ WaitLoop:
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
close(cancel)
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
// The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process.
s.cancel = true
close(cancel)
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
@@ -89,16 +89,16 @@ WaitLoop:
func (s *StepConnectSSH) Cleanup(map[string]interface{}) {
}
func (s *StepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan struct{}) (packer.Communicator, error) {
handshakeAttempts := 0
var comm packer.Communicator
for {
time.Sleep(5 * time.Second)
if s.cancel {
select {
case <-cancel:
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
case <-time.After(5 * time.Second):
}
// First we request the TCP connection information
@@ -130,6 +130,7 @@ func (s *StepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Commun
SSHConfig: sshConfig,
}
log.Println("Attempting SSH connection...")
comm, err = ssh.New(config)
if err != nil {
log.Printf("SSH handshake err: %s", err)
+18 -39
View File
@@ -7,15 +7,12 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"sort"
"strconv"
"strings"
"text/template"
"time"
)
@@ -31,6 +28,8 @@ type snapshotNameData struct {
// 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"`
@@ -41,8 +40,6 @@ type config struct {
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"`
@@ -61,37 +58,13 @@ type Builder struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
// Optional configuration with defaults
if b.config.APIKey == "" {
@@ -154,28 +127,33 @@ func (b *Builder) Prepare(raws ...interface{}) error {
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = append(errs, errors.New("a client_id must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id must be specified"))
}
if b.config.APIKey == "" {
errs = append(errs, errors.New("an api_key must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing event_delay: %s", err))
}
b.config.eventDelay = eventDelay
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
@@ -186,14 +164,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
} else {
t.Execute(snapNameBuf, tData)
b.config.SnapshotName = snapNameBuf.String()
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
+35 -112
View File
@@ -3,16 +3,13 @@ package virtualbox
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"net/url"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
@@ -26,6 +23,8 @@ type Builder struct {
}
type config struct {
common.PackerConfig `mapstructure:",squash"`
BootCommand []string `mapstructure:"boot_command"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
@@ -51,10 +50,6 @@ type config struct {
VBoxManage [][]string `mapstructure:"vboxmanage"`
VMName string `mapstructure:"vm_name"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
@@ -65,37 +60,13 @@ type config struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
@@ -154,62 +125,37 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOChecksum == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_checksum is required"))
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = append(errs, errors.New("The iso_checksum_type must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
}
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
scheme := strings.ToLower(url.Scheme)
found := false
for _, supported := range supportedSchemes {
if scheme == supported {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url: %s", err))
}
}
@@ -218,53 +164,25 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if b.config.GuestAdditionsURL != "" {
url, err := url.Parse(b.config.GuestAdditionsURL)
b.config.GuestAdditionsURL, err = common.DownloadableURL(b.config.GuestAdditionsURL)
if err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
}
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
scheme := strings.ToLower(url.Scheme)
found := false
for _, supported := range supportedSchemes {
if scheme == supported {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
}
}
}
if len(errs) == 0 {
// Put the URL back together since we may have modified it
b.config.GuestAdditionsURL = url.String()
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url: %s", err))
}
}
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
errors.New("Output directory already exists. It must not exist."))
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
if b.config.RawShutdownTimeout == "" {
@@ -277,29 +195,34 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
b.driver, err = b.newDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
-56
View File
@@ -209,39 +209,11 @@ func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) {
t.Fatalf("should be empty: %s", b.config.GuestAdditionsURL)
}
config["guest_additions_url"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["guest_additions_url"] = "file:i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["guest_additions_url"] = "http://www.packer.io"
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["guest_additions_url"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.GuestAdditionsURL != "file://"+tf.Name() {
t.Fatalf("guest_additions_url should be modified: %s", b.config.GuestAdditionsURL)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
@@ -347,39 +319,11 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
t.Fatal("should have error")
}
config["iso_url"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "file:i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "http://www.packer.io"
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["iso_url"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOUrl != "file://"+tf.Name() {
t.Fatalf("iso_url should be modified: %s", b.config.ISOUrl)
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
+3
View File
@@ -50,6 +50,9 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
}
for _, line := range strings.Split(stdout.String(), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
if line == `VMState="running"` {
return true, nil
}
+4 -44
View File
@@ -6,11 +6,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
@@ -23,11 +19,9 @@ import (
//
// Produces:
// iso_path string
type stepDownloadISO struct {
isoCopyDir string
}
type stepDownloadISO struct{}
func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
@@ -84,44 +78,10 @@ DownloadWaitLoop:
}
}
// VirtualBox is really dumb and can't figure out that the file is an
// ISO unless it has a ".iso" extension. We can't modify the cache
// filenames so we just do a copy.
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
s.isoCopyDir = tempdir
f, err := os.Create(filepath.Join(tempdir, "image.iso"))
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer f.Close()
sourceF, err := os.Open(cachePath)
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer sourceF.Close()
log.Printf("Copying ISO to temp location: %s", tempdir)
if _, err := io.Copy(f, sourceF); err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = f.Name()
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (s *stepDownloadISO) Cleanup(map[string]interface{}) {
if s.isoCopyDir != "" {
os.RemoveAll(s.isoCopyDir)
}
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
+11 -1
View File
@@ -3,7 +3,9 @@ package virtualbox
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"time"
)
type stepPrepareOutputDir struct{}
@@ -34,6 +36,14 @@ func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting output directory...")
os.RemoveAll(config.OutputDir)
for i := 0; i < 5; i++ {
err := os.RemoveAll(config.OutputDir)
if err == nil {
break
}
log.Printf("Error removing output dir: %s", err)
time.Sleep(2 * time.Second)
}
}
}
+1 -4
View File
@@ -34,16 +34,13 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
if err := cmd.StartWithUi(comm, ui); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the command to run
cmd.Wait()
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
shutdownTimer := time.After(config.shutdownTimeout)
@@ -91,8 +91,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func scancodes(message string) []string {
special := make(map[string][]string)
special["<bs>"] = []string{"ff", "08"}
special["<del>"] = []string{"ff", "ff"}
special["<enter>"] = []string{"1c", "9c"}
special["<esc>"] = []string{"01", "81"}
special["<f1>"] = []string{"ff", "be"}
special["<f2>"] = []string{"ff", "bf"}
special["<f3>"] = []string{"ff", "c0"}
special["<f4>"] = []string{"ff", "c1"}
special["<f5>"] = []string{"ff", "c2"}
special["<f6>"] = []string{"ff", "c3"}
special["<f7>"] = []string{"ff", "c4"}
special["<f8>"] = []string{"ff", "c5"}
special["<f9>"] = []string{"ff", "c6"}
special["<f10>"] = []string{"ff", "c7"}
special["<f11>"] = []string{"ff", "c8"}
special["<f12>"] = []string{"ff", "c9"}
special["<return>"] = []string{"1c", "9c"}
special["<tab>"] = []string{"0f", "8f"}
+35 -92
View File
@@ -3,16 +3,13 @@ package vmware
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
@@ -27,6 +24,8 @@ type Builder struct {
}
type config struct {
common.PackerConfig `mapstructure:",squash"`
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
@@ -52,10 +51,6 @@ type config struct {
VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
@@ -66,37 +61,13 @@ type config struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if b.config.DiskName == "" {
b.config.DiskName = "disk"
@@ -151,81 +122,58 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOChecksum == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_checksum is required"))
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = append(errs, errors.New("The iso_checksum_type must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
}
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
scheme := strings.ToLower(url.Scheme)
found := false
for _, supported := range supportedSchemes {
if scheme == supported {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url: %s", err))
}
}
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
errors.New("Output directory already exists. It must not exist."))
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
if b.config.RawBootWait != "" {
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
}
@@ -235,7 +183,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.RawSSHWaitTimeout == "" {
@@ -244,24 +193,28 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
if _, err := template.New("path").Parse(b.config.ToolsUploadPath); err != nil {
errs = append(errs, fmt.Errorf("tools_upload_path invalid: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("tools_upload_path invalid: %s", err))
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
b.driver, err = b.newDriver()
b.driver, err = NewDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VMware driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VMware driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -358,13 +311,3 @@ func (b *Builder) Cancel() {
b.runner.Cancel()
}
}
func (b *Builder) newDriver() (Driver, error) {
fusionAppPath := "/Applications/VMware Fusion.app"
driver := &Fusion5Driver{fusionAppPath}
if err := driver.Verify(); err != nil {
return nil, err
}
return driver, nil
}
-28
View File
@@ -238,39 +238,11 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
t.Fatal("should have error")
}
config["iso_url"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "file:i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "http://www.packer.io"
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["iso_url"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOUrl != "file://"+tf.Name() {
t.Fatalf("iso_url should be modified: %s", b.config.ISOUrl)
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
+49 -108
View File
@@ -4,9 +4,8 @@ import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
@@ -30,6 +29,9 @@ type Driver interface {
// Get the path to the VMware ISO for the given flavor.
ToolsIsoPath(string) string
// Get the path to the DHCP leases file for the given device.
DhcpLeasesPath(string) string
// Verify checks to make sure that this driver should function
// properly. This should check that all the files it will use
// appear to exist and so on. If everything is okay, this doesn't
@@ -37,120 +39,47 @@ type Driver interface {
Verify() error
}
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
// NewDriver returns a new driver implementation for this operating
// system, or an error if the driver couldn't be initialized.
func NewDriver() (Driver, error) {
drivers := []Driver{}
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
switch runtime.GOOS {
case "darwin":
drivers = []Driver{
&Fusion5Driver{
AppPath: "/Applications/VMware Fusion.app",
},
}
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
case "linux":
drivers = []Driver{
new(Workstation9Driver),
new(Player5LinuxDriver),
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
case "windows":
drivers = []Driver{
new(Workstation9Driver),
}
return err
default:
return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS)
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
errs := ""
for _, driver := range drivers {
err := driver.Verify()
if err == nil {
return driver, nil
}
return err
errs += "* " + err.Error() + "\n"
}
return nil
return nil, fmt.Errorf(
"Unable to initialize any driver for this platform. The errors\n"+
"from each driver are shown below. Please fix at least one driver\n"+
"to continue:\n%s", errs)
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
func runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
@@ -158,8 +87,20 @@ func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
return stdout.String(), stderr.String(), err
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("VMware error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
// Replace these for Windows, we only want to deal with Unix
// style line endings.
returnStdout := strings.Replace(stdout.String(), "\r\n", "\n", -1)
returnStderr := strings.Replace(stderr.String(), "\r\n", "\n", -1)
return returnStdout, returnStderr, err
}
+126
View File
@@ -0,0 +1,126 @@
package vmware
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
}
return err
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
}
return err
}
return nil
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) DhcpLeasesPath(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
+174
View File
@@ -0,0 +1,174 @@
package vmware
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Player5LinuxDriver is a driver that can run VMware Player 5 on Linux.
type Player5LinuxDriver struct {
AppPath string
VdiskManagerPath string
QemuImgPath string
VmrunPath string
}
func (d *Player5LinuxDriver) CompactDisk(diskPath string) error {
if d.QemuImgPath != "" {
return d.qemuCompactDisk(diskPath)
}
defragCmd := exec.Command(d.VdiskManagerPath, "-d", diskPath)
if _, _, err := runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.VdiskManagerPath, "-k", diskPath)
if _, _, err := runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Player5LinuxDriver) qemuCompactDisk(diskPath string) error {
cmd := exec.Command(d.QemuImgPath, "convert", "-f", "vmdk", "-O", "vmdk", "-o", "compat6", diskPath, diskPath+".new")
if _, _, err := runAndLog(cmd); err != nil {
return err
}
if err := os.Remove(diskPath); err != nil {
return err
}
if err := os.Rename(diskPath+".new", diskPath); err != nil {
return err
}
return nil
}
func (d *Player5LinuxDriver) CreateDisk(output string, size string) error {
var cmd *exec.Cmd
if d.QemuImgPath != "" {
cmd = exec.Command(d.QemuImgPath, "create", "-f", "vmdk", "-o", "compat6", output, size)
} else {
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
}
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Player5LinuxDriver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.VmrunPath, "-T", "player", "list")
stdout, _, err := runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Player5LinuxDriver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless {
guiArgument = "nogui"
}
cmd := exec.Command(d.VmrunPath, "-T", "player", "start", vmxPath, guiArgument)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Player5LinuxDriver) Stop(vmxPath string) error {
cmd := exec.Command(d.VmrunPath, "-T", "player", "stop", vmxPath, "hard")
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Player5LinuxDriver) Verify() error {
if err := d.findApp(); err != nil {
return fmt.Errorf("VMware Player application ('vmplayer') not found in path.")
}
if err := d.findVmrun(); err != nil {
return fmt.Errorf("Critical application 'vmrun' not found in path.")
}
if err := d.findVdiskManager(); err != nil {
if err := d.findQemuImg(); err != nil {
return fmt.Errorf(
"Neither 'vmware-vdiskmanager', nor 'qemu-img' found in path.\n" +
"One of these is required to configure disks for VMware Player.")
}
}
return nil
}
func (d *Player5LinuxDriver) findApp() error {
path, err := exec.LookPath("vmplayer")
if err != nil {
return err
}
d.AppPath = path
return nil
}
func (d *Player5LinuxDriver) findVdiskManager() error {
path, err := exec.LookPath("vmware-vdiskmanager")
if err != nil {
return err
}
d.VdiskManagerPath = path
return nil
}
func (d *Player5LinuxDriver) findQemuImg() error {
path, err := exec.LookPath("qemu-img")
if err != nil {
return err
}
d.QemuImgPath = path
return nil
}
func (d *Player5LinuxDriver) findVmrun() error {
path, err := exec.LookPath("vmrun")
if err != nil {
return err
}
d.VmrunPath = path
return nil
}
func (d *Player5LinuxDriver) ToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func (d *Player5LinuxDriver) DhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
}
+137
View File
@@ -0,0 +1,137 @@
package vmware
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Workstation9Driver is a driver that can run VMware Workstation 9
// on non-Windows platforms.
type Workstation9Driver struct {
AppPath string
VdiskManagerPath string
VmrunPath string
}
func (d *Workstation9Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.VdiskManagerPath, "-d", diskPath)
if _, _, err := runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.VdiskManagerPath, "-k", diskPath)
if _, _, err := runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Workstation9Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "list")
stdout, _, err := runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Workstation9Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless {
guiArgument = "nogui"
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "start", vmxPath, guiArgument)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.VmrunPath, "-T", "ws", "stop", vmxPath, "hard")
if _, _, err := runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9Driver) Verify() error {
var err error
if d.AppPath == "" {
if d.AppPath, err = workstationFindVMware(); err != nil {
return err
}
}
if d.VmrunPath == "" {
if d.VmrunPath, err = workstationFindVmrun(); err != nil {
return err
}
}
if d.VdiskManagerPath == "" {
if d.VdiskManagerPath, err = workstationFindVdiskManager(); err != nil {
return err
}
}
log.Printf("VMware app path: %s", d.AppPath)
log.Printf("vmrun path: %s", d.VmrunPath)
log.Printf("vdisk-manager path: %s", d.VdiskManagerPath)
if _, err := os.Stat(d.AppPath); err != nil {
return fmt.Errorf("VMware application not found: %s", d.AppPath)
}
if _, err := os.Stat(d.VmrunPath); err != nil {
return fmt.Errorf("'vmrun' application not found: %s", d.VmrunPath)
}
if _, err := os.Stat(d.VdiskManagerPath); err != nil {
return fmt.Errorf("'vmrun' application not found: %s", d.VdiskManagerPath)
}
// Check to see if it APPEARS to be licensed.
if err := workstationCheckLicense(); err != nil {
return err
}
return nil
}
func (d *Workstation9Driver) ToolsIsoPath(flavor string) string {
return workstationToolsIsoPath(flavor)
}
func (d *Workstation9Driver) DhcpLeasesPath(device string) string {
return workstationDhcpLeasesPath(device)
}
@@ -0,0 +1,43 @@
// +build !windows
package vmware
import (
"errors"
"fmt"
"os/exec"
"path/filepath"
)
func workstationCheckLicense() error {
matches, err := filepath.Glob("/etc/vmware/license-*")
if err != nil {
return fmt.Errorf("Error looking for VMware license: %s", err)
}
if len(matches) == 0 {
return errors.New("Workstation does not appear to be licensed. Please license it.")
}
return nil
}
func workstationFindVdiskManager() (string, error) {
return exec.LookPath("vmware-vdiskmanager")
}
func workstationFindVMware() (string, error) {
return exec.LookPath("vmware")
}
func workstationFindVmrun() (string, error) {
return exec.LookPath("vmrun")
}
func workstationDhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
}
func workstationToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
@@ -0,0 +1,131 @@
// +build windows
package vmware
import (
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unsafe"
)
func workstationCheckLicense() error {
// Not implemented on Windows
return nil
}
func workstationFindVdiskManager() (string, error) {
path, err := exec.LookPath("vmware-vdiskmanager.exe")
if err == nil {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmware-vdiskmanager.exe"), nil
}
func workstationFindVMware() (string, error) {
path, err := exec.LookPath("vmware.exe")
if err == nil {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmware.exe"), nil
}
func workstationFindVmrun() (string, error) {
path, err := exec.LookPath("vmrun.exe")
if err == nil {
return path, nil
}
path, err = workstationVMwareRoot()
if err != nil {
return "", err
}
return filepath.Join(path, "vmrun.exe"), nil
}
func workstationToolsIsoPath(flavor string) string {
path, err := workstationVMwareRoot()
if err != nil {
return ""
}
return filepath.Join(path, flavor+".iso")
}
func workstationDhcpLeasesPath(device string) string {
programData := os.Getenv("ProgramData")
if programData == "" {
return ""
}
return filepath.Join(programData, "/VMware/vmnetdhcp.leases")
}
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
//
// This is used by workstationVMwareRoot in order to read some registry data.
func readRegString(hive syscall.Handle, subKeyPath, valueName string) (value string, err error) {
var h syscall.Handle
err = syscall.RegOpenKeyEx(hive, syscall.StringToUTF16Ptr(subKeyPath), 0, syscall.KEY_READ, &h)
if err != nil {
return
}
defer syscall.RegCloseKey(h)
var typ uint32
var bufSize uint32
err = syscall.RegQueryValueEx(
h,
syscall.StringToUTF16Ptr(valueName),
nil,
&typ,
nil,
&bufSize)
if err != nil {
return
}
data := make([]uint16, bufSize/2+1)
err = syscall.RegQueryValueEx(
h,
syscall.StringToUTF16Ptr(valueName),
nil,
&typ,
(*byte)(unsafe.Pointer(&data[0])),
&bufSize)
if err != nil {
return
}
return syscall.UTF16ToString(data), nil
}
// This reads the VMware installation path from the Windows registry.
func workstationVMwareRoot() (s string, err error) {
key := `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\vmware.exe`
subkey := "Path"
s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey)
if err != nil {
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
return
}
s = strings.Replace(s, "\\", "/", -1)
return
}
+14 -2
View File
@@ -2,8 +2,8 @@ package vmware
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
@@ -18,6 +18,9 @@ type GuestIPFinder interface {
// DHCPLeaseGuestLookup looks up the IP address of a guest using DHCP
// lease information from the VMware network devices.
type DHCPLeaseGuestLookup struct {
// Driver that is being used (to find leases path)
Driver Driver
// Device that the guest is connected to.
Device string
@@ -26,7 +29,13 @@ type DHCPLeaseGuestLookup struct {
}
func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
fh, err := os.Open(fmt.Sprintf("/var/db/vmware/vmnet-dhcpd-%s.leases", f.Device))
dhcpLeasesPath := f.Driver.DhcpLeasesPath(f.Device)
log.Printf("DHCP leases path: %s", dhcpLeasesPath)
if dhcpLeasesPath == "" {
return "", errors.New("no DHCP leases path found.")
}
fh, err := os.Open(dhcpLeasesPath)
if err != nil {
return "", err
}
@@ -48,6 +57,9 @@ func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
macLineRe := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`)
for _, line := range strings.Split(string(dhcpBytes), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
matches := ipLineRe.FindStringSubmatch(line)
if matches != nil {
lastIp = matches[1]
-36
View File
@@ -1,43 +1,7 @@
package vmware
import (
"bytes"
"errors"
"os/exec"
"regexp"
)
// Interface to help find the host IP that is available from within
// the VMware virtual machines.
type HostIPFinder interface {
HostIP() (string, error)
}
// IfconfigIPFinder finds the host IP based on the output of `ifconfig`.
type IfconfigIPFinder struct {
Device string
}
func (f *IfconfigIPFinder) HostIP() (string, error) {
ifconfigPath, err := exec.LookPath("ifconfig")
if err != nil {
return "", err
}
stdout := new(bytes.Buffer)
cmd := exec.Command(ifconfigPath, f.Device)
cmd.Stdout = stdout
cmd.Stderr = new(bytes.Buffer)
if err := cmd.Run(); err != nil {
return "", err
}
re := regexp.MustCompile(`inet\s*(.+?)\s`)
matches := re.FindStringSubmatch(stdout.String())
if matches == nil {
return "", errors.New("IP not found in ifconfig output...")
}
return matches[1], nil
}
+37
View File
@@ -0,0 +1,37 @@
package vmware
import (
"bytes"
"errors"
"os/exec"
"regexp"
)
// IfconfigIPFinder finds the host IP based on the output of `ifconfig`.
type IfconfigIPFinder struct {
Device string
}
func (f *IfconfigIPFinder) HostIP() (string, error) {
ifconfigPath, err := exec.LookPath("ifconfig")
if err != nil {
return "", err
}
stdout := new(bytes.Buffer)
cmd := exec.Command(ifconfigPath, f.Device)
cmd.Stdout = stdout
cmd.Stderr = new(bytes.Buffer)
if err := cmd.Run(); err != nil {
return "", err
}
re := regexp.MustCompile(`inet\s*(?:addr:)?(.+?)\s`)
matches := re.FindStringSubmatch(stdout.String())
if matches == nil {
return "", errors.New("IP not found in ifconfig output...")
}
return matches[1], nil
}
+66
View File
@@ -0,0 +1,66 @@
package vmware
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
// VMnetNatConfIPFinder finds the IP address of the host machine by
// retrieving the IP from the vmnetnat.conf. This isn't a full proof
// technique but so far it has not failed.
type VMnetNatConfIPFinder struct{}
func (*VMnetNatConfIPFinder) HostIP() (string, error) {
programData := os.Getenv("ProgramData")
if programData == "" {
return "", errors.New("ProgramData directory not found.")
}
programData = strings.Replace(programData, "\\", "/", -1)
vmnetnat := filepath.Join(programData, "/VMware/vmnetnat.conf")
if _, err := os.Stat(vmnetnat); err != nil {
return "", fmt.Errorf("Error with vmnetnat.conf: %s", err)
}
f, err := os.Open(vmnetnat)
if err != nil {
return "", err
}
defer f.Close()
ipRe := regexp.MustCompile(`^\s*ip\s*=\s*(.+?)\s*$`)
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if line != "" {
matches := ipRe.FindStringSubmatch(line)
if matches != nil {
ip := matches[1]
dotIndex := strings.LastIndex(ip, ".")
if dotIndex == -1 {
continue
}
ip = ip[0:dotIndex] + ".1"
return ip, nil
}
}
if err == io.EOF {
break
}
if err != nil {
return "", err
}
}
return "", errors.New("host IP not found in NAT config")
}
@@ -0,0 +1,11 @@
package vmware
import "testing"
func TestVMnetNatConfIPFinder_Impl(t *testing.T) {
var raw interface{}
raw = &VMnetNatConfIPFinder{}
if _, ok := raw.(HostIPFinder); !ok {
t.Fatalf("VMnetNatConfIPFinder is not a host IP finder")
}
}
+2
View File
@@ -12,6 +12,7 @@ import (
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(*config)
driver := state["driver"].(Driver)
vmxPath := state["vmx_path"].(string)
log.Println("Lookup up IP information...")
@@ -37,6 +38,7 @@ func sshAddress(state map[string]interface{}) (string, error) {
}
ipLookup := &DHCPLeaseGuestLookup{
Driver: driver,
Device: "vmnet8",
MACAddress: macAddress,
}
+30 -10
View File
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"log"
"os"
"regexp"
"strings"
)
@@ -22,28 +23,47 @@ import (
type stepCleanVMX struct{}
func (s stepCleanVMX) Run(state map[string]interface{}) multistep.StepAction {
if _, ok := state["floppy_path"]; !ok {
return multistep.ActionContinue
}
isoPath := state["iso_path"].(string)
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
ui.Say("Cleaning VMX prior to finishing up...")
vmxData, err := s.readVMX(vmxPath)
if err != nil {
state["error"] = fmt.Errorf("Error reading VMX: %s", err)
return multistep.ActionHalt
}
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Say("Unmounting floppy from VMX...")
if _, ok := state["floppy_path"]; ok {
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Message("Unmounting floppy from VMX...")
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
}
}
vmxData["floppy0.present"] = "FALSE"
}
ui.Message("Detatching ISO from CD-ROM device...")
devRe := regexp.MustCompile(`^ide\d:\d\.`)
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
match := devRe.FindString(k)
if match == "" {
continue
}
filenameKey := match + ".filename"
if filename, ok := vmxData[filenameKey]; ok {
if filename == isoPath {
// Change the CD-ROM device back to auto-detect to eject
vmxData[filenameKey] = "auto detect"
vmxData[match+".deviceType"] = "cdrom-raw"
}
}
}
vmxData["floppy0.present"] = "FALSE"
// Rewrite the VMX
if err := WriteVMX(vmxPath, vmxData); err != nil {
+11 -1
View File
@@ -3,7 +3,9 @@ package vmware
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"time"
)
type stepPrepareOutputDir struct{}
@@ -34,6 +36,14 @@ func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting output directory...")
os.RemoveAll(config.OutputDir)
for i := 0; i < 5; i++ {
err := os.RemoveAll(config.OutputDir)
if err == nil {
break
}
log.Printf("Error removing output dir: %s", err)
time.Sleep(2 * time.Second)
}
}
}
+8
View File
@@ -8,6 +8,7 @@ import (
"github.com/mitchellh/packer/packer"
"log"
"path/filepath"
"runtime"
"strings"
"time"
)
@@ -120,6 +121,13 @@ LockWaitLoop:
}
}
if runtime.GOOS == "windows" {
// Windows takes a while to yield control of the files when the
// process is exiting. We just sleep here. In the future, it'd be
// nice to find a better solution to this.
time.Sleep(5 * time.Second)
}
log.Println("VM shut down.")
return multistep.ActionContinue
}
+24 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/mitchellh/packer/packer"
"log"
"net"
"runtime"
"strings"
"text/template"
"time"
@@ -64,7 +65,13 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
// Determine the host IP
ipFinder := &IfconfigIPFinder{"vmnet8"}
var ipFinder HostIPFinder
if runtime.GOOS == "windows" {
ipFinder = new(VMnetNatConfIPFinder)
} else {
ipFinder = &IfconfigIPFinder{Device: "vmnet8"}
}
hostIp, err := ipFinder.HostIP()
if err != nil {
err := fmt.Errorf("Error detecting host IP: %s", err)
@@ -73,6 +80,8 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
return multistep.ActionHalt
}
log.Printf("Host IP for the VMware machine: %s", hostIp)
tplData := &bootCommandTemplateData{
hostIp,
httpPort,
@@ -95,8 +104,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func vncSendString(c *vnc.ClientConn, original string) {
special := make(map[string]uint32)
special["<bs>"] = 0xFF08
special["<del>"] = 0xFFFF
special["<enter>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<f1>"] = 0xFFBE
special["<f2>"] = 0xFFBF
special["<f3>"] = 0xFFC0
special["<f4>"] = 0xFFC1
special["<f5>"] = 0xFFC2
special["<f6>"] = 0xFFC3
special["<f7>"] = 0xFFC4
special["<f8>"] = 0xFFC5
special["<f9>"] = 0xFFC6
special["<f10>"] = 0xFFC7
special["<f11>"] = 0xFFC8
special["<f12>"] = 0xFFC9
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
+5
View File
@@ -250,6 +250,11 @@ func (c Command) Run(env packer.Environment, args []string) int {
env.Ui().Say("\n==> Builds finished but no artifacts were created.")
}
if len(errors) > 0 {
// If any errors occurred, exit with a non-zero exit status
return 1
}
return 0
}
+14
View File
@@ -0,0 +1,14 @@
package validate
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}
+52 -13
View File
@@ -1,6 +1,7 @@
package ssh
import (
"bufio"
"bytes"
"code.google.com/p/go.crypto/ssh"
"errors"
@@ -78,15 +79,16 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
go func() {
defer session.Close()
err := session.Wait()
cmd.ExitStatus = 0
exitStatus := 0
if err != nil {
exitErr, ok := err.(*ssh.ExitError)
if ok {
cmd.ExitStatus = exitErr.ExitStatus()
exitStatus = exitErr.ExitStatus()
}
}
cmd.Exited = true
log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command)
cmd.SetExited(exitStatus)
}()
return
@@ -106,12 +108,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
// Set stderr/stdout to a bytes buffer
stderr := new(bytes.Buffer)
stdout := new(bytes.Buffer)
session.Stderr = stderr
session.Stdout = stdout
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
@@ -120,10 +116,26 @@ func (c *comm) Upload(path string, input io.Reader) error {
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash seperators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
log.Println("Starting remote scp process in sink mode")
@@ -143,11 +155,17 @@ func (c *comm) Upload(path string, input io.Reader) error {
// Start the protocol
log.Println("Beginning file upload...")
fmt.Fprintln(w, "C0644", input_memory.Len(), target_file)
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
io.Copy(w, input_memory)
fmt.Fprint(w, "\x00")
// TODO(mitchellh): Each step above results in a 0/1/2 being sent by
// the remote side to confirm. We should check for those confirmations.
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
@@ -178,7 +196,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
log.Printf("scp stdout (length %d): %#v", stdout.Len(), stdout.Bytes())
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
@@ -223,3 +240,25 @@ func (c *comm) reconnect() (err error) {
return
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("Error reading error message: %s", err)
}
return errors.New(string(message))
}
return nil
}
+4 -1
View File
@@ -20,6 +20,8 @@ const defaultConfig = `
"builders": {
"amazon-ebs": "packer-builder-amazon-ebs",
"amazon-chroot": "packer-builder-amazon-chroot",
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
@@ -37,7 +39,8 @@ const defaultConfig = `
"provisioners": {
"file": "packer-provisioner-file",
"shell": "packer-provisioner-shell"
"shell": "packer-provisioner-shell",
"salt-masterless": "packer-provisioner-salt-masterless"
}
}
`
+3
View File
@@ -46,6 +46,9 @@ func main() {
packer.Version, packer.VersionPrerelease, packer.GitCommit)
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
// Prepare stdin for plugin usage by switching it to a pipe
setupStdin()
config, err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
+21 -2
View File
@@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"sync"
)
@@ -14,6 +15,9 @@ type Cache interface {
// Packer guarantees that no other process will write to this file while
// the lock is held.
//
// If the key has an extension (e.g., file.ext), the resulting path
// will have that extension as well.
//
// The cache will block and wait for the lock.
Lock(string) string
@@ -44,7 +48,7 @@ func (f *FileCache) Lock(key string) string {
rw := f.rwLock(hashKey)
rw.Lock()
return filepath.Join(f.CacheDir, hashKey)
return f.cachePath(key, hashKey)
}
func (f *FileCache) Unlock(key string) {
@@ -58,7 +62,7 @@ func (f *FileCache) RLock(key string) (string, bool) {
rw := f.rwLock(hashKey)
rw.RLock()
return filepath.Join(f.CacheDir, hashKey), true
return f.cachePath(key, hashKey), true
}
func (f *FileCache) RUnlock(key string) {
@@ -67,6 +71,21 @@ func (f *FileCache) RUnlock(key string) {
rw.RUnlock()
}
func (f *FileCache) cachePath(key string, hashKey string) string {
suffix := ""
endIndex := strings.Index(key, "?")
if endIndex == -1 {
endIndex = len(key)
}
dotIndex := strings.LastIndex(key[0:endIndex], ".")
if dotIndex > -1 {
suffix = key[dotIndex:endIndex]
}
return filepath.Join(f.CacheDir, hashKey+suffix)
}
func (f *FileCache) hashKey(key string) string {
sha := sha256.New()
sha.Write([]byte(key))
+15 -4
View File
@@ -3,6 +3,7 @@ package packer
import (
"io/ioutil"
"os"
"strings"
"testing"
)
@@ -36,19 +37,29 @@ func TestFileCache(t *testing.T) {
defer os.RemoveAll(cacheDir)
cache := &FileCache{CacheDir: cacheDir}
path := cache.Lock("foo")
path := cache.Lock("foo.ext?foo=bar.foo")
defer cache.Unlock("foo.ext?foo=bar.foo")
if !strings.HasSuffix(path, ".ext") {
t.Fatalf("bad extension with question mark: %s", path)
}
path = cache.Lock("foo.iso")
if !strings.HasSuffix(path, ".iso") {
t.Fatalf("path doesn't end with suffix '%s': '%s'", ".iso", path)
}
err = ioutil.WriteFile(path, []byte("data"), 0666)
if err != nil {
t.Fatalf("error writing: %s", err)
}
cache.Unlock("foo")
cache.Unlock("foo.iso")
path, ok := cache.RLock("foo")
path, ok := cache.RLock("foo.iso")
if !ok {
t.Fatal("cache says key doesn't exist")
}
defer cache.RUnlock("foo")
defer cache.RUnlock("foo.iso")
data, err := ioutil.ReadFile(path)
if err != nil {
+96 -3
View File
@@ -1,8 +1,10 @@
package packer
import (
"github.com/mitchellh/iochan"
"io"
"time"
"strings"
"sync"
)
// RemoteCmd represents a remote command being prepared or run.
@@ -30,6 +32,10 @@ type RemoteCmd struct {
// Once Exited is true, this will contain the exit code of the process.
ExitStatus int
// Internal locks and such used for safely setting some shared variables
l sync.Mutex
exitCh chan struct{}
}
// A Communicator is the interface used to communicate with the machine
@@ -57,9 +63,96 @@ type Communicator interface {
Download(string, io.Writer) error
}
// StartWithUi runs the remote command and streams the output to any
// configured Writers for stdout/stderr, while also writing each line
// as it comes to a Ui.
func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
} else {
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
}
if r.Stderr == nil {
r.Stderr = stderr_w
} else {
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
}
// Start the command
if err := c.Start(r); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan int, 1)
stdoutCh := iochan.DelimReader(stdout_r, '\n')
stderrCh := iochan.DelimReader(stderr_r, '\n')
// Start the goroutine to watch for the exit
go func() {
defer stdout_w.Close()
defer stderr_w.Close()
r.Wait()
exitCh <- r.ExitStatus
}()
// Loop and get all our output
OutputLoop:
for {
select {
case output := <-stderrCh:
ui.Message(strings.TrimSpace(output))
case output := <-stdoutCh:
ui.Message(strings.TrimSpace(output))
case <-exitCh:
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel before finishing these first.
for output := range stdoutCh {
ui.Message(strings.TrimSpace(output))
}
for output := range stderrCh {
ui.Message(strings.TrimSpace(output))
}
return nil
}
// SetExited is a helper for setting that this process is exited. This
// should be called by communicators who are running a remote command in
// order to set that the command is done.
func (r *RemoteCmd) SetExited(status int) {
r.l.Lock()
defer r.l.Unlock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.Exited = true
r.ExitStatus = status
close(r.exitCh)
}
// Wait waits for the remote command to complete.
func (r *RemoteCmd) Wait() {
for !r.Exited {
time.Sleep(50 * time.Millisecond)
// Make sure our condition variable is initialized.
r.l.Lock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.l.Unlock()
<-r.exitCh
}
+72 -2
View File
@@ -1,10 +1,81 @@
package packer
import (
"bytes"
"io"
"strings"
"testing"
"time"
)
type TestCommunicator struct {
Stderr io.Reader
Stdout io.Reader
}
func (c *TestCommunicator) Start(rc *RemoteCmd) error {
go func() {
if rc.Stdout != nil && c.Stdout != nil {
io.Copy(rc.Stdout, c.Stdout)
}
if rc.Stderr != nil && c.Stderr != nil {
io.Copy(rc.Stderr, c.Stderr)
}
}()
return nil
}
func (c *TestCommunicator) Upload(string, io.Reader) error {
return nil
}
func (c *TestCommunicator) Download(string, io.Writer) error {
return nil
}
func TestRemoteCmd_StartWithUi(t *testing.T) {
data := "hello\nworld\nthere"
originalOutput := new(bytes.Buffer)
rcOutput := new(bytes.Buffer)
uiOutput := new(bytes.Buffer)
rcOutput.WriteString(data)
testComm := &TestCommunicator{
Stdout: rcOutput,
}
testUi := &ReaderWriterUi{
Reader: new(bytes.Buffer),
Writer: uiOutput,
}
rc := &RemoteCmd{
Command: "test",
Stdout: originalOutput,
}
go func() {
time.Sleep(100 * time.Millisecond)
rc.SetExited(0)
}()
err := rc.StartWithUi(testComm, testUi)
if err != nil {
t.Fatalf("err: %s", err)
}
if uiOutput.String() != strings.TrimSpace(data)+"\n" {
t.Fatalf("bad output: '%s'", uiOutput.String())
}
if originalOutput.String() != data {
t.Fatalf("original is bad: '%s'", originalOutput.String())
}
}
func TestRemoteCmd_Wait(t *testing.T) {
var cmd RemoteCmd
@@ -14,8 +85,7 @@ func TestRemoteCmd_Wait(t *testing.T) {
result <- true
}()
cmd.ExitStatus = 42
cmd.Exited = true
cmd.SetExited(42)
select {
case <-result:
+30
View File
@@ -22,3 +22,33 @@ func (e *MultiError) Error() string {
"%d error(s) occurred:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
// MultiErrorAppend is a helper function that will append more errors
// onto a MultiError in order to create a larger multi-error. If the
// original error is not a MultiError, it will be turned into one.
func MultiErrorAppend(err error, errs ...error) *MultiError {
if err == nil {
err = new(MultiError)
}
switch err := err.(type) {
case *MultiError:
if err == nil {
err = new(MultiError)
}
if err.Errors == nil {
err.Errors = make([]error, 0, len(errs))
}
err.Errors = append(err.Errors, errs...)
return err
default:
newErrs := make([]error, len(errs)+1)
newErrs[0] = err
copy(newErrs[1:], errs)
return &MultiError{
Errors: newErrs,
}
}
}
+25
View File
@@ -30,3 +30,28 @@ func TestMultiErrorError(t *testing.T) {
multi := &MultiError{errors}
assert.Equal(multi.Error(), expected, "should have proper error")
}
func TestMultiErrorAppend_MultiError(t *testing.T) {
original := &MultiError{
Errors: []error{errors.New("foo")},
}
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
original = &MultiError{}
result = MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 1 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}
func TestMultiErrorAppend_NonMultiError(t *testing.T) {
original := errors.New("foo")
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}
+41 -28
View File
@@ -1,12 +1,14 @@
package plugin
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"io"
"io/ioutil"
"log"
"net/rpc"
"os"
@@ -14,6 +16,7 @@ import (
"strings"
"sync"
"time"
"unicode"
)
// This is a slice of the "managed" clients which are cleaned up when
@@ -26,7 +29,7 @@ var managedClients = make([]*Client, 0, 5)
type Client struct {
config *ClientConfig
exited bool
doneLogging bool
doneLogging chan struct{}
l sync.Mutex
address string
}
@@ -53,6 +56,10 @@ type ClientConfig struct {
// StartTimeout is the timeout to wait for the plugin to say it
// has started successfully.
StartTimeout time.Duration
// If non-nil, then the stderr of the client will be written to here
// (as well as the log).
Stderr io.Writer
}
// This makes sure all the managed subprocesses are killed and properly
@@ -94,6 +101,10 @@ func NewClient(config *ClientConfig) (c *Client) {
config.StartTimeout = 1 * time.Minute
}
if config.Stderr == nil {
config.Stderr = ioutil.Discard
}
c = &Client{config: config}
if config.Managed {
managedClients = append(managedClients, c)
@@ -178,16 +189,7 @@ func (c *Client) Kill() {
cmd.Process.Kill()
// Wait for the client to finish logging so we have a complete log
done := make(chan bool)
go func() {
for !c.doneLogging {
time.Sleep(10 * time.Millisecond)
}
done <- true
}()
<-done
<-c.doneLogging
}
// Starts the underlying subprocess, communicating with it to negotiate
@@ -204,6 +206,8 @@ func (c *Client) Start() (address string, err error) {
return c.address, nil
}
c.doneLogging = make(chan struct{})
env := []string{
fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort),
@@ -211,12 +215,13 @@ func (c *Client) Start() (address string, err error) {
}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
stderr_r, stderr_w := io.Pipe()
cmd := c.config.Cmd
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
cmd.Stderr = stderr
cmd.Stdin = os.Stdin
cmd.Stderr = stderr_w
cmd.Stdout = stdout
log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args)
@@ -240,13 +245,23 @@ func (c *Client) Start() (address string, err error) {
// Start goroutine to wait for process to exit
go func() {
// Make sure we close the write end of our stderr listener so
// that the log goroutine ends properly.
defer stderr_w.Close()
// Wait for the command to end.
cmd.Wait()
// Log and make sure to flush the logs write away
log.Printf("%s: plugin process exited\n", cmd.Path)
os.Stderr.Sync()
// Mark that we exited
c.exited = true
}()
// Start goroutine that logs the stderr
go c.logStderr(stderr)
go c.logStderr(stderr_r)
// Some channels for the next step
timeout := time.After(c.config.StartTimeout)
@@ -287,26 +302,24 @@ func (c *Client) Start() (address string, err error) {
return
}
func (c *Client) logStderr(buf *bytes.Buffer) {
for done := false; !done; {
if c.Exited() {
done = true
func (c *Client) logStderr(r io.Reader) {
bufR := bufio.NewReader(r)
for {
line, err := bufR.ReadString('\n')
if line != "" {
c.config.Stderr.Write([]byte(line))
line = strings.TrimRightFunc(line, unicode.IsSpace)
log.Printf("%s: %s", c.config.Cmd.Path, line)
}
var err error
for err != io.EOF {
var line string
line, err = buf.ReadString('\n')
if line != "" {
log.Printf("%s: %s", c.config.Cmd.Path, line)
}
if err == io.EOF {
break
}
time.Sleep(10 * time.Millisecond)
}
// Flag that we've completed logging for others
c.doneLogging = true
close(c.doneLogging)
}
func (c *Client) rpcClient() (*rpc.Client, error) {
+77
View File
@@ -1,6 +1,10 @@
package plugin
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"time"
)
@@ -47,3 +51,76 @@ func TestClient_Start_Timeout(t *testing.T) {
t.Fatal("err should not be nil")
}
}
func TestClient_Stderr(t *testing.T) {
stderr := new(bytes.Buffer)
process := helperProcess("stderr")
c := NewClient(&ClientConfig{
Cmd: process,
Stderr: stderr,
})
defer c.Kill()
if _, err := c.Start(); err != nil {
t.Fatalf("err: %s", err)
}
for !c.Exited() {
time.Sleep(10 * time.Millisecond)
}
if !strings.Contains(stderr.String(), "HELLO\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
if !strings.Contains(stderr.String(), "WORLD\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
}
func TestClient_Stdin(t *testing.T) {
// Overwrite stdin for this test with a temporary file
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err = tf.WriteString("hello"); err != nil {
t.Fatalf("error: %s", err)
}
if err = tf.Sync(); err != nil {
t.Fatalf("error: %s", err)
}
if _, err = tf.Seek(0, 0); err != nil {
t.Fatalf("error: %s", err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()
os.Stdin = tf
process := helperProcess("stdin")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
_, err = c.Start()
if err != nil {
t.Fatalf("error: %s", err)
}
for {
if c.Exited() {
break
}
time.Sleep(50 * time.Millisecond)
}
if !process.ProcessState.Success() {
t.Fatal("process didn't exit cleanly")
}
}
+18
View File
@@ -2,6 +2,7 @@ package plugin
import (
"fmt"
"log"
"os"
"os/exec"
"testing"
@@ -65,6 +66,23 @@ func TestHelperProcess(*testing.T) {
ServeProvisioner(new(helperProvisioner))
case "start-timeout":
time.Sleep(1 * time.Minute)
os.Exit(1)
case "stderr":
fmt.Println(":1234")
log.Println("HELLO")
log.Println("WORLD")
case "stdin":
fmt.Println(":1234")
data := make([]byte, 5)
if _, err := os.Stdin.Read(data); err != nil {
log.Printf("stdin read error: %s", err)
os.Exit(100)
}
if string(data) == "hello" {
os.Exit(0)
}
os.Exit(1)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)

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