Compare commits
242 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc96bd21f | |||
| 66fac2e82f | |||
| 39c80262d4 | |||
| b5dfad2c38 | |||
| bc9a09e3fc | |||
| 21ae9a02d6 | |||
| 39aea8c5ee | |||
| 82bbbbbf32 | |||
| 358e71d98e | |||
| 29af228a96 | |||
| e8401a08a8 | |||
| e994ff605d | |||
| 8dc8fcce77 | |||
| 3a9b00fa4e | |||
| 8d84d0cafa | |||
| 244a0cc569 | |||
| a5c2d6014f | |||
| 13bff7e353 | |||
| 9a318ceddc | |||
| 213cfb3dad | |||
| 73a2e52a75 | |||
| b308c942d5 | |||
| 6d87e6aa76 | |||
| 5cd45b1278 | |||
| c5790de3db | |||
| 8d638aaa75 | |||
| 0836c46e64 | |||
| da47d46c45 | |||
| bd9c54e820 | |||
| 0d0c403037 | |||
| 09fabf1e22 | |||
| 7720be6d84 | |||
| 2291d5ba79 | |||
| ef1378462c | |||
| 9c37cf8ef1 | |||
| 8351d01c71 | |||
| 32a5d3c2b7 | |||
| 3e4b19a1e5 | |||
| cf62c812f3 | |||
| 90b97b147b | |||
| 2ba65c2328 | |||
| 7884eb5bdf | |||
| b229e02d33 | |||
| 014fce77d8 | |||
| b4fe0530d8 | |||
| 21025f64d5 | |||
| 7b32212c97 | |||
| 95767b9d85 | |||
| 47c7a30bbd | |||
| 962e5b1888 | |||
| 2e0a051539 | |||
| d16d5eeec5 | |||
| 9e786cf754 | |||
| 227ab55617 | |||
| 0f0041725e | |||
| accc2c27f5 | |||
| 32e319920c | |||
| dd92d492ee | |||
| 3e1d902560 | |||
| e3aada4a2f | |||
| 504ba96506 | |||
| 11a4604102 | |||
| f6ae2f242c | |||
| 3cf1a2fbf4 | |||
| a33fac607c | |||
| 14551e4af7 | |||
| b117f617cf | |||
| b9dbefb21a | |||
| 2f77d06756 | |||
| 7f68cece3a | |||
| d09debd85a | |||
| 3bff2e5ffd | |||
| c13d867b95 | |||
| e38b84a2f3 | |||
| 5772937515 | |||
| d7ec646506 | |||
| f0255837d4 | |||
| 7c2475e886 | |||
| b06b8e67f5 | |||
| 3fb6fa2444 | |||
| e1cca94579 | |||
| 1b64c62faf | |||
| 777c869c61 | |||
| 972a28c4f4 | |||
| 3259bd7e23 | |||
| 9e8b1b424b | |||
| 58e677d112 | |||
| 4fb31f1cab | |||
| c51a233970 | |||
| fc5c63d697 | |||
| d4cdccb51b | |||
| f6113de170 | |||
| f68639c5fa | |||
| 688be43811 | |||
| 1e6fd243b1 | |||
| d1ade20413 | |||
| 436d796689 | |||
| 2ac81bfc4d | |||
| fb139b2925 | |||
| 8f7ea692c6 | |||
| cf2fb01edb | |||
| 4d41d90c97 | |||
| fc9604abb0 | |||
| 641c626f11 | |||
| e082abea28 | |||
| 7411e8dc41 | |||
| d809444065 | |||
| 456aec3390 | |||
| c29c4ff968 | |||
| 62406b5ab5 | |||
| 0c59ad8087 | |||
| 702cef84fa | |||
| 892299346f | |||
| 7c98be0e52 | |||
| 1ced19c3ce | |||
| 1ecfe4b274 | |||
| b87cdda9e4 | |||
| 577e6caa1a | |||
| 84d84199f6 | |||
| bbb8dd774a | |||
| b2b85d460f | |||
| 6538163094 | |||
| c2d18a28a4 | |||
| 170198157b | |||
| 79eb0cdfb1 | |||
| 684b6b182d | |||
| 7eaccfd522 | |||
| 838aadc579 | |||
| 5d5ec90592 | |||
| 3721ba72d8 | |||
| 7b1777cf37 | |||
| 089f00b6f1 | |||
| dcf34b6578 | |||
| 40a9356b9f | |||
| 0ac56cc13d | |||
| 0d6dc0852c | |||
| 5448a997cc | |||
| c0981c412e | |||
| 2d1707ec2a | |||
| 0b6d0efa7b | |||
| bf664fce1b | |||
| 55889aee64 | |||
| e9593bf49e | |||
| a68f6bf6de | |||
| b019a15c80 | |||
| 581173eeb8 | |||
| a148672433 | |||
| 7efbfa0b15 | |||
| c573cf2370 | |||
| 04a2afd8ee | |||
| 0e421d2eb0 | |||
| badad141d3 | |||
| ebccdda8ab | |||
| 091533246e | |||
| 95da55c0fa | |||
| 23712375aa | |||
| 7586b6ec90 | |||
| 0aa9b2df78 | |||
| 525813722d | |||
| 6e805be1d6 | |||
| 6bb15b7d58 | |||
| 79f230b0b0 | |||
| 641ece6376 | |||
| b37c171676 | |||
| d1c69048ed | |||
| 8b9263d38f | |||
| 848985b200 | |||
| 0028563253 | |||
| 7db824f457 | |||
| c490911eb6 | |||
| cac0f49bb8 | |||
| 6d57e0c530 | |||
| 0235a00545 | |||
| 0770e2ddec | |||
| 214d679e2d | |||
| 7dbe330099 | |||
| f76027d449 | |||
| 360a6939a4 | |||
| d3e120c6fb | |||
| dabe7ac584 | |||
| 221281b714 | |||
| 72f5d84cb7 | |||
| 91253c4f32 | |||
| c3de114585 | |||
| 593295c7d9 | |||
| b5546262ca | |||
| 6b42b3b329 | |||
| a8b66cf020 | |||
| 6e0685047e | |||
| 9e78cbaa89 | |||
| 44087ca7df | |||
| 78de34538d | |||
| 31982bcfc6 | |||
| 338298b8af | |||
| 0edeb49467 | |||
| 4f229dea09 | |||
| 0f354c79d1 | |||
| ce1900c477 | |||
| 1bf8237d64 | |||
| c992de45a3 | |||
| 51fe46e6d9 | |||
| 9b9af6dc9d | |||
| ca7e8dbb74 | |||
| e2534fe88d | |||
| 04a8bfb455 | |||
| 1015df8fa8 | |||
| dab3eb5ece | |||
| 91a6a7797d | |||
| 18f9677b54 | |||
| 6a549f0548 | |||
| 7522c36112 | |||
| 9bd36a76e8 | |||
| e02afe1775 | |||
| a6e0ea8bd2 | |||
| 148d95def5 | |||
| 75db279364 | |||
| e1c0616a14 | |||
| dc6519f7c1 | |||
| 206ec4e5bf | |||
| 8d41363085 | |||
| e0a4e72be5 | |||
| 1a6f410257 | |||
| 7127ad967b | |||
| 9bcca1a77b | |||
| 7d63c196d5 | |||
| d31b2d0038 | |||
| 1cbd3d6a9f | |||
| 46d3e7c1a4 | |||
| 1e6780e496 | |||
| 031b20f197 | |||
| 54e8eaab1c | |||
| 875ee0a871 | |||
| c1e7d4314f | |||
| 676041dc15 | |||
| 7f8cd0caf7 | |||
| abbf9798b4 | |||
| 3435e63b52 | |||
| 793877568f | |||
| ba1c7101c5 | |||
| d0dc0a769e | |||
| 5b31c2f073 | |||
| a6eea4642a |
@@ -1,12 +1,5 @@
|
||||
/bin
|
||||
/local
|
||||
/pkg
|
||||
/src
|
||||
/website/.sass-cache
|
||||
/website/build
|
||||
.DS_Store
|
||||
.vagrant
|
||||
test/.env
|
||||
|
||||
website/.bundle
|
||||
website/vendor
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
sudo: false
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
- tip
|
||||
|
||||
install: make updatedeps
|
||||
|
||||
script:
|
||||
- GOMAXPROCS=2 make test
|
||||
#- go test -race ./...
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#packer-tool"
|
||||
skip_join: true
|
||||
use_notice: true
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
-1404
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
# Contributing to Packer
|
||||
|
||||
**First:** if you're unsure or afraid of _anything_, just ask
|
||||
or submit the issue or pull request anyways. You won't be yelled at for
|
||||
giving your best effort. The worst that can happen is that you'll be
|
||||
politely asked to change something. We appreciate any sort of contributions,
|
||||
and don't want a wall of rules to get in the way of that.
|
||||
|
||||
However, for those individuals who want a bit more guidance on the
|
||||
best way to contribute to the project, read on. This document will cover
|
||||
what we're looking for. By addressing all the points we're looking for,
|
||||
it raises the chances we can quickly merge or address your contributions.
|
||||
|
||||
## Issues
|
||||
|
||||
### Reporting an Issue
|
||||
|
||||
* Make sure you test against the latest released version. It is possible
|
||||
we already fixed the bug you're experiencing.
|
||||
|
||||
* Run the command with debug ouput with the environment variable
|
||||
`PACKER_LOG`. For example: `PACKER_LOG=1 packer build template.json`. Take
|
||||
the *entire* output and create a [gist](https://gist.github.com) for linking
|
||||
to in your issue. Packer should strip sensitive keys from the output,
|
||||
but take a look through just in case.
|
||||
|
||||
* Provide a reproducible test case. If a contributor can't reproduce an
|
||||
issue, then it dramatically lowers the chances it'll get fixed. And in
|
||||
some cases, the issue will eventually be closed.
|
||||
|
||||
* Respond promptly to any questions made by the Packer team to your issue.
|
||||
Stale issues will be closed.
|
||||
|
||||
### Issue Lifecycle
|
||||
|
||||
1. The issue is reported.
|
||||
|
||||
2. The issue is verified and categorized by a Packer collaborator.
|
||||
Categorization is done via tags. For example, bugs are marked as "bugs"
|
||||
and easy fixes are marked as "easy".
|
||||
|
||||
3. Unless it is critical, the issue is left for a period of time (sometimes
|
||||
many weeks), giving outside contributors a chance to address the issue.
|
||||
|
||||
4. The issue is addressed in a pull request or commit. The issue will be
|
||||
referenced in the commit message so that the code that fixes it is clearly
|
||||
linked.
|
||||
|
||||
5. The issue is closed.
|
||||
|
||||
## Setting up Go to work on Packer
|
||||
|
||||
If you have never worked with Go before, you will have to complete the
|
||||
following steps in order to be able to compile and test Packer.
|
||||
|
||||
1. Install Go. Make sure the Go version is at least Go 1.4. Packer will not work with anything less than
|
||||
Go 1.4. On a Mac, you can `brew install go` to install Go 1.4.
|
||||
|
||||
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
||||
For example, you can add to your `.bash_profile`.
|
||||
|
||||
```
|
||||
export GOPATH=$HOME/Documents/golang
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
3. Install and build `gox` with
|
||||
|
||||
```
|
||||
go get github.com/mitchellh/gox
|
||||
cd $GOPATH/src/github.com/mitchellh/gox
|
||||
go build
|
||||
```
|
||||
|
||||
4. Download the Packer source (and its dependencies) by running
|
||||
`go get github.com/mitchellh/packer`. This will download the Packer
|
||||
source to `$GOPATH/src/github.com/mitchellh/packer`.
|
||||
|
||||
5. Make your changes to the Packer source. You can run `make` from the main
|
||||
source directory to recompile all the binaries. Any compilation errors
|
||||
will be shown when the binaries are rebuilding.
|
||||
|
||||
6. Test your changes by running `make test` and then running
|
||||
`$GOPATH/src/github.com/mitchellh/packer/bin/packer` to build a machine.
|
||||
|
||||
7. If everything works well and the tests pass, run `go fmt` on your code
|
||||
before submitting a pull request.
|
||||
@@ -1,56 +1,22 @@
|
||||
TEST?=./...
|
||||
NO_COLOR=\x1b[0m
|
||||
OK_COLOR=\x1b[32;01m
|
||||
ERROR_COLOR=\x1b[31;01m
|
||||
WARN_COLOR=\x1b[33;01m
|
||||
|
||||
default: test vet dev
|
||||
all:
|
||||
@mkdir -p bin/
|
||||
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
@go get -d -v ./...
|
||||
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
|
||||
@./scripts/build.sh
|
||||
|
||||
bin:
|
||||
@sh -c "$(CURDIR)/scripts/build.sh"
|
||||
|
||||
dev:
|
||||
@TF_DEV=1 sh -c "$(CURDIR)/scripts/build.sh"
|
||||
|
||||
# generate runs `go generate` to build the dynamically generated
|
||||
# source files.
|
||||
generate:
|
||||
go generate ./...
|
||||
format:
|
||||
go fmt ./...
|
||||
|
||||
test:
|
||||
@echo "Running tests on:"; git symbolic-ref HEAD; git rev-parse HEAD
|
||||
go test $(TEST) $(TESTARGS) -timeout=10s
|
||||
@$(MAKE) vet
|
||||
@echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
|
||||
@go list -f '{{range .TestImports}}{{.}}\
|
||||
{{end}}' ./... | xargs -n1 go get -d
|
||||
go test ./...
|
||||
|
||||
# testacc runs acceptance tests
|
||||
testacc: generate
|
||||
@if [ "$(TEST)" = "./..." ]; then \
|
||||
echo "ERROR: Set TEST to a specific package"; \
|
||||
exit 1; \
|
||||
fi
|
||||
PACKER_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 45m
|
||||
|
||||
testrace:
|
||||
go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
updatedeps:
|
||||
@echo "Updating deps on:"; git symbolic-ref HEAD; git rev-parse HEAD
|
||||
go get -u github.com/mitchellh/gox
|
||||
go get -u golang.org/x/tools/cmd/stringer
|
||||
go list ./... \
|
||||
| xargs go list -f '{{join .Deps "\n"}}' \
|
||||
| grep -v github.com/mitchellh/packer \
|
||||
| grep -v '/internal/' \
|
||||
| sort -u \
|
||||
| xargs go get -f -u -v
|
||||
@echo "Finished updating deps, now on:"; git symbolic-ref HEAD; git rev-parse HEAD
|
||||
|
||||
vet:
|
||||
@echo "Running go vet on:"; git symbolic-ref HEAD; git rev-parse HEAD
|
||||
@go vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
|
||||
go get golang.org/x/tools/cmd/vet; \
|
||||
fi
|
||||
@go vet ./... ; if [ $$? -eq 1 ]; then \
|
||||
echo ""; \
|
||||
echo "Vet found suspicious constructs. Please check the reported constructs"; \
|
||||
echo "and fix them if necessary before submitting the code for reviewal."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: bin default generate test testacc updatedeps vet
|
||||
.PHONY: all format test
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Packer
|
||||
|
||||
[](https://travis-ci.org/mitchellh/packer)
|
||||
[](https://ci.appveyor.com/project/hashicorp/packer)
|
||||
|
||||
* Website: http://www.packer.io
|
||||
* IRC: `#packer-tool` on Freenode
|
||||
* Mailing list: [Google Groups](http://groups.google.com/group/packer-tool)
|
||||
@@ -12,31 +9,20 @@ from a single source configuration.
|
||||
|
||||
Packer is lightweight, runs on every major operating system, and is highly
|
||||
performant, creating machine images for multiple platforms in parallel.
|
||||
Packer comes out of the box with support for the following platforms:
|
||||
* Amazon EC2 (AMI). Both EBS-backed and instance-store AMIs
|
||||
* DigitalOcean
|
||||
* Docker
|
||||
* Google Compute Engine
|
||||
* OpenStack
|
||||
* Parallels
|
||||
* QEMU. Both KVM and Xen images.
|
||||
* VirtualBox
|
||||
* VMware
|
||||
|
||||
Support for other platforms can be added via plugins.
|
||||
|
||||
The images that Packer creates can easily be turned into
|
||||
Packer comes out of the box with support for creating AMIs (EC2), VMware
|
||||
images, and VirtualBox images. Support for more platforms can be added via
|
||||
plugins. The images that Packer creates an easily be turned into
|
||||
[Vagrant](http://www.vagrantup.com) boxes.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Note:** There is a great
|
||||
[introduction and getting started guide](http://www.packer.io/intro)
|
||||
[introduction and getting started guide](http://localhost:4567/intro)
|
||||
for those with a bit more patience. Otherwise, the quick start below
|
||||
will get you up and running quickly, at the sacrifice of not explaining some
|
||||
key points.
|
||||
|
||||
First, [download a pre-built Packer binary](http://www.packer.io/downloads.html)
|
||||
First, [downloada pre-built Packer binary](http://localhost:4567/downloads.html)
|
||||
for your operating system or [compile Packer yourself](#developing-packer).
|
||||
|
||||
After Packer is installed, create your first template, which tells Packer
|
||||
@@ -55,7 +41,7 @@ own.
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-example {{timestamp}}"
|
||||
"ami_name": "packer-example {{.CreateTime}}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
@@ -81,76 +67,16 @@ http://www.packer.io/docs
|
||||
|
||||
## Developing Packer
|
||||
|
||||
If you wish to work on Packer itself or any of its built-in providers,
|
||||
you'll first need [Go](http://www.golang.org) installed (version 1.4+ is
|
||||
_required_). Make sure Go is properly installed, including setting up
|
||||
a [GOPATH](http://golang.org/doc/code.html#GOPATH).
|
||||
If you wish to work on Packer itself, you'll first need [Go](http://golang.org)
|
||||
installed (version 1.1+ is _required_). Next, clone this repository then just type `make`.
|
||||
In a few moments, you'll have a working `packer` executable:
|
||||
|
||||
Next, install the following software packages, which are needed for some dependencies:
|
||||
|
||||
- [Bazaar](http://bazaar.canonical.com/en/)
|
||||
- [Git](http://git-scm.com/)
|
||||
- [Mercurial](http://mercurial.selenic.com/)
|
||||
|
||||
Then, install [Gox](https://github.com/mitchellh/gox), which is used
|
||||
as a compilation tool on top of Go:
|
||||
|
||||
$ go get -u github.com/mitchellh/gox
|
||||
|
||||
Next, clone this repository into `$GOPATH/src/github.com/mitchellh/packer`.
|
||||
Install the necessary dependencies by running `make updatedeps` and then just
|
||||
type `make`. This will compile some more dependencies and then run the tests. If
|
||||
this exits with exit status 0, then everything is working!
|
||||
|
||||
$ make updatedeps
|
||||
...
|
||||
$ make
|
||||
...
|
||||
|
||||
To compile a development version of Packer and the built-in plugins,
|
||||
run `make dev`. This will put Packer binaries in the `bin` folder:
|
||||
|
||||
$ make dev
|
||||
...
|
||||
$ bin/packer
|
||||
...
|
||||
|
||||
|
||||
If you're developing a specific package, you can run tests for just that
|
||||
package by specifying the `TEST` variable. For example below, only
|
||||
`packer` package tests will be run.
|
||||
|
||||
$ make test TEST=./packer
|
||||
...
|
||||
|
||||
### Acceptance Tests
|
||||
|
||||
Packer has comprehensive [acceptance tests](https://en.wikipedia.org/wiki/Acceptance_testing)
|
||||
covering the builders of Packer.
|
||||
|
||||
If you're working on a feature of a builder or a new builder and want
|
||||
verify it is functioning (and also hasn't broken anything else), we recommend
|
||||
running the acceptance tests.
|
||||
|
||||
**Warning:** The acceptance tests create/destroy/modify *real resources*, which
|
||||
may incur real costs in some cases. In the presence of a bug, it is technically
|
||||
possible that broken backends could leave dangling data behind. Therefore,
|
||||
please run the acceptance tests at your own risk. At the very least,
|
||||
we recommend running them in their own private account for whatever builder
|
||||
you're testing.
|
||||
|
||||
To run the acceptance tests, invoke `make testacc`:
|
||||
|
||||
```sh
|
||||
$ make testacc TEST=./builder/amazon/ebs
|
||||
```
|
||||
$ make
|
||||
...
|
||||
$ bin/packer
|
||||
...
|
||||
```
|
||||
|
||||
The `TEST` variable is required, and you should specify the folder where the
|
||||
backend is. The `TESTARGS` variable is recommended to filter down to a specific
|
||||
resource to test, since testing all of them at once can sometimes take a very
|
||||
long time.
|
||||
|
||||
Acceptance tests typically require other environment variables to be set for
|
||||
things such as access keys. The test itself should error early and tell
|
||||
you what to set, so it is not documented here.
|
||||
You can run tests by typing `make test`. This will run tests for Packer core
|
||||
along with all the core builders and commands and such that come with Packer.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
* builder/amazonebs: Copy AMI to multiple regions
|
||||
* builder/vmware: VMX templates
|
||||
* communicator/ssh: Ability to re-establish connection
|
||||
* communicator/ssh: Download()
|
||||
* packer: Communicator should have Close() method
|
||||
* packer/plugin: Better error messages/detection if plugin crashes
|
||||
* provisioner/shell: Arguments
|
||||
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
$script = <<SCRIPT
|
||||
SRCROOT="/opt/go"
|
||||
|
||||
# Install Go
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential mercurial
|
||||
sudo hg clone -u release https://code.google.com/p/go ${SRCROOT}
|
||||
cd ${SRCROOT}/src
|
||||
sudo ./all.bash
|
||||
|
||||
# Setup the GOPATH
|
||||
sudo mkdir -p /opt/gopath
|
||||
cat <<EOF >/tmp/gopath.sh
|
||||
export GOPATH="/opt/gopath"
|
||||
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH"
|
||||
EOF
|
||||
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
|
||||
sudo chmod 0755 /etc/profile.d/gopath.sh
|
||||
|
||||
# Make sure the gopath is usable by vagrant
|
||||
sudo chown -R vagrant:vagrant $SRCROOT
|
||||
sudo chown -R vagrant:vagrant /opt/gopath
|
||||
|
||||
# Install some other stuff we need
|
||||
sudo apt-get install -y curl git-core zip
|
||||
SCRIPT
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "chef/ubuntu-12.04"
|
||||
|
||||
config.vm.provision "shell", inline: $script
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
["vmware_fusion", "vmware_workstation"].each do |p|
|
||||
config.vm.provider "p" do |v|
|
||||
v.vmx["memsize"] = "2048"
|
||||
v.vmx["numvcpus"] = "2"
|
||||
v.vmx["cpuid.coresPerSocket"] = "1"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# appveyor.yml reference : http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
version: "{build}"
|
||||
|
||||
skip_tags: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
matrix:
|
||||
- GOARCH: 386
|
||||
GOVERSION: 1.4.2
|
||||
- GOARCH: amd64
|
||||
GOVERSION: 1.4.2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mitchellh\packer
|
||||
|
||||
install:
|
||||
- set Path=c:\go\bin;%Path%
|
||||
- echo %Path%
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-%GOARCH%.msi
|
||||
- msiexec /i go%GOVERSION%.windows-%GOARCH%.msi /q
|
||||
- go version
|
||||
- go env
|
||||
- go get -d -v -t ./...
|
||||
|
||||
build_script:
|
||||
- go test -v ./...
|
||||
- go vet ./...
|
||||
- git rev-parse HEAD
|
||||
|
||||
test: off
|
||||
|
||||
deploy: off
|
||||
@@ -1,236 +0,0 @@
|
||||
// The chroot package is able to create an Amazon AMI without requiring
|
||||
// the launch of a new instance for every build. It does this by attaching
|
||||
// and mounting the root volume of another AMI and chrooting into that
|
||||
// directory. It then creates an AMI from that attached drive.
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
const BuilderId = "mitchellh.amazon.chroot"
|
||||
|
||||
// Config is the configuration that is chained through the steps and
|
||||
// settable from the template.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CommandWrapper string `mapstructure:"command_wrapper"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
RootVolumeSize int64 `mapstructure:"root_volume_size"`
|
||||
MountOptions []string `mapstructure:"mount_options"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type wrappedCommandTemplate struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
b.config.ctx.Funcs = awscommon.TemplateFuncs
|
||||
err := config.Decode(&b.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &b.config.ctx,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"command_wrapper",
|
||||
"mount_path",
|
||||
},
|
||||
},
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, 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.CommandWrapper == "" {
|
||||
b.config.CommandWrapper = "{{.Command}}"
|
||||
}
|
||||
|
||||
if b.config.MountPath == "" {
|
||||
b.config.MountPath = "/mnt/packer-amazon-chroot-volumes/{{.Device}}"
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
var errs *packer.MultiError
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
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 nil, errs
|
||||
}
|
||||
|
||||
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, errors.New("The amazon-chroot builder only works on Linux environments.")
|
||||
}
|
||||
|
||||
config, err := b.config.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2conn := ec2.New(config)
|
||||
|
||||
wrappedCommand := func(command string) (string, error) {
|
||||
ctx := b.config.ctx
|
||||
ctx.Data = &wrappedCommandTemplate{Command: command}
|
||||
return interpolate.Render(b.config.CommandWrapper, &ctx)
|
||||
}
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&StepInstanceInfo{},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&StepCheckRootDevice{},
|
||||
&StepFlock{},
|
||||
&StepPrepareDevice{},
|
||||
&StepCreateVolume{
|
||||
RootVolumeSize: b.config.RootVolumeSize,
|
||||
},
|
||||
&StepAttachVolume{},
|
||||
&StepEarlyUnflock{},
|
||||
&StepMountDevice{
|
||||
MountOptions: b.config.MountOptions,
|
||||
},
|
||||
&StepMountExtra{},
|
||||
&StepCopyFiles{},
|
||||
&StepChrootProvision{},
|
||||
&StepEarlyCleanup{},
|
||||
&StepSnapshot{},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&StepRegisterAMI{
|
||||
RootVolumeSize: b.config.RootVolumeSize,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If there are no AMIs, then just return
|
||||
if _, ok := state.GetOk("amis"); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &awscommon.Artifact{
|
||||
Amis: state.Get("amis").(map[string]string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Conn: ec2conn,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"ami_name": "foo",
|
||||
"source_ami": "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ami_name"] = "foo"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ami_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ami_name")
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ChrootMounts(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["chroot_mounts"] = nil
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
|
||||
config["chroot_mounts"] = [][]string{
|
||||
[]string{"bad"},
|
||||
}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
func TestBuilderPrepare_SourceAmi(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["source_ami"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["source_ami"] = "foo"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_CommandWrapper(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["command_wrapper"] = "echo hi; {{.Command}}"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
// Cleanup is an interface that some steps implement for early cleanup.
|
||||
type Cleanup interface {
|
||||
CleanupFunc(multistep.StateBag) error
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// CommandWrapper is a type that given a command, will possibly modify that
|
||||
// command in-flight. This might return an error.
|
||||
type CommandWrapper func(string) (string, error)
|
||||
|
||||
// ShellCommand takes a command string and returns an *exec.Cmd to execute
|
||||
// it within the context of a shell (/bin/sh).
|
||||
func ShellCommand(command string) *exec.Cmd {
|
||||
return exec.Command("/bin/sh", "-c", command)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Communicator is a special communicator that works by executing
|
||||
// commands locally but within a chroot.
|
||||
type Communicator struct {
|
||||
Chroot string
|
||||
CmdWrapper CommandWrapper
|
||||
}
|
||||
|
||||
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||
command, err := c.CmdWrapper(
|
||||
fmt.Sprintf("chroot %s /bin/sh -c \"%s\"", c.Chroot, cmd.Command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localCmd := ShellCommand(command)
|
||||
localCmd.Stdin = cmd.Stdin
|
||||
localCmd.Stdout = cmd.Stdout
|
||||
localCmd.Stderr = cmd.Stderr
|
||||
log.Printf("Executing: %s %#v", localCmd.Path, localCmd.Args)
|
||||
if err := localCmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
exitStatus := 0
|
||||
if err := localCmd.Wait(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitStatus = 1
|
||||
|
||||
// There is no process-independent way to get the REAL
|
||||
// exit status so we just try to go deeper.
|
||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||
exitStatus = status.ExitStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Chroot execution exited with '%d': '%s'",
|
||||
exitStatus, cmd.Command)
|
||||
cmd.SetExited(exitStatus)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Communicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
|
||||
dst = filepath.Join(c.Chroot, dst)
|
||||
log.Printf("Uploading to chroot dir: %s", dst)
|
||||
tf, err := ioutil.TempFile("", "packer-amazon-chroot")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error preparing shell script: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
io.Copy(tf, r)
|
||||
|
||||
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp %s %s", tf.Name(), dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ShellCommand(cpCmd).Run()
|
||||
}
|
||||
|
||||
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
// If src ends with a trailing "/", copy from "src/." so that
|
||||
// directory contents (including hidden files) are copied, but the
|
||||
// directory "src" is omitted. BSD does this automatically when
|
||||
// the source contains a trailing slash, but linux does not.
|
||||
if src[len(src)-1] == '/' {
|
||||
src = src + "."
|
||||
}
|
||||
|
||||
// TODO: remove any file copied if it appears in `exclude`
|
||||
chrootDest := filepath.Join(c.Chroot, dst)
|
||||
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, chrootDest)
|
||||
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R '%s' %s", src, chrootDest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := ShellCommand(cpCmd)
|
||||
cmd.Env = append(cmd.Env, "LANG=C")
|
||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||
cmd.Stderr = &stderr
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(stderr.String(), "No such file") {
|
||||
// This just means that the directory was empty. Just ignore it.
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Communicator) Download(src string, w io.Writer) error {
|
||||
src = filepath.Join(c.Chroot, src)
|
||||
log.Printf("Downloading from chroot dir: %s", src)
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Communicator{}
|
||||
if _, ok := raw.(packer.Communicator); !ok {
|
||||
t.Fatalf("Communicator should be a communicator")
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package chroot
|
||||
@@ -1,49 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
}
|
||||
|
||||
first, err := ioutil.TempFile("", "copy_files_test")
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't create temp file.")
|
||||
}
|
||||
defer os.Remove(first.Name())
|
||||
newName := first.Name() + "-new"
|
||||
|
||||
payload := "copy_files_test.go payload"
|
||||
if _, err = first.WriteString(payload); err != nil {
|
||||
t.Fatalf("Couldn't write payload to first file.")
|
||||
}
|
||||
first.Sync()
|
||||
|
||||
cmd := ShellCommand(fmt.Sprintf("cp %s %s", first.Name(), newName))
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Couldn't copy file")
|
||||
}
|
||||
defer os.Remove(newName)
|
||||
|
||||
second, err := os.Open(newName)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't open copied file.")
|
||||
}
|
||||
defer second.Close()
|
||||
|
||||
var copiedPayload = make([]byte, len(payload))
|
||||
if _, err := second.Read(copiedPayload); err != nil {
|
||||
t.Fatalf("Couldn't open copied file for reading.")
|
||||
}
|
||||
|
||||
if string(copiedPayload) != payload {
|
||||
t.Fatalf("payload not copied.")
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AvailableDevice finds an available device and returns it. Note that
|
||||
// you should externally hold a flock or something in order to guarantee
|
||||
// that this device is available across processes.
|
||||
func AvailableDevice() (string, error) {
|
||||
prefix, err := devicePrefix()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
letters := "fghijklmnop"
|
||||
for _, letter := range letters {
|
||||
device := fmt.Sprintf("/dev/%s%c", prefix, letter)
|
||||
|
||||
// If the block device itself, i.e. /dev/sf, exists, then we
|
||||
// can't use any of the numbers either.
|
||||
if _, err := os.Stat(device); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// To be able to build both Paravirtual and HVM images, the unnumbered
|
||||
// device and the first numbered one must be available.
|
||||
// E.g. /dev/xvdf and /dev/xvdf1
|
||||
numbered_device := fmt.Sprintf("%s%d", device, 1)
|
||||
if _, err := os.Stat(numbered_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")
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
func lockFile(*os.File) error {
|
||||
return errors.New("not supported on Windows")
|
||||
}
|
||||
|
||||
func unlockFile(f *os.File) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// See: http://linux.die.net/include/sys/file.h
|
||||
const LOCK_EX = 2
|
||||
const LOCK_NB = 4
|
||||
const LOCK_UN = 8
|
||||
|
||||
func lockFile(f *os.File) error {
|
||||
err := syscall.Flock(int(f.Fd()), LOCK_EX)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unlockFile(f *os.File) error {
|
||||
return syscall.Flock(int(f.Fd()), LOCK_UN)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepAttachVolume attaches the previously created volume to an
|
||||
// available device location.
|
||||
//
|
||||
// Produces:
|
||||
// device string - The location where the volume was attached.
|
||||
// attach_cleanup CleanupFunc
|
||||
type StepAttachVolume struct {
|
||||
attached bool
|
||||
volumeId string
|
||||
}
|
||||
|
||||
func (s *StepAttachVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
device := state.Get("device").(string)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
volumeId := state.Get("volume_id").(string)
|
||||
|
||||
// For the API call, it expects "sd" prefixed devices.
|
||||
attachVolume := strings.Replace(device, "/xvd", "/sd", 1)
|
||||
|
||||
ui.Say(fmt.Sprintf("Attaching the root volume to %s", attachVolume))
|
||||
_, err := ec2conn.AttachVolume(&ec2.AttachVolumeInput{
|
||||
InstanceID: instance.InstanceID,
|
||||
VolumeID: &volumeId,
|
||||
Device: &attachVolume,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error attaching volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Mark that we attached it so we can detach it later
|
||||
s.attached = true
|
||||
s.volumeId = volumeId
|
||||
|
||||
// Wait for the volume to become attached
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"attaching"},
|
||||
StepState: state,
|
||||
Target: "attached",
|
||||
Refresh: func() (interface{}, string, error) {
|
||||
attempts := 0
|
||||
for attempts < 30 {
|
||||
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIDs: []*string{&volumeId}})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(resp.Volumes[0].Attachments) > 0 {
|
||||
a := resp.Volumes[0].Attachments[0]
|
||||
return a, *a.State, nil
|
||||
}
|
||||
// When Attachment on volume is not present sleep for 2s and retry
|
||||
attempts += 1
|
||||
ui.Say(fmt.Sprintf(
|
||||
"Volume %s show no attachments. Attempt %d/30. Sleeping for 2s and will retry.",
|
||||
volumeId, attempts))
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Attachment on volume is not present after all attempts
|
||||
return nil, "", errors.New("No attachments on volume.")
|
||||
},
|
||||
}
|
||||
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("attach_cleanup", s)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAttachVolume) Cleanup(state multistep.StateBag) {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
if err := s.CleanupFunc(state); err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
|
||||
if !s.attached {
|
||||
return nil
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Detaching EBS volume...")
|
||||
_, err := ec2conn.DetachVolume(&ec2.DetachVolumeInput{VolumeID: &s.volumeId})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error detaching EBS volume: %s", err)
|
||||
}
|
||||
|
||||
s.attached = false
|
||||
|
||||
// Wait for the volume to detach
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"attaching", "attached", "detaching"},
|
||||
StepState: state,
|
||||
Target: "detached",
|
||||
Refresh: func() (interface{}, string, error) {
|
||||
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIDs: []*string{&s.volumeId}})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
v := resp.Volumes[0]
|
||||
if len(v.Attachments) > 0 {
|
||||
return v, *v.Attachments[0].State, nil
|
||||
} else {
|
||||
return v, "detached", nil
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error waiting for volume: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAttachVolumeCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepAttachVolume)
|
||||
if _, ok := raw.(Cleanup); !ok {
|
||||
t.Fatalf("cleanup func should be a CleanupFunc")
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCheckRootDevice makes sure the root device on the AMI is EBS-backed.
|
||||
type StepCheckRootDevice struct{}
|
||||
|
||||
func (s *StepCheckRootDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Checking the root device on source AMI...")
|
||||
|
||||
// It must be EBS-backed otherwise the build won't work
|
||||
if *image.RootDeviceType != "ebs" {
|
||||
err := fmt.Errorf("The root device of the source AMI must be EBS-backed.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCheckRootDevice) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,36 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// StepChrootProvision provisions the instance within a chroot.
|
||||
type StepChrootProvision struct {
|
||||
mounts []string
|
||||
}
|
||||
|
||||
func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction {
|
||||
hook := state.Get("hook").(packer.Hook)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
// Create our communicator
|
||||
comm := &Communicator{
|
||||
Chroot: mountPath,
|
||||
CmdWrapper: wrappedCommand,
|
||||
}
|
||||
|
||||
// Provision
|
||||
log.Println("Running the provision hook")
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepChrootProvision) Cleanup(state multistep.StateBag) {}
|
||||
@@ -1,89 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StepCopyFiles copies some files from the host into the chroot environment.
|
||||
//
|
||||
// Produces:
|
||||
// copy_files_cleanup CleanupFunc - A function to clean up the copied files
|
||||
// early.
|
||||
type StepCopyFiles struct {
|
||||
files []string
|
||||
}
|
||||
|
||||
func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
s.files = make([]string, 0, len(config.CopyFiles))
|
||||
if len(config.CopyFiles) > 0 {
|
||||
ui.Say("Copying files from host to chroot...")
|
||||
for _, path := range config.CopyFiles {
|
||||
ui.Message(path)
|
||||
chrootPath := filepath.Join(mountPath, path)
|
||||
log.Printf("Copying '%s' to '%s'", path, chrootPath)
|
||||
|
||||
cmdText, err := wrappedCommand(fmt.Sprintf("cp --remove-destination %s %s", path, chrootPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error building copy command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
stderr.Reset()
|
||||
cmd := ShellCommand(cmdText)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Error copying file: %s\nnStderr: %s", err, stderr.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.files = append(s.files, chrootPath)
|
||||
}
|
||||
}
|
||||
|
||||
state.Put("copy_files_cleanup", s)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCopyFiles) Cleanup(state multistep.StateBag) {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
if err := s.CleanupFunc(state); err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepCopyFiles) CleanupFunc(state multistep.StateBag) error {
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
if s.files != nil {
|
||||
for _, file := range s.files {
|
||||
log.Printf("Removing: %s", file)
|
||||
localCmdText, err := wrappedCommand(fmt.Sprintf("rm -f %s", file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localCmd := ShellCommand(localCmdText)
|
||||
if err := localCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.files = nil
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCopyFilesCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepCopyFiles)
|
||||
if _, ok := raw.(Cleanup); !ok {
|
||||
t.Fatalf("cleanup func should be a CleanupFunc")
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// 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
|
||||
RootVolumeSize int64
|
||||
}
|
||||
|
||||
func (s *StepCreateVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Determine the root device snapshot
|
||||
log.Printf("Searching for root device of the image (%s)", *image.RootDeviceName)
|
||||
var rootDevice *ec2.BlockDeviceMapping
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if *device.DeviceName == *image.RootDeviceName {
|
||||
rootDevice = device
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rootDevice == nil {
|
||||
err := fmt.Errorf("Couldn't find root device!")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Creating the root volume...")
|
||||
vs := *rootDevice.EBS.VolumeSize
|
||||
if s.RootVolumeSize > *rootDevice.EBS.VolumeSize {
|
||||
vs = s.RootVolumeSize
|
||||
}
|
||||
createVolume := &ec2.CreateVolumeInput{
|
||||
AvailabilityZone: instance.Placement.AvailabilityZone,
|
||||
Size: aws.Int64(vs),
|
||||
SnapshotID: rootDevice.EBS.SnapshotID,
|
||||
VolumeType: rootDevice.EBS.VolumeType,
|
||||
IOPS: rootDevice.EBS.IOPS,
|
||||
}
|
||||
log.Printf("Create args: %s", createVolume)
|
||||
|
||||
createVolumeResp, err := ec2conn.CreateVolume(createVolume)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating root volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the volume ID so we remember to delete it later
|
||||
s.volumeId = *createVolumeResp.VolumeID
|
||||
log.Printf("Volume ID: %s", s.volumeId)
|
||||
|
||||
// Wait for the volume to become ready
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"creating"},
|
||||
StepState: state,
|
||||
Target: "available",
|
||||
Refresh: func() (interface{}, string, error) {
|
||||
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIDs: []*string{&s.volumeId}})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
v := resp.Volumes[0]
|
||||
return v, *v.State, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for volume: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("volume_id", s.volumeId)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCreateVolume) Cleanup(state multistep.StateBag) {
|
||||
if s.volumeId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Deleting the created EBS volume...")
|
||||
_, err := ec2conn.DeleteVolume(&ec2.DeleteVolumeInput{VolumeID: &s.volumeId})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error deleting EBS volume: %s", err))
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// StepEarlyCleanup performs some of the cleanup steps early in order to
|
||||
// prepare for snapshotting and creating an AMI.
|
||||
type StepEarlyCleanup struct{}
|
||||
|
||||
func (s *StepEarlyCleanup) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
cleanupKeys := []string{
|
||||
"copy_files_cleanup",
|
||||
"mount_extra_cleanup",
|
||||
"mount_device_cleanup",
|
||||
"attach_cleanup",
|
||||
}
|
||||
|
||||
for _, key := range cleanupKeys {
|
||||
c := state.Get(key).(Cleanup)
|
||||
log.Printf("Running cleanup func: %s", key)
|
||||
if err := c.CleanupFunc(state); err != nil {
|
||||
err := fmt.Errorf("Error cleaning up: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepEarlyCleanup) Cleanup(state multistep.StateBag) {}
|
||||
@@ -1,28 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// StepEarlyUnflock unlocks the flock.
|
||||
type StepEarlyUnflock struct{}
|
||||
|
||||
func (s *StepEarlyUnflock) Run(state multistep.StateBag) multistep.StepAction {
|
||||
cleanup := state.Get("flock_cleanup").(Cleanup)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
log.Println("Unlocking file lock...")
|
||||
if err := cleanup.CleanupFunc(state); err != nil {
|
||||
err := fmt.Errorf("Error unlocking file lock: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepEarlyUnflock) Cleanup(state multistep.StateBag) {}
|
||||
@@ -1,72 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StepFlock provisions the instance within a chroot.
|
||||
//
|
||||
// Produces:
|
||||
// flock_cleanup Cleanup - To perform early cleanup
|
||||
type StepFlock struct {
|
||||
fh *os.File
|
||||
}
|
||||
|
||||
func (s *StepFlock) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
lockfile := "/var/lock/packer-chroot/lock"
|
||||
if err := os.MkdirAll(filepath.Dir(lockfile), 0755); err != nil {
|
||||
err := fmt.Errorf("Error creating lock: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Obtaining lock: %s", lockfile)
|
||||
f, err := os.Create(lockfile)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating lock: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// LOCK!
|
||||
if err := lockFile(f); err != nil {
|
||||
err := fmt.Errorf("Error obtaining lock: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the file handle, we can't close it because we need to hold
|
||||
// the lock.
|
||||
s.fh = f
|
||||
|
||||
state.Put("flock_cleanup", s)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepFlock) Cleanup(state multistep.StateBag) {
|
||||
s.CleanupFunc(state)
|
||||
}
|
||||
|
||||
func (s *StepFlock) CleanupFunc(state multistep.StateBag) error {
|
||||
if s.fh == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Unlocking: %s", s.fh.Name())
|
||||
if err := unlockFile(s.fh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.fh = nil
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFlockCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepFlock)
|
||||
if _, ok := raw.(Cleanup); !ok {
|
||||
t.Fatalf("cleanup func should be a CleanupFunc")
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepInstanceInfo verifies that this builder is running on an EC2 instance.
|
||||
type StepInstanceInfo struct{}
|
||||
|
||||
func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Get our own instance ID
|
||||
ui.Say("Gathering information about this EC2 instance...")
|
||||
instanceIdBytes, err := common.GetInstanceMetaData("instance-id")
|
||||
if err != nil {
|
||||
log.Printf("Error: %s", err)
|
||||
err := fmt.Errorf(
|
||||
"Error retrieving the ID of the instance Packer is running on.\n" +
|
||||
"Please verify Packer is running on a proper AWS EC2 instance.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
instanceId := string(instanceIdBytes)
|
||||
log.Printf("Instance ID: %s", instanceId)
|
||||
|
||||
// Query the entire instance metadata
|
||||
instancesResp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{InstanceIDs: []*string{&instanceId}})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting instance data: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(instancesResp.Reservations) == 0 {
|
||||
err := fmt.Errorf("Error getting instance data: no instance found.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
instance := instancesResp.Reservations[0].Instances[0]
|
||||
state.Put("instance", instance)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepInstanceInfo) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,138 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
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 {
|
||||
MountOptions []string
|
||||
|
||||
mountPath string
|
||||
}
|
||||
|
||||
func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
device := state.Get("device").(string)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &mountPathData{Device: filepath.Base(device)}
|
||||
mountPath, err := interpolate.Render(config.MountPath, &ctx)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
mountPath, err = filepath.Abs(mountPath)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Mount path: %s", mountPath)
|
||||
|
||||
if err := os.MkdirAll(mountPath, 0755); err != nil {
|
||||
err := fmt.Errorf("Error creating mount directory: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Source image virtualization type is: %s", *image.VirtualizationType)
|
||||
deviceMount := device
|
||||
if *image.VirtualizationType == "hvm" {
|
||||
deviceMount = fmt.Sprintf("%s%d", device, 1)
|
||||
}
|
||||
state.Put("deviceMount", deviceMount)
|
||||
|
||||
ui.Say("Mounting the root device...")
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
// build mount options from mount_options config, usefull for nouuid options
|
||||
// or other specific device type settings for mount
|
||||
opts := ""
|
||||
if len(s.MountOptions) > 0 {
|
||||
opts = "-o " + strings.Join(s.MountOptions, " -o ")
|
||||
}
|
||||
mountCommand, err := wrappedCommand(
|
||||
fmt.Sprintf("mount %s %s %s", opts, deviceMount, mountPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating mount command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
cmd := ShellCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Error mounting root volume: %s\nStderr: %s", err, stderr.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the mount path so we remember to unmount it later
|
||||
s.mountPath = mountPath
|
||||
state.Put("mount_path", s.mountPath)
|
||||
state.Put("mount_device_cleanup", s)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepMountDevice) Cleanup(state multistep.StateBag) {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
if err := s.CleanupFunc(state); err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error {
|
||||
if s.mountPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
ui.Say("Unmounting the root device...")
|
||||
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", s.mountPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating unmount command: %s", err)
|
||||
}
|
||||
|
||||
cmd := ShellCommand(unmountCommand)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("Error unmounting root device: %s", err)
|
||||
}
|
||||
|
||||
s.mountPath = ""
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMountDeviceCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepMountDevice)
|
||||
if _, ok := raw.(Cleanup); !ok {
|
||||
t.Fatalf("cleanup func should be a CleanupFunc")
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// StepMountExtra mounts the attached device.
|
||||
//
|
||||
// Produces:
|
||||
// mount_extra_cleanup CleanupFunc - To perform early cleanup
|
||||
type StepMountExtra struct {
|
||||
mounts []string
|
||||
}
|
||||
|
||||
func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
s.mounts = make([]string, 0, len(config.ChrootMounts))
|
||||
|
||||
ui.Say("Mounting additional paths within the chroot...")
|
||||
for _, mountInfo := range config.ChrootMounts {
|
||||
innerPath := mountPath + mountInfo[2]
|
||||
|
||||
if err := os.MkdirAll(innerPath, 0755); err != nil {
|
||||
err := fmt.Errorf("Error creating mount directory: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
flags := "-t " + mountInfo[0]
|
||||
if mountInfo[0] == "bind" {
|
||||
flags = "--bind"
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2]))
|
||||
stderr := new(bytes.Buffer)
|
||||
mountCommand, err := wrappedCommand(fmt.Sprintf(
|
||||
"mount %s %s %s",
|
||||
flags,
|
||||
mountInfo[1],
|
||||
innerPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating mount command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
cmd := ShellCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Error mounting: %s\nStderr: %s", err, stderr.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.mounts = append(s.mounts, innerPath)
|
||||
}
|
||||
|
||||
state.Put("mount_extra_cleanup", s)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepMountExtra) Cleanup(state multistep.StateBag) {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if err := s.CleanupFunc(state); err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error {
|
||||
if s.mounts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
for len(s.mounts) > 0 {
|
||||
var path string
|
||||
lastIndex := len(s.mounts) - 1
|
||||
path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex]
|
||||
|
||||
grepCommand, err := wrappedCommand(fmt.Sprintf("grep %s /proc/mounts", path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating grep command: %s", err)
|
||||
}
|
||||
|
||||
// Before attempting to unmount,
|
||||
// check to see if path is already unmounted
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd := ShellCommand(grepCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
exitStatus := status.ExitStatus()
|
||||
if exitStatus == 1 {
|
||||
// path has already been unmounted
|
||||
// just skip this path
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating unmount command: %s", err)
|
||||
}
|
||||
|
||||
stderr = new(bytes.Buffer)
|
||||
cmd = ShellCommand(unmountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"Error unmounting device: %s\nStderr: %s", err, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
s.mounts = nil
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMountExtraCleanupFunc_ImplementsCleanupFunc(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepMountExtra)
|
||||
if _, ok := raw.(Cleanup); !ok {
|
||||
t.Fatalf("cleanup func should be a CleanupFunc")
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// StepPrepareDevice finds an available device and sets it.
|
||||
type StepPrepareDevice struct {
|
||||
mounts []string
|
||||
}
|
||||
|
||||
func (s *StepPrepareDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
device := config.DevicePath
|
||||
if device == "" {
|
||||
var err error
|
||||
log.Println("Device path not specified, searching for available device...")
|
||||
device, err = AvailableDevice()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error finding available device: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(device); err == nil {
|
||||
err := fmt.Errorf("Device is in use: %s", device)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Device: %s", device)
|
||||
state.Put("device", device)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPrepareDevice) Cleanup(state multistep.StateBag) {}
|
||||
@@ -1,110 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepRegisterAMI creates the AMI.
|
||||
type StepRegisterAMI struct {
|
||||
RootVolumeSize int64
|
||||
}
|
||||
|
||||
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
snapshotId := state.Get("snapshot_id").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
blockDevices := make([]*ec2.BlockDeviceMapping, len(image.BlockDeviceMappings))
|
||||
for i, device := range image.BlockDeviceMappings {
|
||||
newDevice := device
|
||||
if *newDevice.DeviceName == *image.RootDeviceName {
|
||||
if newDevice.EBS != nil {
|
||||
newDevice.EBS.SnapshotID = aws.String(snapshotId)
|
||||
} else {
|
||||
newDevice.EBS = &ec2.EBSBlockDevice{SnapshotID: aws.String(snapshotId)}
|
||||
}
|
||||
|
||||
if s.RootVolumeSize > *newDevice.EBS.VolumeSize {
|
||||
newDevice.EBS.VolumeSize = aws.Int64(s.RootVolumeSize)
|
||||
}
|
||||
}
|
||||
|
||||
// assume working from a snapshot, so we unset the Encrypted field if set,
|
||||
// otherwise AWS API will return InvalidParameter
|
||||
if newDevice.EBS != nil && newDevice.EBS.Encrypted != nil {
|
||||
newDevice.EBS.Encrypted = nil
|
||||
}
|
||||
|
||||
blockDevices[i] = newDevice
|
||||
}
|
||||
|
||||
registerOpts := buildRegisterOpts(config, image, blockDevices)
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
registerOpts.SRIOVNetSupport = aws.String("simple")
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error registering AMI: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the AMI ID in the state
|
||||
ui.Say(fmt.Sprintf("AMI: %s", *registerResp.ImageID))
|
||||
amis := make(map[string]string)
|
||||
amis[*ec2conn.Config.Region] = *registerResp.ImageID
|
||||
state.Put("amis", amis)
|
||||
|
||||
// Wait for the image to become ready
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "available",
|
||||
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *registerResp.ImageID),
|
||||
StepState: state,
|
||||
}
|
||||
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
if _, err := awscommon.WaitForState(&stateChange); err != nil {
|
||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) {}
|
||||
|
||||
func buildRegisterOpts(config *Config, image *ec2.Image, blockDevices []*ec2.BlockDeviceMapping) *ec2.RegisterImageInput {
|
||||
registerOpts := &ec2.RegisterImageInput{
|
||||
Name: &config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
RootDeviceName: image.RootDeviceName,
|
||||
BlockDeviceMappings: blockDevices,
|
||||
VirtualizationType: image.VirtualizationType,
|
||||
}
|
||||
|
||||
if config.AMIVirtType != "" {
|
||||
registerOpts.VirtualizationType = aws.String(config.AMIVirtType)
|
||||
}
|
||||
|
||||
if config.AMIVirtType != "hvm" {
|
||||
registerOpts.KernelID = image.KernelID
|
||||
registerOpts.RAMDiskID = image.RAMDiskID
|
||||
}
|
||||
|
||||
return registerOpts
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
)
|
||||
|
||||
func testImage() ec2.Image {
|
||||
return ec2.Image{
|
||||
ImageID: aws.String("ami-abcd1234"),
|
||||
Name: aws.String("ami_test_name"),
|
||||
Architecture: aws.String("x86_64"),
|
||||
KernelID: aws.String("aki-abcd1234"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepRegisterAmi_buildRegisterOpts_pv(t *testing.T) {
|
||||
config := Config{}
|
||||
config.AMIName = "test_ami_name"
|
||||
config.AMIDescription = "test_ami_description"
|
||||
config.AMIVirtType = "paravirtual"
|
||||
|
||||
image := testImage()
|
||||
|
||||
blockDevices := []*ec2.BlockDeviceMapping{}
|
||||
|
||||
opts := buildRegisterOpts(&config, &image, blockDevices)
|
||||
|
||||
expected := config.AMIVirtType
|
||||
if *opts.VirtualizationType != expected {
|
||||
t.Fatalf("Unexpected VirtType value: expected %s got %s\n", expected, *opts.VirtualizationType)
|
||||
}
|
||||
|
||||
expected = config.AMIName
|
||||
if *opts.Name != expected {
|
||||
t.Fatalf("Unexpected Name value: expected %s got %s\n", expected, *opts.Name)
|
||||
}
|
||||
|
||||
expected = *image.KernelID
|
||||
if *opts.KernelID != expected {
|
||||
t.Fatalf("Unexpected KernelId value: expected %s got %s\n", expected, *opts.KernelID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStepRegisterAmi_buildRegisterOpts_hvm(t *testing.T) {
|
||||
config := Config{}
|
||||
config.AMIName = "test_ami_name"
|
||||
config.AMIDescription = "test_ami_description"
|
||||
config.AMIVirtType = "hvm"
|
||||
|
||||
image := testImage()
|
||||
|
||||
blockDevices := []*ec2.BlockDeviceMapping{}
|
||||
|
||||
opts := buildRegisterOpts(&config, &image, blockDevices)
|
||||
|
||||
expected := config.AMIVirtType
|
||||
if *opts.VirtualizationType != expected {
|
||||
t.Fatalf("Unexpected VirtType value: expected %s got %s\n", expected, *opts.VirtualizationType)
|
||||
}
|
||||
|
||||
expected = config.AMIName
|
||||
if *opts.Name != expected {
|
||||
t.Fatalf("Unexpected Name value: expected %s got %s\n", expected, *opts.Name)
|
||||
}
|
||||
|
||||
if opts.KernelID != nil {
|
||||
t.Fatalf("Unexpected KernelId value: expected nil got %s\n", *opts.KernelID)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepSnapshot creates a snapshot of the created volume.
|
||||
//
|
||||
// Produces:
|
||||
// snapshot_id string - ID of the created snapshot
|
||||
type StepSnapshot struct {
|
||||
snapshotId string
|
||||
}
|
||||
|
||||
func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
volumeId := state.Get("volume_id").(string)
|
||||
|
||||
ui.Say("Creating snapshot...")
|
||||
description := fmt.Sprintf("Packer: %s", time.Now().String())
|
||||
|
||||
createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeID: &volumeId,
|
||||
Description: &description,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the snapshot ID so we can delete it later
|
||||
s.snapshotId = *createSnapResp.SnapshotID
|
||||
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
|
||||
|
||||
// Wait for the snapshot to be ready
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
StepState: state,
|
||||
Target: "completed",
|
||||
Refresh: func() (interface{}, string, error) {
|
||||
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{SnapshotIDs: []*string{&s.snapshotId}})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(resp.Snapshots) == 0 {
|
||||
return nil, "", errors.New("No snapshots found.")
|
||||
}
|
||||
|
||||
s := resp.Snapshots[0]
|
||||
return s, *s.State, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("snapshot_id", s.snapshotId)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepSnapshot) Cleanup(state multistep.StateBag) {
|
||||
if s.snapshotId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||
_, halted := state.GetOk(multistep.StateHalted)
|
||||
|
||||
if cancelled || halted {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
ui.Say("Removing snapshot since we cancelled or halted...")
|
||||
_, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{SnapshotID: &s.snapshotId})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Token string `mapstructure:"token"`
|
||||
}
|
||||
|
||||
// Config returns a valid aws.Config object for access to AWS services, or
|
||||
// an error if the authentication and region couldn't be resolved
|
||||
func (c *AccessConfig) Config() (*aws.Config, error) {
|
||||
creds := credentials.NewChainCredentials([]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: c.AccessKey,
|
||||
SecretAccessKey: c.SecretKey,
|
||||
SessionToken: c.Token,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: ""},
|
||||
&ec2rolecreds.EC2RoleProvider{},
|
||||
})
|
||||
|
||||
region, err := c.Region()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: creds,
|
||||
MaxRetries: aws.Int(11),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Region returns the aws.Region object for access to AWS services, requesting
|
||||
// the region from the instance metadata if possible.
|
||||
func (c *AccessConfig) Region() (string, error) {
|
||||
if c.RawRegion != "" {
|
||||
if valid := ValidateRegion(c.RawRegion); valid == false {
|
||||
return "", fmt.Errorf("Not a valid region: %s", c.RawRegion)
|
||||
}
|
||||
return c.RawRegion, nil
|
||||
}
|
||||
|
||||
md, err := GetInstanceMetaData("placement/availability-zone")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
region := strings.TrimRightFunc(string(md), unicode.IsLetter)
|
||||
return region, nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
if c.RawRegion != "" {
|
||||
if valid := ValidateRegion(c.RawRegion); valid == false {
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", c.RawRegion))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetInstanceMetaData(path string) (contents []byte, err error) {
|
||||
url := "http://169.254.169.254/latest/meta-data/" + path
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return []byte(body), err
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAccessConfig() *AccessConfig {
|
||||
return &AccessConfig{}
|
||||
}
|
||||
|
||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
c.RawRegion = ""
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-12"
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-1"
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// AMIConfig is for common configuration related to creating AMIs.
|
||||
type AMIConfig struct {
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIVirtType string `mapstructure:"ami_virtualization_type"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
AMIRegions []string `mapstructure:"ami_regions"`
|
||||
AMITags map[string]string `mapstructure:"tags"`
|
||||
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
|
||||
AMIForceDeregister bool `mapstructure:"force_deregister"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
if c.AMIName == "" {
|
||||
errs = append(errs, fmt.Errorf("ami_name must be specified"))
|
||||
}
|
||||
|
||||
if len(c.AMIRegions) > 0 {
|
||||
regionSet := make(map[string]struct{})
|
||||
regions := make([]string, 0, len(c.AMIRegions))
|
||||
|
||||
for _, region := range c.AMIRegions {
|
||||
// If we already saw the region, then don't look again
|
||||
if _, ok := regionSet[region]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark that we saw the region
|
||||
regionSet[region] = struct{}{}
|
||||
|
||||
// Verify the region is real
|
||||
if valid := ValidateRegion(region); valid == false {
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", region))
|
||||
continue
|
||||
}
|
||||
|
||||
regions = append(regions, region)
|
||||
}
|
||||
|
||||
c.AMIRegions = regions
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAMIConfig() *AMIConfig {
|
||||
return &AMIConfig{
|
||||
AMIName: "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_name(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.AMIName = ""
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_regions(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
c.AMIRegions = nil
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.AMIRegions = []string{"foo"}
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
c.AMIRegions = []string{"us-east-1", "us-west-1", "us-east-1"}
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"us-east-1", "us-west-1"}
|
||||
if !reflect.DeepEqual(c.AMIRegions, expected) {
|
||||
t.Fatalf("bad: %#v", c.AMIRegions)
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built AMIs.
|
||||
type Artifact struct {
|
||||
// A map of regions to AMI IDs.
|
||||
Amis map[string]string
|
||||
|
||||
// BuilderId is the unique ID for the builder that created this AMI
|
||||
BuilderIdValue string
|
||||
|
||||
// EC2 connection for performing API stuff.
|
||||
Conn *ec2.EC2
|
||||
}
|
||||
|
||||
func (a *Artifact) BuilderId() string {
|
||||
return a.BuilderIdValue
|
||||
}
|
||||
|
||||
func (*Artifact) Files() []string {
|
||||
// We have no files
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
parts := make([]string, 0, len(a.Amis))
|
||||
for region, amiId := range a.Amis {
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
|
||||
}
|
||||
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
amiStrings := make([]string, 0, len(a.Amis))
|
||||
for region, id := range a.Amis {
|
||||
single := fmt.Sprintf("%s: %s", region, id)
|
||||
amiStrings = append(amiStrings, single)
|
||||
}
|
||||
|
||||
sort.Strings(amiStrings)
|
||||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
switch name {
|
||||
case "atlas.artifact.metadata":
|
||||
return a.stateAtlasMetadata()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for region, imageId := range a.Amis {
|
||||
log.Printf("Deregistering image ID (%s) from region (%s)", imageId, region)
|
||||
|
||||
regionConfig := &aws.Config{
|
||||
Credentials: a.Conn.Config.Credentials,
|
||||
Region: aws.String(region),
|
||||
}
|
||||
regionConn := ec2.New(regionConfig)
|
||||
|
||||
input := &ec2.DeregisterImageInput{
|
||||
ImageID: &imageId,
|
||||
}
|
||||
if _, err := regionConn.DeregisterImage(input); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// TODO(mitchellh): Delete the snapshots associated with an AMI too
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
if len(errors) == 1 {
|
||||
return errors[0]
|
||||
} else {
|
||||
return &packer.MultiError{Errors: errors}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) stateAtlasMetadata() interface{} {
|
||||
metadata := make(map[string]string)
|
||||
for region, imageId := range a.Amis {
|
||||
k := fmt.Sprintf("region.%s", region)
|
||||
metadata[k] = imageId
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
var _ packer.Artifact = new(Artifact)
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
expected := `east:foo,west:bar`
|
||||
|
||||
amis := make(map[string]string)
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &Artifact{
|
||||
Amis: amis,
|
||||
}
|
||||
|
||||
result := a.Id()
|
||||
if result != expected {
|
||||
t.Fatalf("bad: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactState_atlasMetadata(t *testing.T) {
|
||||
a := &Artifact{
|
||||
Amis: map[string]string{
|
||||
"east": "foo",
|
||||
"west": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
actual := a.State("atlas.artifact.metadata")
|
||||
expected := map[string]string{
|
||||
"region.east": "foo",
|
||||
"region.west": "bar",
|
||||
}
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
expected := `AMIs were created:
|
||||
|
||||
east: foo
|
||||
west: bar`
|
||||
|
||||
amis := make(map[string]string)
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &Artifact{Amis: amis}
|
||||
result := a.String()
|
||||
if result != expected {
|
||||
t.Fatalf("bad: %s", result)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// BlockDevice
|
||||
type BlockDevice struct {
|
||||
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
|
||||
DeviceName string `mapstructure:"device_name"`
|
||||
Encrypted bool `mapstructure:"encrypted"`
|
||||
IOPS int64 `mapstructure:"iops"`
|
||||
NoDevice bool `mapstructure:"no_device"`
|
||||
SnapshotId string `mapstructure:"snapshot_id"`
|
||||
VirtualName string `mapstructure:"virtual_name"`
|
||||
VolumeType string `mapstructure:"volume_type"`
|
||||
VolumeSize int64 `mapstructure:"volume_size"`
|
||||
}
|
||||
|
||||
type BlockDevices struct {
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
|
||||
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings"`
|
||||
}
|
||||
|
||||
func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping {
|
||||
var blockDevices []*ec2.BlockDeviceMapping
|
||||
|
||||
for _, blockDevice := range b {
|
||||
ebsBlockDevice := &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String(blockDevice.VolumeType),
|
||||
VolumeSize: aws.Int64(blockDevice.VolumeSize),
|
||||
DeleteOnTermination: aws.Bool(blockDevice.DeleteOnTermination),
|
||||
}
|
||||
|
||||
// IOPS is only valid for SSD Volumes
|
||||
if blockDevice.VolumeType != "" && blockDevice.VolumeType != "standard" && blockDevice.VolumeType != "gp2" {
|
||||
ebsBlockDevice.IOPS = aws.Int64(blockDevice.IOPS)
|
||||
}
|
||||
|
||||
// You cannot specify Encrypted if you specify a Snapshot ID
|
||||
if blockDevice.SnapshotId != "" {
|
||||
ebsBlockDevice.SnapshotID = aws.String(blockDevice.SnapshotId)
|
||||
} else if blockDevice.Encrypted {
|
||||
ebsBlockDevice.Encrypted = aws.Bool(blockDevice.Encrypted)
|
||||
}
|
||||
|
||||
mapping := &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String(blockDevice.DeviceName),
|
||||
VirtualName: aws.String(blockDevice.VirtualName),
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(blockDevice.VirtualName, "ephemeral") {
|
||||
mapping.EBS = ebsBlockDevice
|
||||
}
|
||||
|
||||
if blockDevice.NoDevice {
|
||||
mapping.NoDevice = aws.String("")
|
||||
}
|
||||
|
||||
blockDevices = append(blockDevices, mapping)
|
||||
}
|
||||
return blockDevices
|
||||
}
|
||||
|
||||
func (b *BlockDevices) Prepare(ctx *interpolate.Context) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildAMIDevices() []*ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.AMIMappings)
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildLaunchDevices() []*ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.LaunchMappings)
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
)
|
||||
|
||||
func TestBlockDevice(t *testing.T) {
|
||||
cases := []struct {
|
||||
Config *BlockDevice
|
||||
Result *ec2.BlockDeviceMapping
|
||||
}{
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
SnapshotId: "snap-1234",
|
||||
VolumeType: "standard",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String(""),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
SnapshotID: aws.String("snap-1234"),
|
||||
VolumeType: aws.String("standard"),
|
||||
VolumeSize: aws.Int64(8),
|
||||
DeleteOnTermination: aws.Bool(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VolumeSize: 8,
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String(""),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String(""),
|
||||
VolumeSize: aws.Int64(8),
|
||||
DeleteOnTermination: aws.Bool(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VolumeType: "io1",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String(""),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String("io1"),
|
||||
VolumeSize: aws.Int64(8),
|
||||
DeleteOnTermination: aws.Bool(true),
|
||||
IOPS: aws.Int64(1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String("ephemeral0"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
blockDevices := BlockDevices{
|
||||
AMIMappings: []BlockDevice{*tc.Config},
|
||||
LaunchMappings: []BlockDevice{*tc.Config},
|
||||
}
|
||||
|
||||
expected := []*ec2.BlockDeviceMapping{tc.Result}
|
||||
got := blockDevices.BuildAMIDevices()
|
||||
if !reflect.DeepEqual(expected, got) {
|
||||
t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s",
|
||||
expected, got)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
|
||||
t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s",
|
||||
expected,
|
||||
blockDevices.BuildLaunchDevices())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package common
|
||||
|
||||
// IsValidRegion returns true if the supplied region is a valid AWS
|
||||
// region and false if it's not.
|
||||
func ValidateRegion(region string) bool {
|
||||
var regions = [11]string{"us-east-1", "us-west-2", "us-west-1", "eu-west-1",
|
||||
"eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1",
|
||||
"sa-east-1", "cn-north-1", "us-gov-west-1"}
|
||||
|
||||
for _, valid := range regions {
|
||||
if region == valid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// RunConfig contains configuration for running an instance from a source
|
||||
// AMI and details on how to access that launched image.
|
||||
type RunConfig struct {
|
||||
AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"`
|
||||
AvailabilityZone string `mapstructure:"availability_zone"`
|
||||
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
RunTags map[string]string `mapstructure:"run_tags"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
SpotPrice string `mapstructure:"spot_price"`
|
||||
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
|
||||
SecurityGroupId string `mapstructure:"security_group_id"`
|
||||
SecurityGroupIds []string `mapstructure:"security_group_ids"`
|
||||
SubnetId string `mapstructure:"subnet_id"`
|
||||
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
|
||||
VpcId string `mapstructure:"vpc_id"`
|
||||
|
||||
// Communicator settings
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
SSHKeyPairName string `mapstructure:"ssh_keypair_name"`
|
||||
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
// if we are not given an explicit keypairname, create a temporary one
|
||||
if c.SSHKeyPairName == "" {
|
||||
c.TemporaryKeyPairName = fmt.Sprintf(
|
||||
"packer %s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
if c.WindowsPasswordTimeout == 0 {
|
||||
c.WindowsPasswordTimeout = 10 * time.Minute
|
||||
}
|
||||
|
||||
// Validation
|
||||
errs := c.Comm.Prepare(ctx)
|
||||
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.SpotPrice == "auto" {
|
||||
if c.SpotPriceAutoProduct == "" {
|
||||
errs = append(errs, errors.New(
|
||||
"spot_price_auto_product must be specified when spot_price is auto"))
|
||||
}
|
||||
}
|
||||
|
||||
if c.UserData != "" && c.UserDataFile != "" {
|
||||
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
|
||||
} else if c.UserDataFile != "" {
|
||||
if _, err := os.Stat(c.UserDataFile); err != nil {
|
||||
errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
|
||||
}
|
||||
}
|
||||
|
||||
if c.SecurityGroupId != "" {
|
||||
if len(c.SecurityGroupIds) > 0 {
|
||||
errs = append(errs, fmt.Errorf("Only one of security_group_id or security_group_ids can be specified."))
|
||||
} else {
|
||||
c.SecurityGroupIds = []string{c.SecurityGroupId}
|
||||
c.SecurityGroupId = ""
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
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",
|
||||
|
||||
Comm: communicator.Config{
|
||||
SSHUsername: "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare(t *testing.T) {
|
||||
c := testConfig()
|
||||
err := c.Prepare(nil)
|
||||
if len(err) > 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.InstanceType = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SourceAmi = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SpotAuto(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SpotPrice = "auto"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.SpotPriceAutoProduct = "foo"
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.Comm.SSHPort = 0
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.Comm.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
|
||||
c.Comm.SSHPort = 44
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.Comm.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.Comm.SSHUsername = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserData(t *testing.T) {
|
||||
c := testConfig()
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserData = "foo"
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserDataFile(t *testing.T) {
|
||||
c := testConfig()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.UserDataFile = "idontexistidontthink"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_TemporaryKeyPairName(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.TemporaryKeyPairName = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
t.Fatal("keypair empty")
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHHost returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the instance DNS name.
|
||||
func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
for j := 0; j < 2; j++ {
|
||||
var host string
|
||||
i := state.Get("instance").(*ec2.Instance)
|
||||
if i.VPCID != nil && *i.VPCID != "" {
|
||||
if i.PublicIPAddress != nil && *i.PublicIPAddress != "" && !private {
|
||||
host = *i.PublicIPAddress
|
||||
} else {
|
||||
host = *i.PrivateIPAddress
|
||||
}
|
||||
} else if i.PublicDNSName != nil && *i.PublicDNSName != "" {
|
||||
host = *i.PublicDNSName
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
return host, nil
|
||||
}
|
||||
|
||||
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
InstanceIDs: []*string{i.InstanceID},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
|
||||
return "", fmt.Errorf("instance not found: %s", *i.InstanceID)
|
||||
}
|
||||
|
||||
state.Put("instance", &r.Reservations[0].Instances[0])
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
return "", errors.New("couldn't determine IP address for instance")
|
||||
}
|
||||
}
|
||||
|
||||
// SSHConfig returns a function that can be used for the SSH communicator
|
||||
// config for connecting to the instance created over SSH using the generated
|
||||
// private key.
|
||||
func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
privateKey := state.Get("privateKey").(string)
|
||||
|
||||
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
}
|
||||
|
||||
return &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||
// responsible for refreshing the item being watched for a state change.
|
||||
//
|
||||
// It returns three results. `result` is any object that will be returned
|
||||
// as the final object after waiting for state change. This allows you to
|
||||
// return the final updated object, for example an EC2 instance after refreshing
|
||||
// it.
|
||||
//
|
||||
// `state` is the latest state of that object. And `err` is any error that
|
||||
// may have happened while refreshing the state.
|
||||
type StateRefreshFunc func() (result interface{}, state string, err error)
|
||||
|
||||
// StateChangeConf is the configuration struct used for `WaitForState`.
|
||||
type StateChangeConf struct {
|
||||
Pending []string
|
||||
Refresh StateRefreshFunc
|
||||
StepState multistep.StateBag
|
||||
Target string
|
||||
}
|
||||
|
||||
// AMIStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an AMI for state changes.
|
||||
func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIDs: []*string{&imageId},
|
||||
})
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on AMIStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.Images) == 0 {
|
||||
// Sometimes AWS has consistency issues and doesn't see the
|
||||
// AMI. Return an empty state.
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
i := resp.Images[0]
|
||||
return i, *i.State, nil
|
||||
}
|
||||
}
|
||||
|
||||
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an EC2 instance.
|
||||
func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
InstanceIDs: []*string{&instanceId},
|
||||
})
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on InstanceStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
|
||||
// Sometimes AWS just has consistency issues and doesn't see
|
||||
// our instance yet. Return an empty state.
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
i := resp.Reservations[0].Instances[0]
|
||||
return i, *i.State.Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// a spot request for state changes.
|
||||
func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
|
||||
SpotInstanceRequestIDs: []*string{&spotRequestId},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on SpotRequestStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.SpotInstanceRequests) == 0 {
|
||||
// Sometimes AWS has consistency issues and doesn't see the
|
||||
// SpotRequest. Return an empty state.
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
i := resp.SpotInstanceRequests[0]
|
||||
return i, *i.State, nil
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForState watches an object and waits for it to achieve a certain
|
||||
// state.
|
||||
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||
log.Printf("Waiting for state to become: %s", conf.Target)
|
||||
|
||||
sleepSeconds := 2
|
||||
maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1
|
||||
notfoundTick := 0
|
||||
|
||||
for {
|
||||
var currentState string
|
||||
i, currentState, err = conf.Refresh()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if i == nil {
|
||||
// If we didn't find the resource, check if we have been
|
||||
// not finding it for awhile, and if so, report an error.
|
||||
notfoundTick += 1
|
||||
if notfoundTick > maxTicks {
|
||||
return nil, errors.New("couldn't find resource")
|
||||
}
|
||||
} else {
|
||||
// Reset the counter for when a resource isn't found
|
||||
notfoundTick = 0
|
||||
|
||||
if currentState == conf.Target {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.StepState != nil {
|
||||
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
|
||||
return nil, errors.New("interrupted")
|
||||
}
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, allowed := range conf.Pending {
|
||||
if currentState == allowed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(sleepSeconds) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func isTransientNetworkError(err error) bool {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns 300 seconds (5 minutes) by default
|
||||
// Some AWS operations, like copying an AMI to a distant region, take a very long time
|
||||
// Allow user to override with AWS_TIMEOUT_SECONDS environment variable
|
||||
func TimeoutSeconds() (seconds int) {
|
||||
seconds = 300
|
||||
|
||||
override := os.Getenv("AWS_TIMEOUT_SECONDS")
|
||||
if override != "" {
|
||||
n, err := strconv.Atoi(override)
|
||||
if err != nil {
|
||||
log.Printf("Invalid timeout seconds '%s', using default", override)
|
||||
} else {
|
||||
seconds = n
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds)
|
||||
return seconds
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepAMIRegionCopy struct {
|
||||
AccessConfig *AccessConfig
|
||||
Regions []string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amis := state.Get("amis").(map[string]string)
|
||||
ami := amis[*ec2conn.Config.Region]
|
||||
|
||||
if len(s.Regions) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Copying AMI (%s) to other regions...", ami))
|
||||
|
||||
var lock sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
errs := new(packer.MultiError)
|
||||
for _, region := range s.Regions {
|
||||
if region == *ec2conn.Config.Region {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Avoiding copying AMI to duplicate region %s", region))
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
ui.Message(fmt.Sprintf("Copying to: %s", region))
|
||||
|
||||
go func(region string) {
|
||||
defer wg.Done()
|
||||
id, err := amiRegionCopy(state, s.AccessConfig, s.Name, ami, region, *ec2conn.Config.Region)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
amis[region] = id
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
}(region)
|
||||
}
|
||||
|
||||
// TODO(mitchellh): Wait but also allow for cancels to go through...
|
||||
ui.Message("Waiting for all copies to complete...")
|
||||
wg.Wait()
|
||||
|
||||
// If there were errors, show them
|
||||
if len(errs.Errors) > 0 {
|
||||
state.Put("error", errs)
|
||||
ui.Error(errs.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("amis", amis)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
||||
|
||||
// amiRegionCopy does a copy for the given AMI to the target region and
|
||||
// returns the resulting ID or error.
|
||||
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, name string, imageId string,
|
||||
target string, source string) (string, error) {
|
||||
|
||||
// Connect to the region where the AMI will be copied to
|
||||
awsConfig, err := config.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
awsConfig.Region = aws.String(target)
|
||||
|
||||
regionconn := ec2.New(awsConfig)
|
||||
resp, err := regionconn.CopyImage(&ec2.CopyImageInput{
|
||||
SourceRegion: &source,
|
||||
SourceImageID: &imageId,
|
||||
Name: &name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error Copying AMI (%s) to region (%s): %s",
|
||||
imageId, target, err)
|
||||
}
|
||||
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "available",
|
||||
Refresh: AMIStateRefreshFunc(regionconn, *resp.ImageID),
|
||||
StepState: state,
|
||||
}
|
||||
|
||||
if _, err := WaitForState(&stateChange); err != nil {
|
||||
return "", fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s",
|
||||
*resp.ImageID, target, err)
|
||||
}
|
||||
|
||||
return *resp.ImageID, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepCreateTags struct {
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amis := state.Get("amis").(map[string]string)
|
||||
|
||||
if len(s.Tags) > 0 {
|
||||
for region, ami := range amis {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
|
||||
|
||||
var ec2Tags []*ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, &ec2.Tag{
|
||||
Key: aws.String(key),
|
||||
Value: aws.String(value),
|
||||
})
|
||||
}
|
||||
|
||||
// Declare list of resources to tag
|
||||
resourceIds := []*string{&ami}
|
||||
|
||||
regionconn := ec2.New(&aws.Config{
|
||||
Credentials: ec2conn.Config.Credentials,
|
||||
Region: aws.String(region),
|
||||
})
|
||||
|
||||
// Retrieve image list for given AMI
|
||||
imageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIDs: resourceIds,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(imageResp.Images) == 0 {
|
||||
err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
image := imageResp.Images[0]
|
||||
|
||||
// Add only those with a Snapshot ID, i.e. not Ephemeral
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if device.EBS != nil && device.EBS.SnapshotID != nil {
|
||||
ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.EBS.SnapshotID))
|
||||
resourceIds = append(resourceIds, device.EBS.SnapshotID)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = regionconn.CreateTags(&ec2.CreateTagsInput{
|
||||
Resources: resourceIds,
|
||||
Tags: ec2Tags,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCreateTags) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepDeregisterAMI struct {
|
||||
ForceDeregister bool
|
||||
AMIName string
|
||||
}
|
||||
|
||||
func (s *StepDeregisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// check for force deregister
|
||||
if s.ForceDeregister {
|
||||
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
Filters: []*ec2.Filter{&ec2.Filter{
|
||||
Name: aws.String("name"),
|
||||
Values: []*string{aws.String(s.AMIName)},
|
||||
}}})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// deregister image(s) by that name
|
||||
for _, i := range resp.Images {
|
||||
_, err := ec2conn.DeregisterImage(&ec2.DeregisterImageInput{
|
||||
ImageID: i.ImageID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error deregistering existing AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
ui.Say(fmt.Sprintf("Deregistered AMI %s, id: %s", s.AMIName, *i.ImageID))
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepDeregisterAMI) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepGetPassword reads the password from a Windows server and sets it
|
||||
// on the WinRM config.
|
||||
type StepGetPassword struct {
|
||||
Debug bool
|
||||
Comm *communicator.Config
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Skip if we're not using winrm
|
||||
if s.Comm.Type != "winrm" {
|
||||
log.Printf("[INFO] Not using winrm communicator, skipping get password...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// If we already have a password, skip it
|
||||
if s.Comm.WinRMPassword != "" {
|
||||
ui.Say("Skipping waiting for password since WinRM password set...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Get the password
|
||||
var password string
|
||||
var err error
|
||||
cancel := make(chan struct{})
|
||||
waitDone := make(chan bool, 1)
|
||||
go func() {
|
||||
ui.Say("Waiting for auto-generated password for instance...")
|
||||
ui.Message(
|
||||
"It is normal for this process to take up to 15 minutes,\n" +
|
||||
"but it usually takes around 5. Please wait.")
|
||||
password, err = s.waitForPassword(state, cancel)
|
||||
waitDone <- true
|
||||
}()
|
||||
|
||||
timeout := time.After(s.Timeout)
|
||||
WaitLoop:
|
||||
for {
|
||||
// Wait for either SSH to become available, a timeout to occur,
|
||||
// or an interrupt to come through.
|
||||
select {
|
||||
case <-waitDone:
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error waiting for password: %s", err))
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf(" \nPassword retrieved!"))
|
||||
s.Comm.WinRMPassword = password
|
||||
break WaitLoop
|
||||
case <-timeout:
|
||||
err := fmt.Errorf("Timeout waiting for password.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
close(cancel)
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
// The step sequence was cancelled, so cancel waiting for password
|
||||
// and just start the halting process.
|
||||
close(cancel)
|
||||
log.Println("[WARN] Interrupt detected, quitting waiting for password.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In debug-mode, we output the password
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Password (since debug is enabled): %s", s.Comm.WinRMPassword))
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepGetPassword) Cleanup(multistep.StateBag) {}
|
||||
|
||||
func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-chan struct{}) (string, error) {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
privateKey := state.Get("privateKey").(string)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancel:
|
||||
log.Println("[INFO] Retrieve password wait cancelled. Exiting loop.")
|
||||
return "", errors.New("Retrieve password wait cancelled")
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
|
||||
resp, err := ec2conn.GetPasswordData(&ec2.GetPasswordDataInput{
|
||||
InstanceID: instance.InstanceID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving auto-generated instance password: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.PasswordData != nil && *resp.PasswordData != "" {
|
||||
decryptedPassword, err := decryptPasswordDataWithPrivateKey(
|
||||
*resp.PasswordData, []byte(privateKey))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error decrypting auto-generated instance password: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return decryptedPassword, nil
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Password is blank, will retry...")
|
||||
}
|
||||
}
|
||||
|
||||
func decryptPasswordDataWithPrivateKey(passwordData string, pemBytes []byte) (string, error) {
|
||||
encryptedPasswd, err := base64.StdEncoding.DecodeString(passwordData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
var asn1Bytes []byte
|
||||
if _, ok := block.Headers["DEK-Info"]; ok {
|
||||
return "", errors.New("encrypted private key isn't yet supported")
|
||||
/*
|
||||
asn1Bytes, err = x509.DecryptPEMBlock(block, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
asn1Bytes = block.Bytes
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(asn1Bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out, err := rsa.DecryptPKCS1v15(nil, key, encryptedPasswd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepKeyPair struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
TemporaryKeyPairName string
|
||||
KeyPairName string
|
||||
PrivateKeyFile string
|
||||
|
||||
keyName string
|
||||
}
|
||||
|
||||
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||
if s.PrivateKeyFile != "" {
|
||||
privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf(
|
||||
"Error loading configured private key file: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("keyPair", s.KeyPairName)
|
||||
state.Put("privateKey", string(privateKeyBytes))
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating temporary keypair: %s", s.TemporaryKeyPairName))
|
||||
keyResp, err := ec2conn.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: &s.TemporaryKeyPairName})
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the keyname so we know to delete it later
|
||||
s.keyName = s.TemporaryKeyPairName
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state.Put("keyPair", s.keyName)
|
||||
state.Put("privateKey", *keyResp.KeyMaterial)
|
||||
|
||||
// If we're in debug mode, output the private key to the working
|
||||
// directory.
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.Create(s.DebugKeyPath)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write([]byte(*keyResp.KeyMaterial)); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Chmod it so that it is SSH ready
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
|
||||
// If no key name is set, then we never created it, so just return
|
||||
// If we used an SSH private key file, do not go about deleting
|
||||
// keypairs
|
||||
if s.PrivateKeyFile != "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Remove the keypair
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
_, err := ec2conn.DeleteKeyPair(&ec2.DeleteKeyPairInput{KeyName: &s.keyName})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||
}
|
||||
|
||||
// Also remove the physical key if we're debugging.
|
||||
if s.Debug {
|
||||
if err := os.Remove(s.DebugKeyPath); err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error removing debug key '%s': %s", s.DebugKeyPath, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepModifyAMIAttributes struct {
|
||||
Users []string
|
||||
Groups []string
|
||||
ProductCodes []string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amis := state.Get("amis").(map[string]string)
|
||||
|
||||
// Determine if there is any work to do.
|
||||
valid := false
|
||||
valid = valid || s.Description != ""
|
||||
valid = valid || (s.Users != nil && len(s.Users) > 0)
|
||||
valid = valid || (s.Groups != nil && len(s.Groups) > 0)
|
||||
valid = valid || (s.ProductCodes != nil && len(s.ProductCodes) > 0)
|
||||
|
||||
if !valid {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Construct the modify image attribute requests we're going to make.
|
||||
// We need to make each separately since the EC2 API only allows changing
|
||||
// one type at a kind currently.
|
||||
options := make(map[string]*ec2.ModifyImageAttributeInput)
|
||||
if s.Description != "" {
|
||||
options["description"] = &ec2.ModifyImageAttributeInput{
|
||||
Description: &ec2.AttributeValue{Value: &s.Description},
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Groups) > 0 {
|
||||
groups := make([]*string, len(s.Groups))
|
||||
adds := make([]*ec2.LaunchPermission, len(s.Groups))
|
||||
addGroups := &ec2.ModifyImageAttributeInput{
|
||||
LaunchPermission: &ec2.LaunchPermissionModifications{},
|
||||
}
|
||||
|
||||
for i, g := range s.Groups {
|
||||
groups[i] = aws.String(g)
|
||||
adds[i] = &ec2.LaunchPermission{
|
||||
Group: aws.String(g),
|
||||
}
|
||||
}
|
||||
addGroups.UserGroups = groups
|
||||
addGroups.LaunchPermission.Add = adds
|
||||
|
||||
options["groups"] = addGroups
|
||||
}
|
||||
|
||||
if len(s.Users) > 0 {
|
||||
users := make([]*string, len(s.Users))
|
||||
adds := make([]*ec2.LaunchPermission, len(s.Users))
|
||||
for i, u := range s.Users {
|
||||
users[i] = aws.String(u)
|
||||
adds[i] = &ec2.LaunchPermission{UserID: aws.String(u)}
|
||||
}
|
||||
options["users"] = &ec2.ModifyImageAttributeInput{
|
||||
UserIDs: users,
|
||||
LaunchPermission: &ec2.LaunchPermissionModifications{
|
||||
Add: adds,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.ProductCodes) > 0 {
|
||||
codes := make([]*string, len(s.ProductCodes))
|
||||
for i, c := range s.ProductCodes {
|
||||
codes[i] = &c
|
||||
}
|
||||
options["product codes"] = &ec2.ModifyImageAttributeInput{
|
||||
ProductCodes: codes,
|
||||
}
|
||||
}
|
||||
|
||||
for region, ami := range amis {
|
||||
ui.Say(fmt.Sprintf("Modifying attributes on AMI (%s)...", ami))
|
||||
regionconn := ec2.New(&aws.Config{
|
||||
Credentials: ec2conn.Config.Credentials,
|
||||
Region: aws.String(region),
|
||||
})
|
||||
for name, input := range options {
|
||||
ui.Message(fmt.Sprintf("Modifying: %s", name))
|
||||
input.ImageID = &ami
|
||||
_, err := regionconn.ModifyImageAttribute(input)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepPreValidate provides an opportunity to pre-validate any configuration for
|
||||
// the build before actually doing any time consuming work
|
||||
//
|
||||
type StepPreValidate struct {
|
||||
DestAmiName string
|
||||
ForceDeregister bool
|
||||
}
|
||||
|
||||
func (s *StepPreValidate) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
if s.ForceDeregister {
|
||||
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
|
||||
ui.Say("Prevalidating AMI Name...")
|
||||
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
Filters: []*ec2.Filter{&ec2.Filter{
|
||||
Name: aws.String("name"),
|
||||
Values: []*string{aws.String(s.DestAmiName)},
|
||||
}}})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error querying AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(resp.Images) > 0 {
|
||||
err := fmt.Errorf("Error: name conflicts with an existing AMI: %s", *resp.Images[0].ImageID)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPreValidate) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,333 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepRunSourceInstance struct {
|
||||
AssociatePublicIpAddress bool
|
||||
AvailabilityZone string
|
||||
BlockDevices BlockDevices
|
||||
Debug bool
|
||||
ExpectedRootDevice string
|
||||
InstanceType string
|
||||
IamInstanceProfile string
|
||||
SourceAMI string
|
||||
SpotPrice string
|
||||
SpotPriceProduct string
|
||||
SubnetId string
|
||||
Tags map[string]string
|
||||
UserData string
|
||||
UserDataFile string
|
||||
|
||||
instanceId string
|
||||
spotRequest *ec2.SpotInstanceRequest
|
||||
}
|
||||
|
||||
func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
keyName := state.Get("keyPair").(string)
|
||||
tempSecurityGroupIds := state.Get("securityGroupIds").([]string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
securityGroupIds := make([]*string, len(tempSecurityGroupIds))
|
||||
for i, sg := range tempSecurityGroupIds {
|
||||
securityGroupIds[i] = aws.String(sg)
|
||||
}
|
||||
|
||||
userData := s.UserData
|
||||
if s.UserDataFile != "" {
|
||||
contents, err := ioutil.ReadFile(s.UserDataFile)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Test if it is encoded already, and if not, encode it
|
||||
if _, err := base64.StdEncoding.DecodeString(string(contents)); err != nil {
|
||||
log.Printf("[DEBUG] base64 encoding user data...")
|
||||
contents = []byte(base64.StdEncoding.EncodeToString(contents))
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIDs: []*string{&s.SourceAMI},
|
||||
})
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("There was a problem with the source AMI: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(imageResp.Images) != 1 {
|
||||
state.Put("error", fmt.Errorf("The source AMI '%s' could not be found.", s.SourceAMI))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if s.ExpectedRootDevice != "" && *imageResp.Images[0].RootDeviceType != s.ExpectedRootDevice {
|
||||
state.Put("error", fmt.Errorf(
|
||||
"The provided source AMI has an invalid root device type.\n"+
|
||||
"Expected '%s', got '%s'.",
|
||||
s.ExpectedRootDevice, *imageResp.Images[0].RootDeviceType))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
spotPrice := s.SpotPrice
|
||||
availabilityZone := s.AvailabilityZone
|
||||
if spotPrice == "auto" {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Finding spot price for %s %s...",
|
||||
s.SpotPriceProduct, s.InstanceType))
|
||||
|
||||
// Detect the spot price
|
||||
startTime := time.Now().Add(-1 * time.Hour)
|
||||
resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
|
||||
InstanceTypes: []*string{&s.InstanceType},
|
||||
ProductDescriptions: []*string{&s.SpotPriceProduct},
|
||||
AvailabilityZone: &s.AvailabilityZone,
|
||||
StartTime: &startTime,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error finding spot price: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
var price float64
|
||||
for _, history := range resp.SpotPriceHistory {
|
||||
log.Printf("[INFO] Candidate spot price: %s", *history.SpotPrice)
|
||||
current, err := strconv.ParseFloat(*history.SpotPrice, 64)
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error parsing spot price: %s", err)
|
||||
continue
|
||||
}
|
||||
if price == 0 || current < price {
|
||||
price = current
|
||||
if s.AvailabilityZone == "" {
|
||||
availabilityZone = *history.AvailabilityZone
|
||||
}
|
||||
}
|
||||
}
|
||||
if price == 0 {
|
||||
err := fmt.Errorf("No candidate spot prices found!")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
|
||||
}
|
||||
|
||||
var instanceId string
|
||||
|
||||
if spotPrice == "" {
|
||||
runOpts := &ec2.RunInstancesInput{
|
||||
KeyName: &keyName,
|
||||
ImageID: &s.SourceAMI,
|
||||
InstanceType: &s.InstanceType,
|
||||
UserData: &userData,
|
||||
MaxCount: aws.Int64(1),
|
||||
MinCount: aws.Int64(1),
|
||||
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
|
||||
BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
|
||||
Placement: &ec2.Placement{AvailabilityZone: &s.AvailabilityZone},
|
||||
}
|
||||
|
||||
if s.SubnetId != "" && s.AssociatePublicIpAddress {
|
||||
runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
|
||||
&ec2.InstanceNetworkInterfaceSpecification{
|
||||
DeviceIndex: aws.Int64(0),
|
||||
AssociatePublicIPAddress: &s.AssociatePublicIpAddress,
|
||||
SubnetID: &s.SubnetId,
|
||||
Groups: securityGroupIds,
|
||||
DeleteOnTermination: aws.Bool(true),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
runOpts.SubnetID = &s.SubnetId
|
||||
runOpts.SecurityGroupIDs = securityGroupIds
|
||||
}
|
||||
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source instance: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
instanceId = *runResp.Instances[0].InstanceID
|
||||
} else {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Requesting spot instance '%s' for: %s",
|
||||
s.InstanceType, spotPrice))
|
||||
runSpotResp, err := ec2conn.RequestSpotInstances(&ec2.RequestSpotInstancesInput{
|
||||
SpotPrice: &spotPrice,
|
||||
LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
|
||||
KeyName: &keyName,
|
||||
ImageID: &s.SourceAMI,
|
||||
InstanceType: &s.InstanceType,
|
||||
UserData: &userData,
|
||||
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
|
||||
&ec2.InstanceNetworkInterfaceSpecification{
|
||||
DeviceIndex: aws.Int64(0),
|
||||
AssociatePublicIPAddress: &s.AssociatePublicIpAddress,
|
||||
SubnetID: &s.SubnetId,
|
||||
Groups: securityGroupIds,
|
||||
DeleteOnTermination: aws.Bool(true),
|
||||
},
|
||||
},
|
||||
Placement: &ec2.SpotPlacement{
|
||||
AvailabilityZone: &availabilityZone,
|
||||
},
|
||||
BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source spot instance: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.spotRequest = runSpotResp.SpotInstanceRequests[0]
|
||||
|
||||
spotRequestId := s.spotRequest.SpotInstanceRequestID
|
||||
ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"open"},
|
||||
Target: "active",
|
||||
Refresh: SpotRequestStateRefreshFunc(ec2conn, *spotRequestId),
|
||||
StepState: state,
|
||||
}
|
||||
_, err = WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
|
||||
SpotInstanceRequestIDs: []*string{spotRequestId},
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
instanceId = *spotResp.SpotInstanceRequests[0].InstanceID
|
||||
}
|
||||
|
||||
// Set the instance ID so that the cleanup works properly
|
||||
s.instanceId = instanceId
|
||||
|
||||
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
|
||||
ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "running",
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, instanceId),
|
||||
StepState: state,
|
||||
}
|
||||
latestInstance, err := WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
instance := latestInstance.(*ec2.Instance)
|
||||
|
||||
ec2Tags := make([]*ec2.Tag, 1, len(s.Tags)+1)
|
||||
ec2Tags[0] = &ec2.Tag{Key: aws.String("Name"), Value: aws.String("Packer Builder")}
|
||||
for k, v := range s.Tags {
|
||||
ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k), Value: aws.String(v)})
|
||||
}
|
||||
|
||||
_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
|
||||
Tags: ec2Tags,
|
||||
Resources: []*string{instance.InstanceID},
|
||||
})
|
||||
if err != nil {
|
||||
ui.Message(
|
||||
fmt.Sprintf("Failed to tag a Name on the builder instance: %s", err))
|
||||
}
|
||||
|
||||
if s.Debug {
|
||||
if instance.PublicDNSName != nil && *instance.PublicDNSName != "" {
|
||||
ui.Message(fmt.Sprintf("Public DNS: %s", *instance.PublicDNSName))
|
||||
}
|
||||
|
||||
if instance.PublicIPAddress != nil && *instance.PublicIPAddress != "" {
|
||||
ui.Message(fmt.Sprintf("Public IP: %s", *instance.PublicIPAddress))
|
||||
}
|
||||
|
||||
if instance.PrivateIPAddress != nil && *instance.PrivateIPAddress != "" {
|
||||
ui.Message(fmt.Sprintf("Private IP: %s", *instance.PrivateIPAddress))
|
||||
}
|
||||
}
|
||||
|
||||
state.Put("instance", instance)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Cancel the spot request if it exists
|
||||
if s.spotRequest != nil {
|
||||
ui.Say("Cancelling the spot request...")
|
||||
input := &ec2.CancelSpotInstanceRequestsInput{
|
||||
SpotInstanceRequestIDs: []*string{s.spotRequest.SpotInstanceRequestID},
|
||||
}
|
||||
if _, err := ec2conn.CancelSpotInstanceRequests(input); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"active", "open"},
|
||||
Refresh: SpotRequestStateRefreshFunc(ec2conn, *s.spotRequest.SpotInstanceRequestID),
|
||||
Target: "cancelled",
|
||||
}
|
||||
|
||||
WaitForState(&stateChange)
|
||||
|
||||
}
|
||||
|
||||
// Terminate the source instance if it exists
|
||||
if s.instanceId != "" {
|
||||
ui.Say("Terminating the source AWS instance...")
|
||||
if _, err := ec2conn.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIDs: []*string{&s.instanceId}}); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, s.instanceId),
|
||||
Target: "terminated",
|
||||
}
|
||||
|
||||
WaitForState(&stateChange)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepSecurityGroup struct {
|
||||
CommConfig *communicator.Config
|
||||
SecurityGroupIds []string
|
||||
VpcId string
|
||||
|
||||
createdGroupId string
|
||||
}
|
||||
|
||||
func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if len(s.SecurityGroupIds) > 0 {
|
||||
log.Printf("Using specified security groups: %v", s.SecurityGroupIds)
|
||||
state.Put("securityGroupIds", s.SecurityGroupIds)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
port := s.CommConfig.Port()
|
||||
if port == 0 {
|
||||
panic("port must be set to a non-zero value.")
|
||||
}
|
||||
|
||||
// Create the group
|
||||
ui.Say("Creating temporary security group for this instance...")
|
||||
groupName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
|
||||
log.Printf("Temporary group name: %s", groupName)
|
||||
group := &ec2.CreateSecurityGroupInput{
|
||||
GroupName: &groupName,
|
||||
Description: aws.String("Temporary group for Packer"),
|
||||
VPCID: &s.VpcId,
|
||||
}
|
||||
groupResp, err := ec2conn.CreateSecurityGroup(group)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the group ID so we can delete it later
|
||||
s.createdGroupId = *groupResp.GroupID
|
||||
|
||||
// Authorize the SSH access for the security group
|
||||
req := &ec2.AuthorizeSecurityGroupIngressInput{
|
||||
GroupID: groupResp.GroupID,
|
||||
IPProtocol: aws.String("tcp"),
|
||||
FromPort: aws.Int64(int64(port)),
|
||||
ToPort: aws.Int64(int64(port)),
|
||||
CIDRIP: aws.String("0.0.0.0/0"),
|
||||
}
|
||||
|
||||
// We loop and retry this a few times because sometimes the security
|
||||
// group isn't available immediately because AWS resources are eventaully
|
||||
// consistent.
|
||||
ui.Say(fmt.Sprintf(
|
||||
"Authorizing access to port %d the temporary security group...",
|
||||
port))
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ec2conn.AuthorizeSecurityGroupIngress(req)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Error authorizing. Will sleep and retry. %s", err)
|
||||
time.Sleep((time.Duration(i) * time.Second) + 1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary security group: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state.Put("securityGroupIds", []string{s.createdGroupId})
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepSecurityGroup) Cleanup(state multistep.StateBag) {
|
||||
if s.createdGroupId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary security group...")
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ec2conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{GroupID: &s.createdGroupId})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Error deleting security group: %s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepSourceAMIInfo extracts critical information from the source AMI
|
||||
// that is used throughout the AMI creation process.
|
||||
//
|
||||
// Produces:
|
||||
// source_image *ec2.Image - the source AMI info
|
||||
type StepSourceAMIInfo struct {
|
||||
SourceAmi string
|
||||
EnhancedNetworking bool
|
||||
}
|
||||
|
||||
func (s *StepSourceAMIInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Inspecting the source AMI...")
|
||||
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIDs: []*string{&s.SourceAmi}})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error querying AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(imageResp.Images) == 0 {
|
||||
err := fmt.Errorf("Source AMI '%s' was not found!", s.SourceAmi)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
image := imageResp.Images[0]
|
||||
|
||||
// Enhanced Networking (SriovNetSupport) can only be enabled on HVM AMIs.
|
||||
// See http://goo.gl/icuXh5
|
||||
if s.EnhancedNetworking && *image.VirtualizationType != "hvm" {
|
||||
err := fmt.Errorf("Cannot enable enhanced networking, source AMI '%s' is not HVM", s.SourceAmi)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("source_image", image)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepSourceAMIInfo) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,38 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func isalphanumeric(b byte) bool {
|
||||
if '0' <= b && b <= '9' {
|
||||
return true
|
||||
}
|
||||
if 'a' <= b && b <= 'z' {
|
||||
return true
|
||||
}
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Clean up AMI name by replacing invalid characters with "-"
|
||||
func templateCleanAMIName(s string) string {
|
||||
allowed := []byte{'(', ')', ',', '/', '-', '_', ' '}
|
||||
b := []byte(s)
|
||||
newb := make([]byte, len(b))
|
||||
for i, c := range b {
|
||||
if isalphanumeric(c) || bytes.IndexByte(allowed, c) != -1 {
|
||||
newb[i] = c
|
||||
} else {
|
||||
newb[i] = '-'
|
||||
}
|
||||
}
|
||||
return string(newb[:])
|
||||
}
|
||||
|
||||
var TemplateFuncs = template.FuncMap{
|
||||
"clean_ami_name": templateCleanAMIName,
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAMITemplatePrepare_clean(t *testing.T) {
|
||||
origName := "AMZamz09(),/-_:&^ $%"
|
||||
expected := "AMZamz09(),/-_--- --"
|
||||
|
||||
name := templateCleanAMIName(origName)
|
||||
|
||||
if name != expected {
|
||||
t.Fatalf("template names do not match: expected %s got %s\n", expected, name)
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
// The amazonebs package contains a packer.Builder implementation that
|
||||
// builds AMIs for Amazon EC2.
|
||||
//
|
||||
// In general, there are two types of AMIs that can be created: ebs-backed or
|
||||
// instance-store. This builder _only_ builds ebs-backed images.
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
const BuilderId = "mitchellh.amazonebs"
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.BlockDevices `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
b.config.ctx.Funcs = awscommon.TemplateFuncs
|
||||
err := config.Decode(&b.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &b.config.ctx,
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
var errs *packer.MultiError
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.BlockDevices.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
config, err := b.config.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2conn := ec2.New(config)
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.SSHKeyPairName,
|
||||
TemporaryKeyPairName: b.config.TemporaryKeyPairName,
|
||||
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
SecurityGroupIds: b.config.SecurityGroupIds,
|
||||
CommConfig: &b.config.RunConfig.Comm,
|
||||
VpcId: b.config.VpcId,
|
||||
},
|
||||
&stepCleanupVolumes{
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
ExpectedRootDevice: "ebs",
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
InstanceType: b.config.InstanceType,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
SubnetId: b.config.SubnetId,
|
||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||
AvailabilityZone: b.config.AvailabilityZone,
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
Tags: b.config.RunTags,
|
||||
},
|
||||
&awscommon.StepGetPassword{
|
||||
Debug: b.config.PackerDebug,
|
||||
Comm: &b.config.RunConfig.Comm,
|
||||
Timeout: b.config.WindowsPasswordTimeout,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.RunConfig.Comm,
|
||||
Host: awscommon.SSHHost(
|
||||
ec2conn,
|
||||
b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(
|
||||
b.config.RunConfig.Comm.SSHUsername),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{SpotPrice: b.config.SpotPrice},
|
||||
// TODO(mitchellh): verify works with spots
|
||||
&stepModifyInstance{},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If there are no AMIs, then just return
|
||||
if _, ok := state.GetOk("amis"); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &awscommon.Artifact{
|
||||
Amis: state.Get("amis").(map[string]string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Conn: ec2conn,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/packer/builder/amazon/common"
|
||||
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilderAcc_basic(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccBasic,
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_regionCopy(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccRegionCopy,
|
||||
Check: checkRegionCopy([]string{"us-east-1", "us-west-2"}),
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_forceDeregister(t *testing.T) {
|
||||
// Build the same AMI name twice, with force_deregister on the second run
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: buildForceDeregisterConfig("false", "dereg"),
|
||||
SkipArtifactTeardown: true,
|
||||
})
|
||||
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: buildForceDeregisterConfig("true", "dereg"),
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_amiSharing(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccSharing,
|
||||
Check: checkAMISharing(2, "932021504756", "all"),
|
||||
})
|
||||
}
|
||||
|
||||
func checkAMISharing(count int, uid, group string) builderT.TestCheckFunc {
|
||||
return func(artifacts []packer.Artifact) error {
|
||||
if len(artifacts) > 1 {
|
||||
return fmt.Errorf("more than 1 artifact")
|
||||
}
|
||||
|
||||
// Get the actual *Artifact pointer so we can access the AMIs directly
|
||||
artifactRaw := artifacts[0]
|
||||
artifact, ok := artifactRaw.(*common.Artifact)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
|
||||
}
|
||||
|
||||
// describe the image, get block devices with a snapshot
|
||||
ec2conn, _ := testEC2Conn()
|
||||
imageResp, err := ec2conn.DescribeImageAttribute(&ec2.DescribeImageAttributeInput{
|
||||
Attribute: aws.String("launchPermission"),
|
||||
ImageID: aws.String(artifact.Amis["us-east-1"]),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving Image Attributes for AMI Artifact (%#v) in AMI Sharing Test: %s", artifact, err)
|
||||
}
|
||||
|
||||
// Launch Permissions are in addition to the userid that created it, so if
|
||||
// you add 3 additional ami_users, you expect 2 Launch Permissions here
|
||||
if len(imageResp.LaunchPermissions) != count {
|
||||
return fmt.Errorf("Error in Image Attributes, expected (%d) Launch Permissions, got (%d)", count, len(imageResp.LaunchPermissions))
|
||||
}
|
||||
|
||||
userFound := false
|
||||
for _, lp := range imageResp.LaunchPermissions {
|
||||
if lp.UserID != nil && uid == *lp.UserID {
|
||||
userFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !userFound {
|
||||
return fmt.Errorf("Error in Image Attributes, expected User ID (%s) to have Launch Permissions, but was not found", uid)
|
||||
}
|
||||
|
||||
groupFound := false
|
||||
for _, lp := range imageResp.LaunchPermissions {
|
||||
if lp.Group != nil && group == *lp.Group {
|
||||
groupFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !groupFound {
|
||||
return fmt.Errorf("Error in Image Attributes, expected Group ID (%s) to have Launch Permissions, but was not found", group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkRegionCopy(regions []string) builderT.TestCheckFunc {
|
||||
return func(artifacts []packer.Artifact) error {
|
||||
if len(artifacts) > 1 {
|
||||
return fmt.Errorf("more than 1 artifact")
|
||||
}
|
||||
|
||||
// Get the actual *Artifact pointer so we can access the AMIs directly
|
||||
artifactRaw := artifacts[0]
|
||||
artifact, ok := artifactRaw.(*common.Artifact)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
|
||||
}
|
||||
|
||||
// Verify that we copied to only the regions given
|
||||
regionSet := make(map[string]struct{})
|
||||
for _, r := range regions {
|
||||
regionSet[r] = struct{}{}
|
||||
}
|
||||
for r, _ := range artifact.Amis {
|
||||
if _, ok := regionSet[r]; !ok {
|
||||
return fmt.Errorf("unknown region: %s", r)
|
||||
}
|
||||
|
||||
delete(regionSet, r)
|
||||
}
|
||||
if len(regionSet) > 0 {
|
||||
return fmt.Errorf("didn't copy to: %#v", regionSet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" {
|
||||
t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests")
|
||||
}
|
||||
|
||||
if v := os.Getenv("AWS_SECRET_ACCESS_KEY"); v == "" {
|
||||
t.Fatal("AWS_SECRET_ACCESS_KEY must be set for acceptance tests")
|
||||
}
|
||||
}
|
||||
|
||||
func testEC2Conn() (*ec2.EC2, error) {
|
||||
access := &common.AccessConfig{RawRegion: "us-east-1"}
|
||||
config, err := access.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ec2.New(config), nil
|
||||
}
|
||||
|
||||
const testBuilderAccBasic = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-test {{timestamp}}"
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
const testBuilderAccRegionCopy = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-test {{timestamp}}",
|
||||
"ami_regions": ["us-east-1", "us-west-2"]
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
const testBuilderAccForceDeregister = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"force_deregister": "%s",
|
||||
"ami_name": "packer-test-%s"
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
// share with catsby
|
||||
const testBuilderAccSharing = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-test {{timestamp}}",
|
||||
"ami_users":["932021504756"],
|
||||
"ami_groups":["all"]
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
func buildForceDeregisterConfig(name, flag string) string {
|
||||
return fmt.Sprintf(testBuilderAccForceDeregister, name, flag)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"access_key": "foo",
|
||||
"secret_key": "bar",
|
||||
"source_ami": "foo",
|
||||
"instance_type": "foo",
|
||||
"region": "us-east-1",
|
||||
"ssh_username": "root",
|
||||
"ami_name": "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Prepare_BadType(t *testing.T) {
|
||||
b := &Builder{}
|
||||
c := map[string]interface{}{
|
||||
"access_key": []string{},
|
||||
}
|
||||
|
||||
warnings, err := b.Prepare(c)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ami_name"] = "foo"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ami_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ami_name")
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Add a random key
|
||||
config["i_should_not_be_valid"] = true
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// stepCleanupVolumes cleans up any orphaned volumes that were not designated to
|
||||
// remain after termination of the instance. These volumes are typically ones
|
||||
// that are marked as "delete on terminate:false" in the source_ami of a build.
|
||||
type stepCleanupVolumes struct {
|
||||
BlockDevices common.BlockDevices
|
||||
}
|
||||
|
||||
func (s *stepCleanupVolumes) Run(state multistep.StateBag) multistep.StepAction {
|
||||
// stepCleanupVolumes is for Cleanup only
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCleanupVolumes) Cleanup(state multistep.StateBag) {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instanceRaw := state.Get("instance")
|
||||
var instance *ec2.Instance
|
||||
if instanceRaw != nil {
|
||||
instance = instanceRaw.(*ec2.Instance)
|
||||
}
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amisRaw := state.Get("amis")
|
||||
if amisRaw == nil {
|
||||
ui.Say("No AMIs to cleanup")
|
||||
return
|
||||
}
|
||||
|
||||
if instance == nil {
|
||||
ui.Say("No volumes to clean up, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("Cleaning up any extra volumes...")
|
||||
|
||||
// We don't actually care about the value here, but we need Set behavior
|
||||
save := make(map[string]struct{})
|
||||
for _, b := range s.BlockDevices.AMIMappings {
|
||||
if !b.DeleteOnTermination {
|
||||
save[b.DeviceName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range s.BlockDevices.LaunchMappings {
|
||||
if !b.DeleteOnTermination {
|
||||
save[b.DeviceName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect Volume information from the cached Instance as a map of volume-id
|
||||
// to device name, to compare with save list above
|
||||
var vl []*string
|
||||
volList := make(map[string]string)
|
||||
for _, bdm := range instance.BlockDeviceMappings {
|
||||
if bdm.EBS != nil {
|
||||
vl = append(vl, bdm.EBS.VolumeID)
|
||||
volList[*bdm.EBS.VolumeID] = *bdm.DeviceName
|
||||
}
|
||||
}
|
||||
|
||||
// Using the volume list from the cached Instance, check with AWS for up to
|
||||
// date information on them
|
||||
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{
|
||||
Filters: []*ec2.Filter{
|
||||
&ec2.Filter{
|
||||
Name: aws.String("volume-id"),
|
||||
Values: vl,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ui.Say(fmt.Sprintf("Error describing volumes: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
// If any of the returned volumes are in a "deleting" stage or otherwise not
|
||||
// available, remove them from the list of volumes
|
||||
for _, v := range resp.Volumes {
|
||||
if v.State != nil && *v.State != "available" {
|
||||
delete(volList, *v.VolumeID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(resp.Volumes) == 0 {
|
||||
ui.Say("No volumes to clean up, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out any devices marked for saving
|
||||
for saveName, _ := range save {
|
||||
for volKey, volName := range volList {
|
||||
if volName == saveName {
|
||||
delete(volList, volKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy remaining volumes
|
||||
for k, _ := range volList {
|
||||
ui.Say(fmt.Sprintf("Destroying volume (%s)...", k))
|
||||
_, err := ec2conn.DeleteVolume(&ec2.DeleteVolumeInput{VolumeID: aws.String(k)})
|
||||
if err != nil {
|
||||
ui.Say(fmt.Sprintf("Error deleting volume: %s", k))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct {
|
||||
image *ec2.Image
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
|
||||
createOpts := &ec2.CreateImageInput{
|
||||
InstanceID: instance.InstanceID,
|
||||
Name: &config.AMIName,
|
||||
BlockDeviceMappings: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the AMI ID in the state
|
||||
ui.Message(fmt.Sprintf("AMI: %s", *createResp.ImageID))
|
||||
amis := make(map[string]string)
|
||||
amis[*ec2conn.Config.Region] = *createResp.ImageID
|
||||
state.Put("amis", amis)
|
||||
|
||||
// Wait for the image to become ready
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "available",
|
||||
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *createResp.ImageID),
|
||||
StepState: state,
|
||||
}
|
||||
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
if _, err := awscommon.WaitForState(&stateChange); err != nil {
|
||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
imagesResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIDs: []*string{createResp.ImageID}})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error searching for AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
s.image = imagesResp.Images[0]
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Cleanup(state multistep.StateBag) {
|
||||
if s.image == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||
_, halted := state.GetOk(multistep.StateHalted)
|
||||
if !cancelled && !halted {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Deregistering the AMI because cancelation or error...")
|
||||
deregisterOpts := &ec2.DeregisterImageInput{ImageID: s.image.ImageID}
|
||||
if _, err := ec2conn.DeregisterImage(deregisterOpts); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepModifyInstance struct{}
|
||||
|
||||
func (s *stepModifyInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
ui.Say("Enabling Enhanced Networking...")
|
||||
simple := "simple"
|
||||
_, err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
|
||||
InstanceID: instance.InstanceID,
|
||||
SRIOVNetSupport: &ec2.AttributeValue{Value: &simple},
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error enabling Enhanced Networking on %s: %s", *instance.InstanceID, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepModifyInstance) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepStopInstance struct {
|
||||
SpotPrice string
|
||||
}
|
||||
|
||||
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Skip when it is a spot instance
|
||||
if s.SpotPrice != "" {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Stop the instance so we can create an AMI from it
|
||||
ui.Say("Stopping the source instance...")
|
||||
_, err := ec2conn.StopInstances(&ec2.StopInstancesInput{
|
||||
InstanceIDs: []*string{instance.InstanceID},
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error stopping instance: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the instance to actual stop
|
||||
ui.Say("Waiting for the instance to stop...")
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"running", "stopping"},
|
||||
Target: "stopped",
|
||||
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, *instance.InstanceID),
|
||||
StepState: state,
|
||||
}
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepStopInstance) Cleanup(multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/packer/builder/amazon/common"
|
||||
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilderTagsAcc_basic(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderTagsAccBasic,
|
||||
Check: checkTags(),
|
||||
})
|
||||
}
|
||||
|
||||
func checkTags() builderT.TestCheckFunc {
|
||||
return func(artifacts []packer.Artifact) error {
|
||||
if len(artifacts) > 1 {
|
||||
return fmt.Errorf("more than 1 artifact")
|
||||
}
|
||||
|
||||
tags := make(map[string]string)
|
||||
tags["OS_Version"] = "Ubuntu"
|
||||
tags["Release"] = "Latest"
|
||||
|
||||
// Get the actual *Artifact pointer so we can access the AMIs directly
|
||||
artifactRaw := artifacts[0]
|
||||
artifact, ok := artifactRaw.(*common.Artifact)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
|
||||
}
|
||||
|
||||
// describe the image, get block devices with a snapshot
|
||||
ec2conn, _ := testEC2Conn()
|
||||
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIDs: []*string{aws.String(artifact.Amis["us-east-1"])},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving details for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||
}
|
||||
|
||||
if len(imageResp.Images) == 0 {
|
||||
return fmt.Errorf("No images found for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||
}
|
||||
|
||||
image := imageResp.Images[0]
|
||||
|
||||
// Check only those with a Snapshot ID, i.e. not Ephemeral
|
||||
var snapshots []*string
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if device.EBS != nil && device.EBS.SnapshotID != nil {
|
||||
snapshots = append(snapshots, device.EBS.SnapshotID)
|
||||
}
|
||||
}
|
||||
|
||||
// grab matching snapshot info
|
||||
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIDs: snapshots,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retreiving Snapshots for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||
}
|
||||
|
||||
if len(resp.Snapshots) == 0 {
|
||||
return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact)
|
||||
}
|
||||
|
||||
// grab the snapshots, check the tags
|
||||
for _, s := range resp.Snapshots {
|
||||
expected := len(tags)
|
||||
for _, t := range s.Tags {
|
||||
for key, value := range tags {
|
||||
if key == *t.Key && value == *t.Value {
|
||||
expected--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expected > 0 {
|
||||
return fmt.Errorf("Not all tags found")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
const testBuilderTagsAccBasic = `
|
||||
{
|
||||
"builders": [
|
||||
{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"source_ami": "ami-9eaa1cf6",
|
||||
"instance_type": "t2.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-tags-testing-{{timestamp}}",
|
||||
"tags": {
|
||||
"OS_Version": "Ubuntu",
|
||||
"Release": "Latest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
@@ -1,287 +0,0 @@
|
||||
// The instance package contains a packer.Builder implementation that builds
|
||||
// AMIs for Amazon EC2 backed by instance storage, as opposed to EBS storage.
|
||||
package instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
const BuilderId = "mitchellh.amazon.instance"
|
||||
|
||||
// Config is the configuration that is chained through the steps and
|
||||
// settable from the template.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.BlockDevices `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
AccountId string `mapstructure:"account_id"`
|
||||
BundleDestination string `mapstructure:"bundle_destination"`
|
||||
BundlePrefix string `mapstructure:"bundle_prefix"`
|
||||
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
|
||||
BundleVolCommand string `mapstructure:"bundle_vol_command"`
|
||||
S3Bucket string `mapstructure:"s3_bucket"`
|
||||
X509CertPath string `mapstructure:"x509_cert_path"`
|
||||
X509KeyPath string `mapstructure:"x509_key_path"`
|
||||
X509UploadPath string `mapstructure:"x509_upload_path"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
configs := make([]interface{}, len(raws)+1)
|
||||
configs[0] = map[string]interface{}{
|
||||
"bundle_prefix": "image-{{timestamp}}",
|
||||
}
|
||||
copy(configs[1:], raws)
|
||||
|
||||
b.config.ctx.Funcs = awscommon.TemplateFuncs
|
||||
err := config.Decode(&b.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &b.config.ctx,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"bundle_upload_command",
|
||||
"bundle_vol_command",
|
||||
},
|
||||
},
|
||||
}, configs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.config.BundleDestination == "" {
|
||||
b.config.BundleDestination = "/tmp"
|
||||
}
|
||||
|
||||
if b.config.BundleUploadCommand == "" {
|
||||
if b.config.IamInstanceProfile != "" {
|
||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||
"-b {{.BucketName}} " +
|
||||
"-m {{.ManifestPath}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--region {{.Region}} " +
|
||||
"--retry"
|
||||
} else {
|
||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||
"-b {{.BucketName}} " +
|
||||
"-m {{.ManifestPath}} " +
|
||||
"-a {{.AccessKey}} " +
|
||||
"-s {{.SecretKey}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--region {{.Region}} " +
|
||||
"--retry"
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.BundleVolCommand == "" {
|
||||
b.config.BundleVolCommand = "sudo -i -n ec2-bundle-vol " +
|
||||
"-k {{.KeyPath}} " +
|
||||
"-u {{.AccountId}} " +
|
||||
"-c {{.CertPath}} " +
|
||||
"-r {{.Architecture}} " +
|
||||
"-e {{.PrivatePath}}/* " +
|
||||
"-d {{.Destination}} " +
|
||||
"-p {{.Prefix}} " +
|
||||
"--batch " +
|
||||
"--no-filter"
|
||||
}
|
||||
|
||||
if b.config.X509UploadPath == "" {
|
||||
b.config.X509UploadPath = "/tmp"
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
var errs *packer.MultiError
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.BlockDevices.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
if b.config.AccountId == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
|
||||
} else {
|
||||
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
|
||||
}
|
||||
|
||||
if b.config.S3Bucket == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
|
||||
}
|
||||
|
||||
if b.config.X509CertPath == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("x509_cert_path is required"))
|
||||
} else if _, err := os.Stat(b.config.X509CertPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("x509_cert_path points to bad file: %s", err))
|
||||
}
|
||||
|
||||
if b.config.X509KeyPath == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("x509_key_path is required"))
|
||||
} else if _, err := os.Stat(b.config.X509KeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("x509_key_path points to bad file: %s", err))
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
config, err := b.config.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2conn := ec2.New(config)
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.SSHKeyPairName,
|
||||
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
|
||||
TemporaryKeyPairName: b.config.TemporaryKeyPairName,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
CommConfig: &b.config.RunConfig.Comm,
|
||||
SecurityGroupIds: b.config.SecurityGroupIds,
|
||||
VpcId: b.config.VpcId,
|
||||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
InstanceType: b.config.InstanceType,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
SubnetId: b.config.SubnetId,
|
||||
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
|
||||
AvailabilityZone: b.config.AvailabilityZone,
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
Tags: b.config.RunTags,
|
||||
},
|
||||
&awscommon.StepGetPassword{
|
||||
Debug: b.config.PackerDebug,
|
||||
Comm: &b.config.RunConfig.Comm,
|
||||
Timeout: b.config.WindowsPasswordTimeout,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.RunConfig.Comm,
|
||||
Host: awscommon.SSHHost(
|
||||
ec2conn,
|
||||
b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(
|
||||
b.config.RunConfig.Comm.SSHUsername),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&StepUploadX509Cert{},
|
||||
&StepBundleVolume{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&StepUploadBundle{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
ProductCodes: b.config.AMIProductCodes,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If there are no AMIs, then just return
|
||||
if _, ok := state.GetOk("amis"); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &awscommon.Artifact{
|
||||
Amis: state.Get("amis").(map[string]string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Conn: ec2conn,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"account_id": "foo",
|
||||
"ami_name": "foo",
|
||||
"instance_type": "m1.small",
|
||||
"region": "us-east-1",
|
||||
"s3_bucket": "foo",
|
||||
"source_ami": "foo",
|
||||
"ssh_username": "bob",
|
||||
"x509_cert_path": tf.Name(),
|
||||
"x509_key_path": tf.Name(),
|
||||
"x509_upload_path": "/foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AccountId(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["account_id"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["account_id"] = "foo"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
|
||||
config["account_id"] = "0123-0456-7890"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.AccountId != "012304567890" {
|
||||
t.Errorf("should strip hyphens: %s", b.config.AccountId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ami_name"] = "foo"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ami_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ami_name")
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_BundleDestination(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["bundle_destination"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.BundleDestination != "/tmp" {
|
||||
t.Fatalf("bad: %s", b.config.BundleDestination)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_BundlePrefix(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.BundlePrefix == "" {
|
||||
t.Fatalf("bad: %s", b.config.BundlePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Add a random key
|
||||
config["i_should_not_be_valid"] = true
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_S3Bucket(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["s3_bucket"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["s3_bucket"] = "foo"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_X509CertPath(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["x509_cert_path"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["x509_cert_path"] = "i/am/a/file/that/doesnt/exist"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("should have error")
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config["x509_cert_path"] = tf.Name()
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_X509KeyPath(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["x509_key_path"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["x509_key_path"] = "i/am/a/file/that/doesnt/exist"
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("should have error")
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config["x509_key_path"] = tf.Name()
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_X509UploadPath(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["x509_upload_path"] = ""
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
type bundleCmdData struct {
|
||||
AccountId string
|
||||
Architecture string
|
||||
CertPath string
|
||||
Destination string
|
||||
KeyPath string
|
||||
Prefix string
|
||||
PrivatePath string
|
||||
}
|
||||
|
||||
type StepBundleVolume struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func (s *StepBundleVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
x509RemoteCertPath := state.Get("x509RemoteCertPath").(string)
|
||||
x509RemoteKeyPath := state.Get("x509RemoteKeyPath").(string)
|
||||
|
||||
// Bundle the volume
|
||||
var err error
|
||||
config.ctx.Data = bundleCmdData{
|
||||
AccountId: config.AccountId,
|
||||
Architecture: *instance.Architecture,
|
||||
CertPath: x509RemoteCertPath,
|
||||
Destination: config.BundleDestination,
|
||||
KeyPath: x509RemoteKeyPath,
|
||||
Prefix: config.BundlePrefix,
|
||||
PrivatePath: config.X509UploadPath,
|
||||
}
|
||||
config.BundleVolCommand, err = interpolate.Render(config.BundleVolCommand, &config.ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle volume command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Bundling the volume...")
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = config.BundleVolCommand
|
||||
|
||||
if s.Debug {
|
||||
ui.Say(fmt.Sprintf("Running: %s", config.BundleVolCommand))
|
||||
}
|
||||
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error bundling volume: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
state.Put("error", fmt.Errorf(
|
||||
"Volume bundling failed. Please see the output above for more\n"+
|
||||
"details on what went wrong."))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Store the manifest path
|
||||
manifestName := config.BundlePrefix + ".manifest.xml"
|
||||
state.Put("manifest_name", manifestName)
|
||||
state.Put("manifest_path", fmt.Sprintf(
|
||||
"%s/%s", config.BundleDestination, manifestName))
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepBundleVolume) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,69 +0,0 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepRegisterAMI struct{}
|
||||
|
||||
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
manifestPath := state.Get("remote_manifest_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
registerOpts := &ec2.RegisterImageInput{
|
||||
ImageLocation: &manifestPath,
|
||||
Name: aws.String(config.AMIName),
|
||||
BlockDeviceMappings: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
if config.AMIVirtType != "" {
|
||||
registerOpts.VirtualizationType = aws.String(config.AMIVirtType)
|
||||
}
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
registerOpts.SRIOVNetSupport = aws.String("simple")
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error registering AMI: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the AMI ID in the state
|
||||
ui.Say(fmt.Sprintf("AMI: %s", *registerResp.ImageID))
|
||||
amis := make(map[string]string)
|
||||
amis[*ec2conn.Config.Region] = *registerResp.ImageID
|
||||
state.Put("amis", amis)
|
||||
|
||||
// Wait for the image to become ready
|
||||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "available",
|
||||
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *registerResp.ImageID),
|
||||
StepState: state,
|
||||
}
|
||||
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
if _, err := awscommon.WaitForState(&stateChange); err != nil {
|
||||
err := fmt.Errorf("Error waiting for AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepRegisterAMI) Cleanup(multistep.StateBag) {}
|
||||
@@ -1,82 +0,0 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
type uploadCmdData struct {
|
||||
AccessKey string
|
||||
BucketName string
|
||||
BundleDirectory string
|
||||
ManifestPath string
|
||||
Region string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
type StepUploadBundle struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func (s *StepUploadBundle) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
manifestName := state.Get("manifest_name").(string)
|
||||
manifestPath := state.Get("manifest_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
region, err := config.Region()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving region: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
config.ctx.Data = uploadCmdData{
|
||||
AccessKey: config.AccessKey,
|
||||
BucketName: config.S3Bucket,
|
||||
BundleDirectory: config.BundleDestination,
|
||||
ManifestPath: manifestPath,
|
||||
Region: region,
|
||||
SecretKey: config.SecretKey,
|
||||
}
|
||||
config.BundleUploadCommand, err = interpolate.Render(config.BundleUploadCommand, &config.ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle upload command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Uploading the bundle...")
|
||||
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
|
||||
|
||||
if s.Debug {
|
||||
ui.Say(fmt.Sprintf("Running: %s", config.BundleUploadCommand))
|
||||
}
|
||||
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error uploading volume: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
state.Put("error", fmt.Errorf(
|
||||
"Bundle upload failed. Please see the output above for more\n"+
|
||||
"details on what went wrong."))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("remote_manifest_path", fmt.Sprintf(
|
||||
"%s/%s", config.S3Bucket, manifestName))
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepUploadBundle) Cleanup(state multistep.StateBag) {}
|
||||
@@ -1,49 +0,0 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
)
|
||||
|
||||
type StepUploadX509Cert struct{}
|
||||
|
||||
func (s *StepUploadX509Cert) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
x509RemoteCertPath := config.X509UploadPath + "/cert.pem"
|
||||
x509RemoteKeyPath := config.X509UploadPath + "/key.pem"
|
||||
|
||||
ui.Say("Uploading X509 Certificate...")
|
||||
if err := s.uploadSingle(comm, x509RemoteCertPath, config.X509CertPath); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error uploading X509 cert: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err := s.uploadSingle(comm, x509RemoteKeyPath, config.X509KeyPath); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error uploading X509 cert: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("x509RemoteCertPath", x509RemoteCertPath)
|
||||
state.Put("x509RemoteKeyPath", x509RemoteKeyPath)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepUploadX509Cert) Cleanup(multistep.StateBag) {}
|
||||
|
||||
func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src string) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return comm.Upload(dst, f, nil)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type artifact struct {
|
||||
// A map of regions to AMI IDs.
|
||||
amis map[string]string
|
||||
|
||||
// EC2 connection for performing API stuff.
|
||||
conn *ec2.EC2
|
||||
}
|
||||
|
||||
func (*artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
func (*artifact) Files() []string {
|
||||
// We have no files
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *artifact) Id() string {
|
||||
parts := make([]string, 0, len(a.amis))
|
||||
for region, amiId := range a.amis {
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (a *artifact) String() string {
|
||||
amiStrings := make([]string, 0, len(a.amis))
|
||||
for region, id := range a.amis {
|
||||
single := fmt.Sprintf("%s: %s", region, id)
|
||||
amiStrings = append(amiStrings, single)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for _, imageId := range a.amis {
|
||||
log.Printf("Degistering image ID: %s", imageId)
|
||||
if _, err := a.conn.DeregisterImage(imageId); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// TODO(mitchellh): Delete the snapshots associated with an AMI too
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
if len(errors) == 1 {
|
||||
return errors[0]
|
||||
} else {
|
||||
return &packer.MultiError{errors}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
var actual packer.Artifact
|
||||
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
expected := `east:foo,west:bar`
|
||||
|
||||
amis := make(map[string]string)
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &artifact{amis, nil}
|
||||
result := a.Id()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
expected := `AMIs were created:
|
||||
|
||||
east: foo
|
||||
west: bar`
|
||||
|
||||
amis := make(map[string]string)
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &artifact{amis, nil}
|
||||
result := a.String()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// The amazonebs package contains a packer.Builder implementation that
|
||||
// builds AMIs for Amazon EC2.
|
||||
//
|
||||
// In general, there are two types of AMIs that can be created: ebs-backed or
|
||||
// instance-store. This builder _only_ builds ebs-backed images.
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
const BuilderId = "mitchellh.amazonebs"
|
||||
|
||||
type config struct {
|
||||
// Access information
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
|
||||
// Information for the source instance
|
||||
Region string
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SSHTimeout time.Duration
|
||||
|
||||
// Configuration of the resulting AMI
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
var err error
|
||||
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
errs := make([]error, 0)
|
||||
|
||||
if b.config.AccessKey == "" {
|
||||
errs = append(errs, errors.New("An access_key must be specified"))
|
||||
}
|
||||
|
||||
if b.config.SecretKey == "" {
|
||||
errs = append(errs, errors.New("A secret_key must be specified"))
|
||||
}
|
||||
|
||||
if b.config.SourceAmi == "" {
|
||||
errs = append(errs, errors.New("A source_ami must be specified"))
|
||||
}
|
||||
|
||||
if b.config.InstanceType == "" {
|
||||
errs = append(errs, errors.New("An instance_type must be specified"))
|
||||
}
|
||||
|
||||
if b.config.Region == "" {
|
||||
errs = append(errs, errors.New("A region must be specified"))
|
||||
} else if _, ok := aws.Regions[b.config.Region]; !ok {
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", b.config.Region))
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = append(errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &packer.MultiError{errs}
|
||||
}
|
||||
|
||||
log.Printf("Config: %+v", b.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
region, ok := aws.Regions[b.config.Region]
|
||||
if !ok {
|
||||
panic("region not found")
|
||||
}
|
||||
|
||||
auth := aws.Auth{b.config.AccessKey, b.config.SecretKey}
|
||||
ec2conn := ec2.New(auth, region)
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := make(map[string]interface{})
|
||||
state["config"] = b.config
|
||||
state["ec2"] = ec2conn
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&stepKeyPair{},
|
||||
&stepSecurityGroup{},
|
||||
&stepRunSourceInstance{},
|
||||
&stepConnectSSH{},
|
||||
&stepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If there are no AMIs, then just return
|
||||
if _, ok := state["amis"]; !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &artifact{
|
||||
amis: state["amis"].(map[string]string),
|
||||
conn: ec2conn,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"access_key": "foo",
|
||||
"secret_key": "bar",
|
||||
"source_ami": "foo",
|
||||
"instance_type": "foo",
|
||||
"region": "us-east-1",
|
||||
"ssh_username": "root",
|
||||
"ami_name": "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Prepare_BadType(t *testing.T) {
|
||||
b := &Builder{}
|
||||
c := map[string]interface{}{
|
||||
"access_key": []string{},
|
||||
}
|
||||
|
||||
err := b.Prepare(c)
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AccessKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["access_key"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.AccessKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.AccessKey)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "access_key")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ami_name"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ami_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ami_name")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InstanceType(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["instance_type"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.InstanceType != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.InstanceType)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "instance_type")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Region(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["region"] = "us-east-1"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Region != "us-east-1" {
|
||||
t.Errorf("invalid: %s", b.config.Region)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "region")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test invalid
|
||||
config["region"] = "i-am-not-real"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SecretKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["secret_key"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SecretKey != "foo" {
|
||||
t.Errorf("secret key invalid: %s", b.config.SecretKey)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "secret_key")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SourceAmi(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["source_ami"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SourceAmi != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.SourceAmi)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "source_ami")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHPort != 22 {
|
||||
t.Errorf("invalid: %d", b.config.SSHPort)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["ssh_port"] = 35
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHPort != 35 {
|
||||
t.Errorf("invalid: %d", b.config.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test with a bad value
|
||||
config["ssh_timeout"] = "this is not good"
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["ssh_timeout"] = "5s"
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHUsername(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ssh_username"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHUsername != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.SSHUsername)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ssh_username")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package amazonebs
|
||||
|
||||
// This hook is fired prior to launching the EC2 instance.
|
||||
const HookPreLaunch = "amazonebs_pre_launch"
|
||||
@@ -0,0 +1,39 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, pending []string, target string) (i *ec2.Instance, err error) {
|
||||
log.Printf("Waiting for instance state to become: %s", target)
|
||||
|
||||
i = originalInstance
|
||||
for i.State.Name != target {
|
||||
found := false
|
||||
for _, allowed := range pending {
|
||||
if i.State.Name == allowed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, target)
|
||||
return
|
||||
}
|
||||
|
||||
var resp *ec2.InstancesResp
|
||||
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i = &resp.Reservations[0].Instances[0]
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepConnectSSH struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
privateKey := state["privateKey"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Build the keyring for authentication. This stores the private key
|
||||
// we'll use to authenticate.
|
||||
keyring := &ssh.SimpleKeychain{}
|
||||
err := keyring.AddPEMKey(privateKey)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Build the actual SSH client configuration
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
// Start trying to connect to SSH
|
||||
connected := make(chan bool, 1)
|
||||
connectQuit := make(chan bool, 1)
|
||||
defer func() {
|
||||
connectQuit <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
ui.Say("Connecting to the instance via SSH...")
|
||||
attempts := 0
|
||||
for {
|
||||
select {
|
||||
case <-connectQuit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
attempts += 1
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:%d (attempt %d)",
|
||||
instance.DNSName, config.SSHPort, attempts)
|
||||
s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// A brief sleep so we're not being overly zealous attempting
|
||||
// to connect to the instance.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
connected <- true
|
||||
}()
|
||||
|
||||
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
|
||||
timeout := time.After(config.SSHTimeout)
|
||||
|
||||
ConnectWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case <-connected:
|
||||
// We connected. Just break the loop.
|
||||
break ConnectWaitLoop
|
||||
case <-timeout:
|
||||
err := errors.New("Timeout waiting for SSH to become available.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
log.Println("Interrupt detected, quitting waiting for SSH.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var comm packer.Communicator
|
||||
if err == nil {
|
||||
comm, err = ssh.New(s.conn, sshConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error connecting to SSH: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the communicator on the state bag so it can be used later
|
||||
state["communicator"] = comm
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct{}
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", amiName))
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: amiName,
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating AMI: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the AMI ID in the state
|
||||
ui.Say(fmt.Sprintf("AMI: %s", createResp.ImageId))
|
||||
amis := make(map[string]string)
|
||||
amis[config.Region] = createResp.ImageId
|
||||
state["amis"] = amis
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
for {
|
||||
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error querying images: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if imageResp.Images[0].State == "available" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Cleanup(map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepKeyPair struct {
|
||||
keyName string
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := ec2conn.CreateKeyPair(keyName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary keypair: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the keyname so we know to delete it later
|
||||
s.keyName = keyName
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state["keyPair"] = keyName
|
||||
state["privateKey"] = keyResp.KeyMaterial
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
|
||||
// If no key name is set, then we never created it, so just return
|
||||
if s.keyName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
_, err := ec2conn.DeleteKeyPair(s.keyName)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepProvision struct{}
|
||||
|
||||
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
||||
comm := state["communicator"].(packer.Communicator)
|
||||
hook := state["hook"].(packer.Hook)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
log.Println("Running the provision hook")
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (*stepProvision) Cleanup(map[string]interface{}) {}
|
||||
@@ -0,0 +1,73 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepRunSourceInstance struct {
|
||||
instance *ec2.Instance
|
||||
}
|
||||
|
||||
func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
keyName := state["keyPair"].(string)
|
||||
securityGroupId := state["securityGroupId"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
runOpts := &ec2.RunInstances{
|
||||
KeyName: keyName,
|
||||
ImageId: config.SourceAmi,
|
||||
InstanceType: config.InstanceType,
|
||||
MinCount: 0,
|
||||
MaxCount: 0,
|
||||
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source instance: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.instance = &runResp.Instances[0]
|
||||
log.Printf("instance id: %s", s.instance.InstanceId)
|
||||
|
||||
ui.Say("Waiting for instance to become ready...")
|
||||
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance to become ready: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state["instance"] = s.instance
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
|
||||
if s.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Terminating the source AWS instance...")
|
||||
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
pending := []string{"pending", "running", "shutting-down", "stopped", "stopping"}
|
||||
waitForState(ec2conn, s.instance, pending, "terminated")
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepSecurityGroup struct {
|
||||
groupId string
|
||||
}
|
||||
|
||||
func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Create the group
|
||||
ui.Say("Creating temporary security group for this instance...")
|
||||
groupName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
log.Printf("Temporary group name: %s", groupName)
|
||||
groupResp, err := ec2conn.CreateSecurityGroup(groupName, "Temporary group for Packer")
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the group ID so we can delete it later
|
||||
s.groupId = groupResp.Id
|
||||
|
||||
// Authorize the SSH access
|
||||
perms := []ec2.IPPerm{
|
||||
ec2.IPPerm{
|
||||
Protocol: "tcp",
|
||||
FromPort: config.SSHPort,
|
||||
ToPort: config.SSHPort,
|
||||
SourceIPs: []string{"0.0.0.0/0"},
|
||||
},
|
||||
}
|
||||
|
||||
ui.Say("Authorizing SSH access on the temporary security group...")
|
||||
if _, err := ec2conn.AuthorizeSecurityGroup(groupResp.SecurityGroup, perms); err != nil {
|
||||
err := fmt.Errorf("Error creating temporary security group: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state["securityGroupId"] = s.groupId
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
|
||||
if s.groupId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary security group...")
|
||||
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
|
||||
if err != nil {
|
||||
log.Printf("Error deleting security group: %s", err)
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up security group. Please delete the group manually: %s", s.groupId))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepStopInstance struct{}
|
||||
|
||||
func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Stop the instance so we can create an AMI from it
|
||||
ui.Say("Stopping the source instance...")
|
||||
_, err := ec2conn.StopInstances(instance.InstanceId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error stopping instance: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the instance to actual stop
|
||||
ui.Say("Waiting for the instance to stop...")
|
||||
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepStopInstance) Cleanup(map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -2,20 +2,14 @@ package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// DownloadConfig is the configuration given to instantiate a new
|
||||
@@ -43,10 +37,6 @@ type DownloadConfig struct {
|
||||
// for the downloader will be used to verify with this checksum after
|
||||
// it is downloaded.
|
||||
Checksum []byte
|
||||
|
||||
// What to use for the user agent for HTTP requests. If set to "", use the
|
||||
// default user agent provided by Go.
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// A DownloadClient helps download, verify checksums, etc.
|
||||
@@ -55,30 +45,12 @@ type DownloadClient struct {
|
||||
downloader Downloader
|
||||
}
|
||||
|
||||
// HashForType returns the Hash implementation for the given string
|
||||
// type, or nil if the type is not supported.
|
||||
func HashForType(t string) hash.Hash {
|
||||
switch t {
|
||||
case "md5":
|
||||
return md5.New()
|
||||
case "sha1":
|
||||
return sha1.New()
|
||||
case "sha256":
|
||||
return sha256.New()
|
||||
case "sha512":
|
||||
return sha512.New()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewDownloadClient returns a new DownloadClient for the given
|
||||
// configuration.
|
||||
func NewDownloadClient(c *DownloadConfig) *DownloadClient {
|
||||
if c.DownloaderMap == nil {
|
||||
c.DownloaderMap = map[string]Downloader{
|
||||
"http": &HTTPDownloader{userAgent: c.UserAgent},
|
||||
"https": &HTTPDownloader{userAgent: c.UserAgent},
|
||||
"http": new(HTTPDownloader),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +61,7 @@ func NewDownloadClient(c *DownloadConfig) *DownloadClient {
|
||||
// downloading it.
|
||||
type Downloader interface {
|
||||
Cancel()
|
||||
Download(*os.File, *url.URL) error
|
||||
Download(io.Writer, *url.URL) error
|
||||
Progress() uint
|
||||
Total() uint
|
||||
}
|
||||
@@ -101,7 +73,6 @@ func (d *DownloadClient) Cancel() {
|
||||
func (d *DownloadClient) Get() (string, error) {
|
||||
// If we already have the file and it matches, then just return the target path.
|
||||
if verify, _ := d.VerifyChecksum(d.config.TargetPath); verify {
|
||||
log.Println("Initial checksum matched, no download needed.")
|
||||
return d.config.TargetPath, nil
|
||||
}
|
||||
|
||||
@@ -110,18 +81,10 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("Parsed URL: %#v", url)
|
||||
|
||||
// Files when we don't copy the file are special cased.
|
||||
var f *os.File
|
||||
var finalPath string
|
||||
if url.Scheme == "file" && !d.config.CopyFile {
|
||||
finalPath = url.Path
|
||||
|
||||
// Remove forward slash on absolute Windows file URLs before processing
|
||||
if runtime.GOOS == "windows" && len(finalPath) > 0 && finalPath[0] == '/' {
|
||||
finalPath = finalPath[1:len(finalPath)]
|
||||
}
|
||||
} else {
|
||||
finalPath = d.config.TargetPath
|
||||
|
||||
@@ -132,29 +95,20 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
}
|
||||
|
||||
// Otherwise, download using the downloader.
|
||||
f, err = os.OpenFile(finalPath, os.O_RDWR|os.O_CREATE, os.FileMode(0666))
|
||||
f, err := os.Create(finalPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Downloading: %s", url.String())
|
||||
err = d.downloader.Download(f, url)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if d.config.Hash != nil {
|
||||
var verify bool
|
||||
verify, err = d.VerifyChecksum(finalPath)
|
||||
if err == nil && !verify {
|
||||
// Delete the file
|
||||
os.Remove(finalPath)
|
||||
|
||||
err = fmt.Errorf(
|
||||
"checksums didn't match expected: %s",
|
||||
hex.EncodeToString(d.config.Checksum))
|
||||
err = fmt.Errorf("checksums didn't match expected: %s", hex.EncodeToString(d.config.Checksum))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +116,8 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
}
|
||||
|
||||
// PercentProgress returns the download progress as a percentage.
|
||||
func (d *DownloadClient) PercentProgress() int {
|
||||
if d.downloader == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
|
||||
func (d *DownloadClient) PercentProgress() uint {
|
||||
return uint((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
|
||||
}
|
||||
|
||||
// VerifyChecksum tests that the path matches the checksum for the
|
||||
@@ -183,7 +133,6 @@ func (d *DownloadClient) VerifyChecksum(path string) (bool, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Verifying checksum of %s", path)
|
||||
d.config.Hash.Reset()
|
||||
io.Copy(d.config.Hash, f)
|
||||
return bytes.Compare(d.config.Hash.Sum(nil), d.config.Checksum) == 0, nil
|
||||
@@ -192,67 +141,23 @@ func (d *DownloadClient) VerifyChecksum(path string) (bool, error) {
|
||||
// HTTPDownloader is an implementation of Downloader that downloads
|
||||
// files over HTTP.
|
||||
type HTTPDownloader struct {
|
||||
progress uint
|
||||
total uint
|
||||
userAgent string
|
||||
progress uint
|
||||
total uint
|
||||
}
|
||||
|
||||
func (*HTTPDownloader) Cancel() {
|
||||
// TODO(mitchellh): Implement
|
||||
}
|
||||
|
||||
func (d *HTTPDownloader) Download(dst *os.File, src *url.URL) error {
|
||||
log.Printf("Starting download: %s", src.String())
|
||||
|
||||
// Seek to the beginning by default
|
||||
if _, err := dst.Seek(0, 0); err != nil {
|
||||
func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error {
|
||||
resp, err := http.Get(src.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset our progress
|
||||
d.progress = 0
|
||||
d.total = uint(resp.ContentLength)
|
||||
|
||||
// Make the request. We first make a HEAD request so we can check
|
||||
// if the server supports range queries. If the server/URL doesn't
|
||||
// support HEAD requests, we just fall back to GET.
|
||||
req, err := http.NewRequest("HEAD", src.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.userAgent != "" {
|
||||
req.Header.Set("User-Agent", d.userAgent)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err == nil && (resp.StatusCode >= 200 && resp.StatusCode < 300) {
|
||||
// If the HEAD request succeeded, then attempt to set the range
|
||||
// query if we can.
|
||||
if resp.Header.Get("Accept-Ranges") == "bytes" {
|
||||
if fi, err := dst.Stat(); err == nil {
|
||||
if _, err = dst.Seek(0, os.SEEK_END); err == nil {
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", fi.Size()))
|
||||
d.progress = uint(fi.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the request to GET now, and redo the query to download
|
||||
req.Method = "GET"
|
||||
|
||||
resp, err = httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.total = d.progress + uint(resp.ContentLength)
|
||||
var buffer [4096]byte
|
||||
for {
|
||||
n, err := resp.Body.Read(buffer[:])
|
||||
@@ -0,0 +1,42 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDownloadClient_VerifyChecksum(t *testing.T) {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("tempfile error: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
// "foo"
|
||||
checksum, err := hex.DecodeString("acbd18db4cc2f85cedef654fccc4a4d8")
|
||||
if err != nil {
|
||||
t.Fatalf("decode err: %s", err)
|
||||
}
|
||||
|
||||
// Write the file
|
||||
tf.Write([]byte("foo"))
|
||||
tf.Close()
|
||||
|
||||
config := &DownloadConfig{
|
||||
Hash: md5.New(),
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
d := NewDownloadClient(config)
|
||||
result, err := d.VerifyChecksum(tf.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Verify err: %s", err)
|
||||
}
|
||||
|
||||
if !result {
|
||||
t.Fatal("didn't verify")
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// MultistepDebugFn will return a proper multistep.DebugPauseFn to
|
||||
// use for debugging if you're using multistep in your builder.
|
||||
func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
|
||||
return func(loc multistep.DebugLocation, name string, state multistep.StateBag) {
|
||||
return func(loc multistep.DebugLocation, name string, state map[string]interface{}) {
|
||||
var locationString string
|
||||
switch loc {
|
||||
case multistep.DebugLocationAfterRun:
|
||||
@@ -23,7 +23,7 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(
|
||||
"Pausing %s step '%s'. Press enter to continue.",
|
||||
"Pausing %s step '%s'. Press any key to continue.",
|
||||
locationString, name)
|
||||
|
||||
result := make(chan string, 1)
|
||||
@@ -41,7 +41,7 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
|
||||
case <-result:
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// All of the methods used to communicate with the digital_ocean API
|
||||
// are here. Their API is on a path to V2, so just plain JSON is used
|
||||
// in place of a proper client library for now.
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
|
||||
|
||||
type Image struct {
|
||||
Id uint
|
||||
Name string
|
||||
Distribution string
|
||||
}
|
||||
|
||||
type ImagesResp struct {
|
||||
Images []Image
|
||||
}
|
||||
|
||||
type DigitalOceanClient struct {
|
||||
// The http client for communicating
|
||||
client *http.Client
|
||||
|
||||
// The base URL of the API
|
||||
BaseURL string
|
||||
|
||||
// Credentials
|
||||
ClientID string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: http.DefaultClient,
|
||||
BaseURL: DIGITALOCEAN_API_URL,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Creates an SSH Key and returns it's id
|
||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
// Escape the public key
|
||||
pub = url.QueryEscape(pub)
|
||||
|
||||
params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub)
|
||||
|
||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the SSH key's ID we just created
|
||||
key := body["ssh_key"].(map[string]interface{})
|
||||
keyId := key["id"].(float64)
|
||||
return uint(keyId), nil
|
||||
}
|
||||
|
||||
// Destroys an SSH key
|
||||
func (d DigitalOceanClient) DestroyKey(id uint) error {
|
||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a droplet and returns it's id
|
||||
func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) {
|
||||
params := fmt.Sprintf(
|
||||
"name=%v&image_id=%v&size_id=%v®ion_id=%v&ssh_key_ids=%v",
|
||||
name, image, size, region, keyId)
|
||||
|
||||
body, err := NewRequest(d, "droplets/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the Droplets ID
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
dropletId := droplet["id"].(float64)
|
||||
return uint(dropletId), err
|
||||
}
|
||||
|
||||
// Destroys a droplet
|
||||
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Powers off a droplet
|
||||
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
||||
|
||||
_, err := NewRequest(d, path, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a snaphot of a droplet by it's ID
|
||||
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
||||
params := fmt.Sprintf("name=%v", name)
|
||||
|
||||
_, err := NewRequest(d, path, params)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns all available images.
|
||||
func (d DigitalOceanClient) Images() ([]Image, error) {
|
||||
resp, err := NewRequest(d, "images", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result ImagesResp
|
||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Images, nil
|
||||
}
|
||||
|
||||
// Destroys an image by its ID.
|
||||
func (d DigitalOceanClient) DestroyImage(id uint) error {
|
||||
path := fmt.Sprintf("images/%d/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
||||
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
||||
path := fmt.Sprintf("droplets/%v", id)
|
||||
|
||||
body, err := NewRequest(d, path, "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var ip string
|
||||
|
||||
// Read the droplet's "status"
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
status := droplet["status"].(string)
|
||||
|
||||
if droplet["ip_address"] != nil {
|
||||
ip = droplet["ip_address"].(string)
|
||||
}
|
||||
|
||||
return ip, status, err
|
||||
}
|
||||
|
||||
// Sends an api request and returns a generic map[string]interface of
|
||||
// the response.
|
||||
func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) {
|
||||
client := d.client
|
||||
url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s",
|
||||
DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey)
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
|
||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &decodedResponse)
|
||||
|
||||
log.Printf("response from digitalocean: %v", decodedResponse)
|
||||
|
||||
// Catch all non-200 status and return an error
|
||||
if resp.StatusCode != 200 {
|
||||
err = errors.New(fmt.Sprintf("Received non-200 HTTP status from DigitalOcean: %v", resp.StatusCode))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
// Catch all non-OK statuses from DO and return an error
|
||||
status := decodedResponse["status"]
|
||||
if status != "OK" {
|
||||
// Get the actual error message if there is one
|
||||
if status == "ERROR" {
|
||||
status = decodedResponse["error_message"]
|
||||
}
|
||||
err = errors.New(fmt.Sprintf("Received bad status from DigitalOcean: %v", status))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
return decodedResponse, nil
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package digitalocean
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
@@ -13,13 +10,10 @@ type Artifact struct {
|
||||
snapshotName string
|
||||
|
||||
// The ID of the image
|
||||
snapshotId int
|
||||
|
||||
// The name of the region
|
||||
regionName string
|
||||
snapshotId uint
|
||||
|
||||
// The client for making API calls
|
||||
client *godo.Client
|
||||
client *DigitalOceanClient
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
@@ -32,19 +26,14 @@ func (*Artifact) Files() []string {
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return strconv.FormatUint(uint64(a.snapshotId), 10)
|
||||
return a.snapshotName
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("A snapshot was created: '%v' in region '%v'", a.snapshotName, a.regionName)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
return fmt.Sprintf("A snapshot was created: %v", a.snapshotName)
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
||||
_, err := a.client.Images.Delete(a.snapshotId)
|
||||
return err
|
||||
log.Printf("Destroying image: %d", a.snapshotId)
|
||||
return a.client.DestroyImage(a.snapshotId)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
@@ -14,18 +13,9 @@ func TestArtifact_Impl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
a := &Artifact{"packer-foobar", 42, "San Francisco", nil}
|
||||
expected := "42"
|
||||
|
||||
if a.Id() != expected {
|
||||
t.Fatalf("artifact ID should match: %v", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
a := &Artifact{"packer-foobar", 42, "San Francisco", nil}
|
||||
expected := "A snapshot was created: 'packer-foobar' in region 'San Francisco'"
|
||||
a := &Artifact{"packer-foobar", 42, nil}
|
||||
expected := "A snapshot was created: packer-foobar"
|
||||
|
||||
if a.String() != expected {
|
||||
t.Fatalf("artifact string should match: %v", expected)
|
||||
|
||||
+158
-37
@@ -4,62 +4,184 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"golang.org/x/oauth2"
|
||||
"log"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
const BuilderId = "pearkes.digitalocean"
|
||||
|
||||
type snapshotNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// Configuration tells the builder the credentials
|
||||
// to use while communicating with DO and describes the image
|
||||
// you are creating
|
||||
type config struct {
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
||||
SnapshotName string
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHTimeout time.Duration
|
||||
EventDelay time.Duration
|
||||
StateTimeout time.Duration
|
||||
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
|
||||
RawSnapshotName string `mapstructure:"snapshot_name"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawEventDelay string `mapstructure:"event_delay"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
config config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
c, warnings, errs := NewConfig(raws...)
|
||||
if errs != nil {
|
||||
return warnings, errs
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.config = *c
|
||||
|
||||
return nil, nil
|
||||
// Optional configuration with defaults
|
||||
//
|
||||
if b.config.RegionID == 0 {
|
||||
// Default to Region "New York"
|
||||
b.config.RegionID = 1
|
||||
}
|
||||
|
||||
if b.config.SizeID == 0 {
|
||||
// Default to 512mb, the smallest droplet size
|
||||
b.config.SizeID = 66
|
||||
}
|
||||
|
||||
if b.config.ImageID == 0 {
|
||||
// Default to base image "Ubuntu 12.04 x64 Server (id: 284203)"
|
||||
b.config.ImageID = 284203
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
// Default to "root". You can override this if your
|
||||
// SourceImage has a different user account then the DO default
|
||||
b.config.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
// Default to port 22 per DO default
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
// Default to 1 minute timeouts
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
if b.config.RawEventDelay == "" {
|
||||
// Default to 5 second delays after creating events
|
||||
// to allow DO to process
|
||||
b.config.RawEventDelay = "5s"
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout == "" {
|
||||
// Default to 6 minute timeouts waiting for
|
||||
// desired state. i.e waiting for droplet to become active
|
||||
b.config.RawStateTimeout = "6m"
|
||||
}
|
||||
|
||||
// A list of errors on the configuration
|
||||
errs := make([]error, 0)
|
||||
|
||||
// Required configurations that will display errors if not set
|
||||
//
|
||||
if b.config.ClientID == "" {
|
||||
errs = append(errs, errors.New("a client_id must be specified"))
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = append(errs, errors.New("an api_key must be specified"))
|
||||
}
|
||||
|
||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
b.config.SSHTimeout = sshTimeout
|
||||
|
||||
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
|
||||
}
|
||||
b.config.EventDelay = eventDelay
|
||||
|
||||
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
|
||||
}
|
||||
b.config.StateTimeout = stateTimeout
|
||||
|
||||
// Parse the name of the snapshot
|
||||
snapNameBuf := new(bytes.Buffer)
|
||||
tData := snapshotNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
|
||||
} else {
|
||||
t.Execute(snapNameBuf, tData)
|
||||
b.config.SnapshotName = snapNameBuf.String()
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &packer.MultiError{errs}
|
||||
}
|
||||
|
||||
log.Printf("Config: %+v", b.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
|
||||
AccessToken: b.config.APIToken,
|
||||
}))
|
||||
// Initialize the DO API client
|
||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
|
||||
|
||||
// Set up the state
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("client", client)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
state := make(map[string]interface{})
|
||||
state["config"] = b.config
|
||||
state["client"] = client
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&stepCreateSSHKey{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
|
||||
},
|
||||
new(stepCreateSSHKey),
|
||||
new(stepCreateDroplet),
|
||||
new(stepDropletInfo),
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(stepShutdown),
|
||||
new(stepConnectSSH),
|
||||
new(stepProvision),
|
||||
new(stepPowerOff),
|
||||
new(stepSnapshot),
|
||||
}
|
||||
@@ -77,19 +199,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("snapshot_name"); !ok {
|
||||
if _, ok := state["snapshot_name"]; !ok {
|
||||
log.Println("Failed to find snapshot_name in state. Bug?")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
snapshotName: state.Get("snapshot_name").(string),
|
||||
snapshotId: state.Get("snapshot_image_id").(int),
|
||||
regionName: state.Get("region").(string),
|
||||
snapshotName: state["snapshot_name"].(string),
|
||||
snapshotId: state["snapshot_image_id"].(uint),
|
||||
client: client,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||
)
|
||||
|
||||
func TestBuilderAcc_basic(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccBasic,
|
||||
})
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
|
||||
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
|
||||
}
|
||||
}
|
||||
|
||||
const testBuilderAccBasic = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "nyc2",
|
||||
"size": "512mb",
|
||||
"image": "ubuntu-12-04-x64"
|
||||
}]
|
||||
}
|
||||
`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user