Compare commits

..

110 Commits

Author SHA1 Message Date
Mitchell Hashimoto 801a13dd38 v0.2.1 2013-07-26 09:54:05 -07:00
Mitchell Hashimoto e5b14ac450 builder/amazon/common: wait for AMI to be available [GH-207] 2013-07-26 09:38:16 -07:00
Mitchell Hashimoto e6557c53c2 ctrl-c closes stdin for plugins so that they are unblocked 2013-07-25 23:27:13 -07:00
Mitchell Hashimoto b455d70346 Remove TODO, it was from pre-0.1 2013-07-25 23:14:56 -07:00
Mitchell Hashimoto 48b08fe71a update comments in signal.go 2013-07-25 21:36:46 -05:00
Mitchell Hashimoto 74ef543d5c packer/plugin: connect stdin to parent stdin 2013-07-25 21:24:49 -05:00
Mitchell Hashimoto 9f38f4cb47 fmt 2013-07-25 20:49:39 -05:00
Mitchell Hashimoto 7a00b240c1 builder/amazon/*: sigint while waiting for state change works 2013-07-25 20:49:15 -05:00
Mitchell Hashimoto f5f4266371 Update CHANGELOG 2013-07-25 15:20:20 -05:00
Mitchell Hashimoto 1ec2eacb92 Merge pull request #218 from jstamerj/master
builder/virtualbox, communicator/ssh: fixes for running on windows
2013-07-25 13:19:08 -07:00
Jeff Stamerjohn 88dcb43a3f Merge pull request #1 from jstamerj/jstamerj/windows
Jstamerj/windows
2013-07-25 09:32:13 -07:00
Jeff Stamerjohn c5aad085f3 Merge branch 'master' into jstamerj/windows 2013-07-25 09:31:02 -07:00
Jeff Stamerjohn 41df9fa2e8 Windows fixes
Strip carriage returns from end of lines in virtualbox/driver.go, fixing wait for virtualbox host shutdown.
Fix target upload directory path in communicator.go to use forward slashes.  (When running on windows path/filepath returns backslashes...which does not work when the target host is unix).
2013-07-25 09:28:04 -07:00
Mitchell Hashimoto 029dabaa49 Update CHANGELOG 2013-07-25 10:55:25 -05:00
Mitchell Hashimoto 4857faa363 Merge branch 'builder-amazon-instance'
This adds the "amazon-instance" builder to Packer which can create
instance-storage backed AMIs. The instance-storage backed AMIs are built
by launching another instance-store backed instance, provisioning it,
and then using EC2 AMI tools to bundle and register a new AMI.
2013-07-25 10:52:16 -05:00
Mitchell Hashimoto 0130b1310c builder/amazon/instance: default x509_upload_path 2013-07-25 10:51:38 -05:00
Mitchell Hashimoto 30a4bf7687 website: document the amazon-instance builder 2013-07-25 10:51:21 -05:00
Mitchell Hashimoto 8c3281405d builder/amazon/*: wait for AMI to be ready in common, use it instance 2013-07-25 00:56:37 -05:00
Mitchell Hashimoto 072d7a647a builder/amazon/instance: register AMI using API 2013-07-25 00:45:55 -05:00
Mitchell Hashimoto 77b92cd048 builder/amazon/instance: register the AMI 2013-07-25 00:19:04 -05:00
Mitchell Hashimoto 17f6914fc1 builder/amazon/instance: prefix has CreateTime support 2013-07-24 23:51:46 -05:00
Mitchell Hashimoto 5e6695264a builder/amazon/instance: upload bundle 2013-07-24 23:29:21 -05:00
Mitchell Hashimoto ad1015f35c builder/amazon/instance: bundle volume and keep track of dir 2013-07-24 23:22:16 -05:00
Mitchell Hashimoto 801541714e builder/amazon/instance: run ec2-bundle-vol in batch mode 2013-07-24 22:51:59 -05:00
Mitchell Hashimoto 3f543dddaa builder/amazon/instance: sudo the ec2-bundle-vol by default 2013-07-24 16:07:33 -05:00
Mitchell Hashimoto b5552c00f3 builder/amazon/instance: detect error bundling 2013-07-24 15:44:58 -05:00
Mitchell Hashimoto c6358b0475 builder/amazon/instance: ec2-bundle-vol is called 2013-07-24 15:43:35 -05:00
Mitchell Hashimoto 544b4b1429 builder/amazon/instance: check for the ami tools 2013-07-23 23:19:44 -05:00
Mitchell Hashimoto 3efdb009d7 builder/amazon/common: SSH into private IP if in VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b9b5de5684 fmt 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto a5546fa0f3 builder/amazon/*: Fix failing tests from rebase of VPC 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto b11b60746b builder/amazon/instance: boilerplate for bundle volume 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto c39299c21f builder/amazon/instance: after upload x509, should continue 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto d11cabf893 builder/amazon/instance: upload x509 cert 2013-07-23 22:48:33 -05:00
Mitchell Hashimoto caaa6667ed plugin/builder-amazon-instance: add the plugin 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto b91fb5e1a0 fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto a780d0e0dd builder/amazon/common: extract Artifact 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5a4f5c3f6f builder/amazon: provision on instance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5c7c0a6ee2 builder/amazon: extract SSH connect funcs 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 51f7db35f4 builder/amazon: extract StepRunSourceInstance 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 579eaaabc5 builder/amazon/common: panic if SSHPot is 0 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto f8319f8881 builder/amazon: extract StepSecurityGroup 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto e67e4cfa16 builder/amazon: extract StepKeyPair for both 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 5921492c8e fmt 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto d489f99aad builder/amazon/*: clean up tests 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto ae864b3efa builder/amazon/common: tests for runconfig 2013-07-23 22:48:32 -05:00
Mitchell Hashimoto 9fb778c012 builder/common: common config struct for Packer configs 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 45d50279f3 builder/amazon/ebs: validate access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 6fdf237c15 builder/amazon/common: RunConfig for launch info 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 8f649af165 builder/amazon/common: AccessConfig for standard access config 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 9348f481dd builder/amazon/instance: boilerplate 2013-07-23 22:48:31 -05:00
Mitchell Hashimoto 17acee653b packer: In the case of an error, close stdout/stderr writers 2013-07-23 22:46:51 -05:00
Mitchell Hashimoto 1c5e9848b5 provisioner/shell: use StartWithUi 2013-07-23 22:44:32 -05:00
Mitchell Hashimoto 479855a4f1 builder/virtualbox: shutdown messages go to Ui 2013-07-23 22:42:39 -05:00
Mitchell Hashimoto 3641c1a708 packer: Preserve original stdout/stderr on StartWithUi
/cc @sit
2013-07-23 22:38:03 -05:00
Mitchell Hashimoto 2e82358d2a fmt 2013-07-23 22:36:09 -05:00
Mitchell Hashimoto 32c84c17b1 packer: RemoteCmd.StartWithUi thanks to @sit
I ended up working on this on my own outside your PR, sorry, but it is
basically the same as yours.
2013-07-23 22:35:05 -05:00
Mitchell Hashimoto bd1fe21718 packer/rpc: remove unused test file 2013-07-23 13:45:35 -07:00
Mitchell Hashimoto 6422fabd19 command/build: if any builds fail, exit with 1 exit status 2013-07-23 13:36:07 -07:00
Mitchell Hashimoto 54fb31b7a3 Update CHANGELOG 2013-07-21 22:40:02 -07:00
Mitchell Hashimoto 51b63efe50 Merge pull request #210 from rgarcia/vpc-launch
builder/amazon: support launching in VPC
2013-07-21 22:39:01 -07:00
Rafael Garcia 282554b2b0 builder/amazon/ebs: support launching in VPC
adds `vpc_id` and `subnet_id` to builder config

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

Conflicts:

	builder/virtualbox/step_download_iso.go
2013-07-20 00:26:46 -04:00
Emil Sit 27da543a41 packer/cache: Preserve any extension found on keys
This allows us to hand cache paths to any programs that may want to
interpret file extensions in order to behave differently. For
example, VirtualBox may want ISO images to end with .iso.
2013-07-20 00:19:05 -04:00
Emil Sit fd1ec31a1d packer/cache: Extract cache path calculation to method 2013-07-20 00:15:14 -04:00
Timothy Sutton 7e6e699593 Remove ISO from ide1 in stepCleanVMX 2013-07-19 20:46:26 -04:00
Mitchell Hashimoto 7d1f799ced builder/*: switch to MultiErrorAppend 2013-07-19 19:08:25 -04:00
Mitchell Hashimoto 40ec63d349 packer: MultiErrorAppend helper method. 2013-07-19 18:14:50 -04:00
Mitchell Hashimoto 5dbae2efde builder/common: CheckUnusedConfig 2013-07-19 15:09:13 -04:00
Mitchell Hashimoto d1ecd89635 builder/*: extract to common the config decoding 2013-07-19 15:00:32 -04:00
Mitchell Hashimoto 6f6a27b008 builder/common: extract out decode config 2013-07-19 14:59:04 -04:00
Mitchell Hashimoto 5b8c372fdc communicator/ssh: style
/cc @markpeek - The main thing is that the style for variables/methods
in Go is to use CamelCase (with the first letter generally downcased
unless an exported thing).
2013-07-19 14:16:39 -04:00
Mitchell Hashimoto c80db71361 packer: fix failing tests 2013-07-19 14:13:02 -04:00
Mitchell Hashimoto abf4cfecb2 communicator/ssh: remove complete TODO 2013-07-19 14:09:20 -04:00
Mitchell Hashimoto 27fc4da5f9 Update CHANGELOG 2013-07-19 14:08:59 -04:00
Mitchell Hashimoto 0784f7961b Merge pull request #195 from markpeek/markpeek-file-error
communicator/ssh: check scp codes and report errors
2013-07-19 11:07:03 -07:00
Mitchell Hashimoto 71c0b2c1d5 Update CHANGELOG 2013-07-19 14:02:54 -04:00
Mitchell Hashimoto 9f93ca7d3b website: powered by bintray 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto bf3e137a5a website: remove the ?direct in downloads to use CDN 2013-07-19 14:02:16 -04:00
Mitchell Hashimoto f226bd13af Merge pull request #199 from qur/builder-name
packer: builder name should be removed from rawConfig
2013-07-19 11:02:05 -07:00
Julian Phillips dca4fabd1b packer/template: Remove name from builder rawConfig
This prevents the builder from rejecting the name (which it doesn't
know about) when validating the config.
2013-07-19 15:36:13 +01:00
Julian Phillips a31fd29b8b packer/template: Test that builder.rawConfig excludes name
The name isn't actually part of the builder config, so it should be
removed during parsing.
2013-07-19 15:35:05 +01:00
Radu Voicilas 0f1bc9ee6f Add a clean target to the Makefile 2013-07-18 10:40:23 -04:00
Mark Peek ed75091bd4 Log an upload error closer to when it occurs
When there is an error returned from the Upload it is propagated back
to the main loop and may have additional log messages before the actual
error is displayed. This displays a log message close to where the error
actually occurred.
2013-07-17 18:17:46 -07:00
Mark Peek a8a53fb0b4 Return errors from remote scp command
Currently file provisioners will silently fail when there is an error
with the remote side. This commit checks the scp error code and will
return the error message from the remote.
2013-07-17 18:15:42 -07:00
Mitchell Hashimoto 104178080c Merge pull request #192 from dougm/fusion-leases-path
builder/vmware: fix Fusion DhcpLeasesPath
2013-07-17 16:23:28 -07:00
Doug MacEachern 923a541d8c fix Fusion DhcpLeasesPath 2013-07-17 13:03:05 -07:00
Mitchell Hashimoto bca05ddd5b builder/vmware: look for license for WS9 2013-07-16 14:58:16 -07:00
Mitchell Hashimoto 04b8ff89a6 builder/vmware: workstation 9 show stderr if command fails 2013-07-16 14:46:34 -07:00
Mitchell Hashimoto b9f0b81821 scripts: Use XC_ARCH/XC_OS to limit what arch/os combo is built 2013-07-16 14:36:54 -07:00
Mitchell Hashimoto 56402f9432 Update CHANGELOG 2013-07-16 14:24:56 -07:00
Mitchell Hashimoto cbddd413f1 builder/vmware: wording 2013-07-16 14:23:52 -07:00
Mitchell Hashimoto 229011def4 builder/vmware: split drivers out into individual files 2013-07-16 14:23:01 -07:00
Julian Phillips d7e5a3bb1d builder/vmware: Add support for Workstation 9 (Linux).
Now the VMware builder should automatically pick between Fusion 5 and
Workstation 9, based on which one is installed.
2013-07-16 14:11:59 -07:00
Mitchell Hashimoto 8cb035cc65 Up version for dev 2013-07-16 11:44:15 -07:00
Mitchell Hashimoto e6dcb739d7 website: fix download parsing 2013-07-16 11:42:11 -07:00
75 changed files with 2578 additions and 880 deletions
+29
View File
@@ -1,3 +1,32 @@
## 0.2.1 (July 26, 2013)
FEATURES:
* New builder: `amazon-instance` can create instance-storage backed
AMIs.
* VMware builder now works with Workstation 9 on Linux.
IMPROVEMENTS:
* builder/amazon/all: Ctrl-C while waiting for state change works
* builder/amazon/ebs: Can now launch instances into a VPC for added protection [GH-210]
* builder/virtualbox,vmware: Add backspace, delete, and F1-F12 keys to the boot
command.
* builder/virtualbox: massive performance improvements with big ISO files because
an expensive copy is avoided. [GH-202]
* builder/vmware: CD is removed prior to exporting final machine. [GH-198]
BUG FIXES:
* builder/amazon/all: Gracefully handle when AMI appears to not exist
while AWS state is propogating. [GH-207]
* builder/virtualbox: Trim carriage returns for Windows to properly
detect VM state on Windows. [GH-218]
* core: build names no longer cause invalid config errors. [GH-197]
* command/build: If any builds fail, exit with non-zero exit status.
* communicator/ssh: SCP exit codes are tested and errors are reported. [GH-195]
* communicator/ssh: Properly change slash direction for Windows hosts. [GH-218]
## 0.2.0 (July 16, 2013)
BACKWARDS INCOMPATIBILITIES:
+3
View File
@@ -10,6 +10,9 @@ all:
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
@./scripts/build.sh
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
format:
go fmt ./...
-7
View File
@@ -1,7 +0,0 @@
* builder/amazonebs: Copy AMI to multiple regions
* builder/vmware: VMX templates
* communicator/ssh: Ability to re-establish connection
* communicator/ssh: Download()
* packer: Communicator should have Close() method
* packer/plugin: Better error messages/detection if plugin crashes
* provisioner/shell: Arguments
+21
View File
@@ -0,0 +1,21 @@
package common
import (
"github.com/mitchellh/goamz/aws"
)
// AccessConfig is for common configuration related to AWS access
type AccessConfig struct {
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
}
// Auth returns a valid aws.Auth object for access to AWS services, or
// an error if the authentication couldn't be resolved.
func (c *AccessConfig) Auth() (aws.Auth, error) {
return aws.GetAuth(c.AccessKey, c.SecretKey)
}
func (c *AccessConfig) Prepare() []error {
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package common
import (
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
// WaitForAMI waits for the given AMI ID to become ready.
func WaitForAMI(c *ec2.EC2, imageId string) error {
for {
imageResp, err := c.Images([]string{imageId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
log.Println("AMI not found, probably state issues on AWS side. Trying again.")
continue
}
return err
}
if imageResp.Images[0].State == "available" {
return nil
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
}
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"fmt"
@@ -8,35 +8,39 @@ import (
"strings"
)
type artifact struct {
// Artifact is an artifact implementation that contains built AMIs.
type Artifact struct {
// A map of regions to AMI IDs.
amis map[string]string
Amis map[string]string
// BuilderId is the unique ID for the builder that created this AMI
BuilderIdValue string
// EC2 connection for performing API stuff.
conn *ec2.EC2
Conn *ec2.EC2
}
func (*artifact) BuilderId() string {
return BuilderId
func (a *Artifact) BuilderId() string {
return a.BuilderIdValue
}
func (*artifact) Files() []string {
func (*Artifact) Files() []string {
// We have no files
return nil
}
func (a *artifact) Id() string {
parts := make([]string, 0, len(a.amis))
for region, amiId := range a.amis {
func (a *Artifact) Id() string {
parts := make([]string, 0, len(a.Amis))
for region, amiId := range a.Amis {
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
}
return strings.Join(parts, ",")
}
func (a *artifact) String() string {
amiStrings := make([]string, 0, len(a.amis))
for region, id := range a.amis {
func (a *Artifact) String() string {
amiStrings := make([]string, 0, len(a.Amis))
for region, id := range a.Amis {
single := fmt.Sprintf("%s: %s", region, id)
amiStrings = append(amiStrings, single)
}
@@ -44,12 +48,12 @@ func (a *artifact) String() string {
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *artifact) Destroy() error {
func (a *Artifact) Destroy() error {
errors := make([]error, 0)
for _, imageId := range a.amis {
for _, imageId := range a.Amis {
log.Printf("Deregistering image ID: %s", imageId)
if _, err := a.conn.DeregisterImage(imageId); err != nil {
if _, err := a.Conn.DeregisterImage(imageId); err != nil {
errors = append(errors, err)
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/asserts"
@@ -10,7 +10,7 @@ func TestArtifact_Impl(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
var actual packer.Artifact
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
assert.Implementor(&Artifact{}, &actual, "should be an Artifact")
}
func TestArtifactId(t *testing.T) {
@@ -22,7 +22,10 @@ func TestArtifactId(t *testing.T) {
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{
Amis: amis,
}
result := a.Id()
assert.Equal(result, expected, "should match output")
}
@@ -39,7 +42,7 @@ west: bar`
amis["east"] = "foo"
amis["west"] = "bar"
a := &artifact{amis, nil}
a := &Artifact{Amis: amis}
result := a.String()
assert.Equal(result, expected, "should match output")
}
+55
View File
@@ -0,0 +1,55 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"log"
"time"
)
type StateChangeConf struct {
Conn *ec2.EC2
Instance *ec2.Instance
Pending []string
StepState map[string]interface{}
Target string
}
func WaitForState(conf *StateChangeConf) (i *ec2.Instance, err error) {
log.Printf("Waiting for instance state to become: %s", conf.Target)
i = conf.Instance
for i.State.Name != conf.Target {
if conf.StepState != nil {
if _, ok := conf.StepState[multistep.StateCancelled]; ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if i.State.Name == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, conf.Target)
return
}
var resp *ec2.InstancesResp
resp, err = conf.Conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return
}
i = &resp.Reservations[0].Instances[0]
time.Sleep(2 * time.Second)
}
return
}
+68
View File
@@ -0,0 +1,68 @@
package common
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"time"
)
// RunConfig contains configuration for running an instance from a source
// AMI and details on how to access that launched image.
type RunConfig struct {
Region string
SourceAmi string `mapstructure:"source_ami"`
InstanceType string `mapstructure:"instance_type"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
SubnetId string `mapstructure:"subnet_id"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare() []error {
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "1m"
}
// Validation
var err error
errs := make([]error, 0)
if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if c.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if c.Region == "" {
errs = append(errs, errors.New("A region must be specified"))
} else if _, ok := aws.Regions[c.Region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", c.Region))
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
+108
View File
@@ -0,0 +1,108 @@
package common
import (
"os"
"testing"
)
func init() {
// Clear out the AWS access key env vars so they don't
// affect our tests.
os.Setenv("AWS_ACCESS_KEY_ID", "")
os.Setenv("AWS_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_KEY", "")
}
func testConfig() *RunConfig {
return &RunConfig{
Region: "us-east-1",
SourceAmi: "abcd",
InstanceType: "m1.small",
SSHUsername: "root",
}
}
func TestRunConfigPrepare(t *testing.T) {
c := testConfig()
err := c.Prepare()
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_Region(t *testing.T) {
c := testConfig()
c.Region = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.Region = "us-east-12"
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.Region = "us-east-1"
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
c.SSHPort = 44
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
if err := c.Prepare(); len(err) != 1 {
t.Fatalf("err: %s", err)
}
}
+45
View File
@@ -0,0 +1,45 @@
package common
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
var host string
instance := state["instance"].(*ec2.Instance)
if instance.VpcId != "" {
host = instance.PrivateIpAddress
} else {
host = instance.DNSName
}
return fmt.Sprintf("%s:%d", host, port), nil
}
}
// SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the instance created over SSH using the generated
// private key.
func SSHConfig(username string) func(map[string]interface{}) (*gossh.ClientConfig, error) {
return func(state map[string]interface{}) (*gossh.ClientConfig, error) {
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: username,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,11 +10,11 @@ import (
"log"
)
type stepKeyPair struct {
type StepKeyPair struct {
keyName string
}
func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
@@ -23,9 +23,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := ec2conn.CreateKeyPair(keyName)
if err != nil {
err := fmt.Errorf("Error creating temporary keypair: %s", err)
state["error"] = err
ui.Error(err.Error())
state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err)
return multistep.ActionHalt
}
@@ -39,7 +37,7 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
return multistep.ActionContinue
}
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
func (s *StepKeyPair) Cleanup(state map[string]interface{}) {
// If no key name is set, then we never created it, so just return
if s.keyName == "" {
return
@@ -1,4 +1,4 @@
package ebs
package common
import (
"fmt"
@@ -8,12 +8,16 @@ import (
"log"
)
type stepRunSourceInstance struct {
type StepRunSourceInstance struct {
ExpectedRootDevice string
InstanceType string
SourceAMI string
SubnetId string
instance *ec2.Instance
}
func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
keyName := state["keyPair"].(string)
securityGroupId := state["securityGroupId"].(string)
@@ -21,24 +25,26 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: config.SourceAmi,
InstanceType: config.InstanceType,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
MinCount: 0,
MaxCount: 0,
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
SubnetId: s.SubnetId,
}
ui.Say("Launching a source AWS instance...")
imageResp, err := ec2conn.Images([]string{config.SourceAmi}, ec2.NewFilter())
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
if err != nil {
state["error"] = fmt.Errorf("There was a problem with the source AMI: %s", err)
return multistep.ActionHalt
}
if imageResp.Images[0].RootDeviceType != "ebs" {
if s.ExpectedRootDevice != "" && imageResp.Images[0].RootDeviceType != s.ExpectedRootDevice {
state["error"] = fmt.Errorf(
"The provided source AMI is instance-store based. The\n" +
"amazon-ebs bundler can only work with EBS based AMIs.")
"The provided source AMI has an invalid root device type.\n"+
"Expected '%s', got '%s'.",
s.ExpectedRootDevice, imageResp.Images[0].RootDeviceType)
return multistep.ActionHalt
}
@@ -54,7 +60,14 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
log.Printf("instance id: %s", s.instance.InstanceId)
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
stateChange := StateChangeConf{
Conn: ec2conn,
Instance: s.instance,
Pending: []string{"pending"},
Target: "running",
StepState: state,
}
s.instance, err = WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
state["error"] = err
@@ -67,7 +80,7 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
return multistep.ActionContinue
}
func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
func (s *StepRunSourceInstance) Cleanup(state map[string]interface{}) {
if s.instance == nil {
return
}
@@ -81,6 +94,12 @@ func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
return
}
pending := []string{"pending", "running", "shutting-down", "stopped", "stopping"}
waitForState(ec2conn, s.instance, pending, "terminated")
stateChange := StateChangeConf{
Conn: ec2conn,
Instance: s.instance,
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "running",
}
WaitForState(&stateChange)
}
@@ -1,4 +1,4 @@
package ebs
package common
import (
"cgl.tideland.biz/identifier"
@@ -10,40 +10,52 @@ import (
"log"
)
type stepSecurityGroup struct {
groupId string
type StepSecurityGroup struct {
SecurityGroupId string
SSHPort int
VpcId string
createdGroupId string
}
func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
func (s *StepSecurityGroup) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
if config.SecurityGroupId != "" {
log.Printf("Using specified security group: %s", config.SecurityGroupId)
state["securityGroupId"] = config.SecurityGroupId
if s.SecurityGroupId != "" {
log.Printf("Using specified security group: %s", s.SecurityGroupId)
state["securityGroupId"] = s.SecurityGroupId
return multistep.ActionContinue
}
if s.SSHPort == 0 {
panic("SSHPort must be set to a non-zero value.")
}
// Create the group
ui.Say("Creating temporary security group for this instance...")
groupName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
log.Printf("Temporary group name: %s", groupName)
groupResp, err := ec2conn.CreateSecurityGroup(groupName, "Temporary group for Packer")
group := ec2.SecurityGroup{
Name: groupName,
Description: "Temporary group for Packer",
VpcId: s.VpcId,
}
groupResp, err := ec2conn.CreateSecurityGroup(group)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the group ID so we can delete it later
s.groupId = groupResp.Id
s.createdGroupId = groupResp.Id
// Authorize the SSH access
perms := []ec2.IPPerm{
ec2.IPPerm{
Protocol: "tcp",
FromPort: config.SSHPort,
ToPort: config.SSHPort,
FromPort: s.SSHPort,
ToPort: s.SSHPort,
SourceIPs: []string{"0.0.0.0/0"},
},
}
@@ -57,13 +69,13 @@ func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepActi
}
// Set some state data for use in future steps
state["securityGroupId"] = s.groupId
state["securityGroupId"] = s.createdGroupId
return multistep.ActionContinue
}
func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.groupId == "" {
func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
if s.createdGroupId == "" {
return
}
@@ -71,10 +83,10 @@ func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
ui := state["ui"].(packer.Ui)
ui.Say("Deleting temporary security group...")
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
if err != nil {
log.Printf("Error deleting security group: %s", err)
ui.Error(fmt.Sprintf(
"Error cleaning up security group. Please delete the group manually: %s", s.groupId))
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
}
}
+35 -93
View File
@@ -10,41 +10,24 @@ import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"sort"
"strings"
"text/template"
"time"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazonebs"
type config struct {
// Access information
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
// Information for the source instance
Region string
SourceAmi string `mapstructure:"source_ami"`
InstanceType string `mapstructure:"instance_type"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
type Builder struct {
@@ -53,81 +36,30 @@ type Builder struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
b.config.RawSSHTimeout = "1m"
}
// Accumulate any errors
if b.config.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
if b.config.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
}
if b.config.Region == "" {
errs = append(errs, errors.New("A region must be specified"))
} else if _, ok := aws.Regions[b.config.Region]; !ok {
errs = append(errs, fmt.Errorf("Unknown region: %s", b.config.Region))
}
if b.config.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
b.config.sshTimeout, err = time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
if b.config.AMIName == "" {
errs = append(errs, errors.New("ami_name must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ami_name: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
@@ -140,7 +72,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
panic("region not found")
}
auth, err := aws.GetAuth(b.config.AccessKey, b.config.SecretKey)
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
@@ -156,13 +88,22 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&stepKeyPair{},
&stepSecurityGroup{},
&stepRunSourceInstance{},
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "ebs",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshTimeout,
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
@@ -192,9 +133,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Build the artifact and return it
artifact := &artifact{
amis: state["amis"].(map[string]string),
conn: ec2conn,
artifact := &awscommon.Artifact{
Amis: state["amis"].(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
-160
View File
@@ -2,19 +2,9 @@ package ebs
import (
"github.com/mitchellh/packer/packer"
"os"
"testing"
)
func init() {
// Clear out the AWS access key env vars so they don't
// affect our tests.
os.Setenv("AWS_ACCESS_KEY_ID", "")
os.Setenv("AWS_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
os.Setenv("AWS_SECRET_KEY", "")
}
func testConfig() map[string]interface{} {
return map[string]interface{}{
"access_key": "foo",
@@ -75,30 +65,6 @@ func TestBuilderPrepare_AMIName(t *testing.T) {
}
}
func TestBuilderPrepare_InstanceType(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["instance_type"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.InstanceType != "foo" {
t.Errorf("invalid: %s", b.config.InstanceType)
}
// Test bad
delete(config, "instance_type")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
@@ -110,129 +76,3 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["region"] = "us-east-1"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != "us-east-1" {
t.Errorf("invalid: %s", b.config.Region)
}
// Test bad
delete(config, "region")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test invalid
config["region"] = "i-am-not-real"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SourceAmi(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["source_ami"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SourceAmi != "foo" {
t.Errorf("invalid: %s", b.config.SourceAmi)
}
// Test bad
delete(config, "source_ami")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SSHPort(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 22 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
// Test set
config["ssh_port"] = 35
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHPort != 35 {
t.Errorf("invalid: %d", b.config.SSHPort)
}
}
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
config["ssh_timeout"] = "this is not good"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["ssh_timeout"] = "5s"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHUsername(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ssh_username"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername)
}
// Test bad
delete(config, "ssh_username")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
-39
View File
@@ -1,39 +0,0 @@
package ebs
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"log"
"time"
)
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, pending []string, target string) (i *ec2.Instance, err error) {
log.Printf("Waiting for instance state to become: %s", target)
i = originalInstance
for i.State.Name != target {
found := false
for _, allowed := range pending {
if i.State.Name == allowed {
found = true
break
}
}
if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, target)
return
}
var resp *ec2.InstancesResp
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return
}
i = &resp.Reservations[0].Instances[0]
time.Sleep(2 * time.Second)
}
return
}
-31
View File
@@ -1,31 +0,0 @@
package ebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(config)
instance := state["instance"].(*ec2.Instance)
return fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
+6 -18
View File
@@ -5,8 +5,8 @@ import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"log"
"strconv"
"text/template"
"time"
@@ -57,23 +57,11 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
for {
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
if err != nil {
err := fmt.Errorf("Error querying images: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
if imageResp.Images[0].State == "available" {
break
}
log.Printf("Image in state %s, sleeping 2s before checking again",
imageResp.Images[0].State)
time.Sleep(2 * time.Second)
if err := awscommon.WaitForAMI(ec2conn, createResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
+9 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
)
@@ -26,7 +27,14 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio
// Wait for the instance to actual stop
ui.Say("Waiting for the instance to stop...")
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
stateChange := awscommon.StateChangeConf{
Conn: ec2conn,
Instance: instance,
Pending: []string{"running", "stopping"},
Target: "stopped",
StepState: state,
}
instance, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
state["error"] = err
+219
View File
@@ -0,0 +1,219 @@
// The instance package contains a packer.Builder implementation that builds
// AMIs for Amazon EC2 backed by instance storage, as opposed to EBS storage.
package instance
import (
"errors"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
"text/template"
)
// The unique ID for this builder
const BuilderId = "mitchellh.amazon.instance"
// Config is the configuration that is chained through the steps and
// settable from the template.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
AccountId string `mapstructure:"account_id"`
AMIName string `mapstructure:"ami_name"`
BundleDestination string `mapstructure:"bundle_destination"`
BundlePrefix string `mapstructure:"bundle_prefix"`
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) error {
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
if b.config.BundleDestination == "" {
b.config.BundleDestination = "/tmp"
}
if b.config.BundlePrefix == "" {
b.config.BundlePrefix = "image-{{.CreateTime}}"
}
if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--retry"
}
if b.config.BundleVolCommand == "" {
b.config.BundleVolCommand = "sudo -n ec2-bundle-vol " +
"-k {{.KeyPath}} " +
"-u {{.AccountId}} " +
"-c {{.CertPath}} " +
"-r {{.Architecture}} " +
"-e {{.PrivatePath}} " +
"-d {{.Destination}} " +
"-p {{.Prefix}} " +
"--batch"
}
if b.config.X509UploadPath == "" {
b.config.X509UploadPath = "/tmp"
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
if b.config.AccountId == "" {
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
} else {
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
}
if b.config.AMIName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_name must be specified"))
} else {
_, err = template.New("ami").Parse(b.config.AMIName)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
}
}
if b.config.S3Bucket == "" {
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
}
if b.config.X509CertPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_cert_path is required"))
} else if _, err := os.Stat(b.config.X509CertPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_cert_path points to bad file: %s", err))
}
if b.config.X509KeyPath == "" {
errs = packer.MultiErrorAppend(errs, errors.New("x509_key_path is required"))
} else if _, err := os.Stat(b.config.X509KeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("x509_key_path points to bad file: %s", err))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
return nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
region, ok := aws.Regions[b.config.Region]
if !ok {
panic("region not found")
}
auth, err := b.config.AccessConfig.Auth()
if err != nil {
return nil, err
}
ec2conn := ec2.New(auth, region)
// Setup the state bag and initial state for the steps
state := make(map[string]interface{})
state["config"] = &b.config
state["ec2"] = ec2conn
state["hook"] = hook
state["ui"] = ui
// Build the steps
steps := []multistep.Step{
&awscommon.StepKeyPair{},
&awscommon.StepSecurityGroup{
SecurityGroupId: b.config.SecurityGroupId,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
ExpectedRootDevice: "instance-store",
InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&StepUploadX509Cert{},
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state["error"]; ok {
return nil, rawErr.(error)
}
// If there are no AMIs, then just return
if _, ok := state["amis"]; !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &awscommon.Artifact{
Amis: state["amis"].(map[string]string),
BuilderIdValue: BuilderId,
Conn: ec2conn,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
+219
View File
@@ -0,0 +1,219 @@
package instance
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
func testConfig() map[string]interface{} {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
panic(err)
}
return map[string]interface{}{
"account_id": "foo",
"ami_name": "foo",
"instance_type": "m1.small",
"region": "us-east-1",
"s3_bucket": "foo",
"source_ami": "foo",
"ssh_username": "bob",
"x509_cert_path": tf.Name(),
"x509_key_path": tf.Name(),
"x509_upload_path": "/foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilderPrepare_AccountId(t *testing.T) {
b := &Builder{}
config := testConfig()
config["account_id"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["account_id"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
config["account_id"] = "0123-0456-7890"
err = b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.AccountId != "012304567890" {
t.Errorf("should strip hyphens: %s", b.config.AccountId)
}
}
func TestBuilderPrepare_AMIName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["ami_name"] = "foo"
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ami_name"] = "foo {{"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "ami_name")
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_BundleDestination(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_destination"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundleDestination != "/tmp" {
t.Fatalf("bad: %s", b.config.BundleDestination)
}
}
func TestBuilderPrepare_BundlePrefix(t *testing.T) {
b := &Builder{}
config := testConfig()
config["bundle_prefix"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
t.Fatalf("bad: %s", b.config.BundlePrefix)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_S3Bucket(t *testing.T) {
b := &Builder{}
config := testConfig()
config["s3_bucket"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["s3_bucket"] = "foo"
err = b.Prepare(config)
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_X509CertPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_cert_path"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_cert_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_cert_path"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509KeyPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_key_path"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["x509_key_path"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["x509_key_path"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_X509UploadPath(t *testing.T) {
b := &Builder{}
config := testConfig()
config["x509_upload_path"] = ""
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
@@ -0,0 +1,104 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type bundleCmdData struct {
AccountId string
Architecture string
CertPath string
Destination string
KeyPath string
Prefix string
PrivatePath string
}
type bundlePrefixData struct {
CreateTime string
}
type StepBundleVolume struct{}
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
instance := state["instance"].(*ec2.Instance)
ui := state["ui"].(packer.Ui)
x509RemoteCertPath := state["x509RemoteCertPath"].(string)
x509RemoteKeyPath := state["x509RemoteKeyPath"].(string)
// Verify the AMI tools are available
ui.Say("Checking for EC2 AMI tools...")
cmd := &packer.RemoteCmd{Command: "ec2-ami-tools-version"}
if err := comm.Start(cmd); err != nil {
state["error"] = fmt.Errorf("Error checking for AMI tools: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
cmd.Wait()
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"The EC2 AMI tools could not be detected. These must be manually\n" +
"via a provisioner or some other means and are required for Packer\n" +
"to create an instance-store AMI.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Bundle the volume
var bundlePrefix bytes.Buffer
prefixTData := bundlePrefixData{
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
t.Execute(&bundlePrefix, prefixTData)
var bundleCmd bytes.Buffer
tData := bundleCmdData{
AccountId: config.AccountId,
Architecture: instance.Architecture,
CertPath: x509RemoteCertPath,
Destination: config.BundleDestination,
KeyPath: x509RemoteKeyPath,
Prefix: bundlePrefix.String(),
PrivatePath: config.X509UploadPath,
}
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
t.Execute(&bundleCmd, tData)
ui.Say("Bundling the volume...")
cmd = new(packer.RemoteCmd)
cmd.Command = bundleCmd.String()
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Volume bundling failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Store the manifest path
manifestName := bundlePrefix.String() + ".manifest.xml"
state["manifest_name"] = manifestName
state["manifest_path"] = fmt.Sprintf(
"%s/%s", config.BundleDestination, manifestName)
return multistep.ActionContinue
}
func (s *StepBundleVolume) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,68 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/packer"
"strconv"
"text/template"
"time"
)
type amiNameData struct {
CreateTime string
}
type StepRegisterAMI struct{}
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*Config)
ec2conn := state["ec2"].(*ec2.EC2)
manifestPath := state["remote_manifest_path"].(string)
ui := state["ui"].(packer.Ui)
// Parse the name of the AMI
amiNameBuf := new(bytes.Buffer)
tData := amiNameData{
strconv.FormatInt(time.Now().UTC().Unix(), 10),
}
t := template.Must(template.New("ami").Parse(config.AMIName))
t.Execute(amiNameBuf, tData)
amiName := amiNameBuf.String()
ui.Say("Registering the AMI...")
registerOpts := &ec2.RegisterImage{
ImageLocation: manifestPath,
Name: amiName,
}
registerResp, err := ec2conn.RegisterImage(registerOpts)
if err != nil {
state["error"] = fmt.Errorf("Error registering AMI: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
// Set the AMI ID in the state
ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId))
amis := make(map[string]string)
amis[config.Region] = registerResp.ImageId
state["amis"] = amis
// Wait for the image to become ready
ui.Say("Waiting for AMI to become ready...")
if err := awscommon.WaitForAMI(ec2conn, registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepRegisterAMI) Cleanup(map[string]interface{}) {}
@@ -0,0 +1,61 @@
package instance
import (
"bytes"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"text/template"
)
type uploadCmdData struct {
AccessKey string
BucketName string
BundleDirectory string
ManifestPath string
SecretKey string
}
type StepUploadBundle struct{}
func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
manifestName := state["manifest_name"].(string)
manifestPath := state["manifest_path"].(string)
ui := state["ui"].(packer.Ui)
var uploadCmd bytes.Buffer
tData := uploadCmdData{
AccessKey: config.AccessKey,
BucketName: config.S3Bucket,
BundleDirectory: config.BundleDestination,
ManifestPath: manifestPath,
SecretKey: config.SecretKey,
}
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
t.Execute(&uploadCmd, tData)
ui.Say("Uploading the bundle...")
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
if err := cmd.StartWithUi(comm, ui); err != nil {
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if cmd.ExitStatus != 0 {
state["error"] = fmt.Errorf(
"Bundle upload failed. Please see the output above for more\n" +
"details on what went wrong.")
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["remote_manifest_path"] = fmt.Sprintf(
"%s/%s", config.S3Bucket, manifestName)
return multistep.ActionContinue
}
func (s *StepUploadBundle) Cleanup(state map[string]interface{}) {}
@@ -0,0 +1,49 @@
package instance
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"os"
)
type StepUploadX509Cert struct{}
func (s *StepUploadX509Cert) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*Config)
ui := state["ui"].(packer.Ui)
x509RemoteCertPath := config.X509UploadPath + "/cert.pem"
x509RemoteKeyPath := config.X509UploadPath + "/key.pem"
ui.Say("Uploading X509 Certificate...")
if err := s.uploadSingle(comm, x509RemoteCertPath, config.X509CertPath); err != nil {
state["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
if err := s.uploadSingle(comm, x509RemoteKeyPath, config.X509KeyPath); err != nil {
state["error"] = fmt.Errorf("Error uploading X509 cert: %s", err)
ui.Error(state["error"].(error).Error())
return multistep.ActionHalt
}
state["x509RemoteCertPath"] = x509RemoteCertPath
state["x509RemoteKeyPath"] = x509RemoteKeyPath
return multistep.ActionContinue
}
func (s *StepUploadX509Cert) Cleanup(map[string]interface{}) {}
func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
return comm.Upload(dst, f)
}
+57
View File
@@ -0,0 +1,57 @@
package common
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/packer"
"sort"
"strings"
)
// CheckUnusedConfig is a helper that makes sure that the there are no
// unused configuration keys, properly ignoring keys that don't matter.
func CheckUnusedConfig(md *mapstructure.Metadata) *packer.MultiError {
errs := make([]error, 0)
if md.Unused != nil && len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
return nil
}
// DecodeConfig is a helper that handles decoding raw configuration using
// mapstructure. It returns the metadata and any errors that may happen.
// If you need extra configuration for mapstructure, you should configure
// it manually and not use this helper function.
func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metadata, error) {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: target,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
if err != nil {
return nil, err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return nil, err
}
}
return &md, nil
}
+63
View File
@@ -0,0 +1,63 @@
package common
import (
"github.com/mitchellh/mapstructure"
"reflect"
"testing"
)
func TestCheckUnusedConfig(t *testing.T) {
md := &mapstructure.Metadata{
Unused: make([]string, 0),
}
err := CheckUnusedConfig(md)
if err != nil {
t.Fatalf("err: %s", err)
}
md.Unused = []string{"foo", "bar"}
err = CheckUnusedConfig(md)
if err == nil {
t.Fatal("should have error")
}
}
func TestDecodeConfig(t *testing.T) {
type Local struct {
Foo string
Bar string
}
raws := []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"bar": "baz",
"baz": "what",
},
}
var result Local
md, err := DecodeConfig(&result, raws...)
if err != nil {
t.Fatalf("err: %s", err)
}
if result.Foo != "bar" {
t.Fatalf("invalid: %#v", result.Foo)
}
if result.Bar != "baz" {
t.Fatalf("invalid: %#v", result.Bar)
}
if md == nil {
t.Fatal("metadata should not be nil")
}
if !reflect.DeepEqual(md.Unused, []string{"baz"}) {
t.Fatalf("unused: %#v", md.Unused)
}
}
+10
View File
@@ -0,0 +1,10 @@
package common
// PackerConfig is a struct that contains the configuration keys that
// are sent by packer, properly tagged already so mapstructure can load
// them. Embed this structure into your configuration class to get it.
type PackerConfig struct {
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
}
+18 -39
View File
@@ -7,15 +7,12 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
"sort"
"strconv"
"strings"
"text/template"
"time"
)
@@ -31,6 +28,8 @@ type snapshotNameData struct {
// to use while communicating with DO and describes the image
// you are creating
type config struct {
common.PackerConfig `mapstructure:",squash"`
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
RegionID uint `mapstructure:"region_id"`
@@ -41,8 +40,6 @@ type config struct {
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
PackerDebug bool `mapstructure:"packer_debug"`
RawSnapshotName string `mapstructure:"snapshot_name"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawEventDelay string `mapstructure:"event_delay"`
@@ -61,37 +58,13 @@ type Builder struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
// Optional configuration with defaults
if b.config.APIKey == "" {
@@ -154,28 +127,33 @@ func (b *Builder) Prepare(raws ...interface{}) error {
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = append(errs, errors.New("a client_id must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id must be specified"))
}
if b.config.APIKey == "" {
errs = append(errs, errors.New("an api_key must be specified"))
errs = packer.MultiErrorAppend(
errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing event_delay: %s", err))
}
b.config.eventDelay = eventDelay
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
@@ -186,14 +164,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
} else {
t.Execute(snapNameBuf, tData)
b.config.SnapshotName = snapNameBuf.String()
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
log.Printf("Config: %+v", b.config)
+43 -55
View File
@@ -3,7 +3,6 @@ package virtualbox
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
@@ -12,7 +11,6 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
@@ -26,6 +24,8 @@ type Builder struct {
}
type config struct {
common.PackerConfig `mapstructure:",squash"`
BootCommand []string `mapstructure:"boot_command"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
@@ -51,10 +51,6 @@ type config struct {
VBoxManage [][]string `mapstructure:"vboxmanage"`
VMName string `mapstructure:"vm_name"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
@@ -65,37 +61,13 @@ type config struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
@@ -154,32 +126,37 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOChecksum == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_checksum is required"))
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = append(errs, errors.New("The iso_checksum_type must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -187,7 +164,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -202,12 +180,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
}
@@ -220,7 +199,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if b.config.GuestAdditionsURL != "" {
url, err := url.Parse(b.config.GuestAdditionsURL)
if err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -228,7 +208,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -243,12 +224,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.GuestAdditionsURL = url.String()
}
@@ -256,15 +238,16 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
errors.New("Output directory already exists. It must not exist."))
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
if b.config.RawShutdownTimeout == "" {
@@ -277,29 +260,34 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
b.driver, err = b.newDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
+3
View File
@@ -50,6 +50,9 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
}
for _, line := range strings.Split(stdout.String(), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
if line == `VMState="running"` {
return true, nil
}
+4 -44
View File
@@ -6,11 +6,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
@@ -23,11 +19,9 @@ import (
//
// Produces:
// iso_path string
type stepDownloadISO struct {
isoCopyDir string
}
type stepDownloadISO struct{}
func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
@@ -84,44 +78,10 @@ DownloadWaitLoop:
}
}
// VirtualBox is really dumb and can't figure out that the file is an
// ISO unless it has a ".iso" extension. We can't modify the cache
// filenames so we just do a copy.
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
s.isoCopyDir = tempdir
f, err := os.Create(filepath.Join(tempdir, "image.iso"))
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer f.Close()
sourceF, err := os.Open(cachePath)
if err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
defer sourceF.Close()
log.Printf("Copying ISO to temp location: %s", tempdir)
if _, err := io.Copy(f, sourceF); err != nil {
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
return multistep.ActionHalt
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = f.Name()
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (s *stepDownloadISO) Cleanup(map[string]interface{}) {
if s.isoCopyDir != "" {
os.RemoveAll(s.isoCopyDir)
}
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
+1 -4
View File
@@ -34,16 +34,13 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
if err := cmd.StartWithUi(comm, ui); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the command to run
cmd.Wait()
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
shutdownTimer := time.After(config.shutdownTimeout)
@@ -91,8 +91,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func scancodes(message string) []string {
special := make(map[string][]string)
special["<bs>"] = []string{"ff", "08"}
special["<del>"] = []string{"ff", "ff"}
special["<enter>"] = []string{"1c", "9c"}
special["<esc>"] = []string{"01", "81"}
special["<f1>"] = []string{"ff", "be"}
special["<f2>"] = []string{"ff", "bf"}
special["<f3>"] = []string{"ff", "c0"}
special["<f4>"] = []string{"ff", "c1"}
special["<f5>"] = []string{"ff", "c2"}
special["<f6>"] = []string{"ff", "c3"}
special["<f7>"] = []string{"ff", "c4"}
special["<f8>"] = []string{"ff", "c5"}
special["<f9>"] = []string{"ff", "c6"}
special["<f10>"] = []string{"ff", "c7"}
special["<f11>"] = []string{"ff", "c8"}
special["<f12>"] = []string{"ff", "c9"}
special["<return>"] = []string{"1c", "9c"}
special["<tab>"] = []string{"0f", "8f"}
+39 -63
View File
@@ -3,7 +3,6 @@ package vmware
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/packer"
@@ -12,7 +11,6 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
@@ -27,6 +25,8 @@ type Builder struct {
}
type config struct {
common.PackerConfig `mapstructure:",squash"`
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"`
@@ -52,10 +52,6 @@ type config struct {
VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
PackerBuildName string `mapstructure:"packer_build_name"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
@@ -66,37 +62,13 @@ type config struct {
}
func (b *Builder) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &b.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&b.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if b.config.DiskName == "" {
b.config.DiskName = "disk"
@@ -151,32 +123,37 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = append(errs, errors.New("http_port_min must be less than http_port_max"))
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOChecksum == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_checksum is required"))
errs = packer.MultiErrorAppend(
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
} else {
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
}
if b.config.ISOChecksumType == "" {
errs = append(errs, errors.New("The iso_checksum_type must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
}
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
@@ -184,7 +161,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if url.Scheme == "file" {
if _, err := os.Stat(url.Path); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
@@ -199,12 +177,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
if errs == nil || len(errs.Errors) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
}
@@ -212,20 +191,22 @@ func (b *Builder) Prepare(raws ...interface{}) error {
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = append(
errs = packer.MultiErrorAppend(
errs,
errors.New("Output directory already exists. It must not exist."))
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
if b.config.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
if b.config.RawBootWait != "" {
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
}
}
@@ -235,7 +216,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.RawSSHWaitTimeout == "" {
@@ -244,24 +226,28 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
if _, err := template.New("path").Parse(b.config.ToolsUploadPath); err != nil {
errs = append(errs, fmt.Errorf("tools_upload_path invalid: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("tools_upload_path invalid: %s", err))
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
b.driver, err = b.newDriver()
b.driver, err = NewDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VMware driver: %s", err))
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed creating VMware driver: %s", err))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -358,13 +344,3 @@ func (b *Builder) Cancel() {
b.runner.Cancel()
}
}
func (b *Builder) newDriver() (Driver, error) {
fusionAppPath := "/Applications/VMware Fusion.app"
driver := &Fusion5Driver{fusionAppPath}
if err := driver.Verify(); err != nil {
return nil, err
}
return driver, nil
}
+19 -125
View File
@@ -1,13 +1,8 @@
package vmware
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"runtime"
)
// A driver is able to talk to VMware, control virtual machines, etc.
@@ -30,6 +25,9 @@ type Driver interface {
// Get the path to the VMware ISO for the given flavor.
ToolsIsoPath(string) string
// Get the path to the DHCP leases file for the given device.
DhcpLeasesPath(string) string
// Verify checks to make sure that this driver should function
// properly. This should check that all the files it will use
// appear to exist and so on. If everything is okay, this doesn't
@@ -37,129 +35,25 @@ type Driver interface {
Verify() error
}
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
// NewDriver returns a new driver implementation for this operating
// system, or an error if the driver couldn't be initialized.
func NewDriver() (Driver, error) {
var driver Driver
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
switch runtime.GOOS {
case "darwin":
driver = &Fusion5Driver{
AppPath: "/Applications/VMware Fusion.app",
}
case "linux":
driver = &Workstation9LinuxDriver{}
default:
return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS)
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
if err := driver.Verify(); err != nil {
return nil, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
}
return err
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
}
return err
}
return nil
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
return stdout.String(), stderr.String(), err
return driver, nil
}
+142
View File
@@ -0,0 +1,142 @@
package vmware
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Fusion5Driver is a driver that can run VMWare Fusion 5.
type Fusion5Driver struct {
// This is the path to the "VMware Fusion.app"
AppPath string
}
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless == true {
guiArgument = "nogui"
}
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Fusion5Driver) Verify() error {
if _, err := os.Stat(d.AppPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Fusion application not found at path: %s", d.AppPath)
}
return err
}
if _, err := os.Stat(d.vmrunPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application 'vmrun' not found at path: %s", d.vmrunPath())
}
return err
}
if _, err := os.Stat(d.vdiskManagerPath()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("Critical application vdisk manager not found at path: %s", d.vdiskManagerPath())
}
return err
}
return nil
}
func (d *Fusion5Driver) vdiskManagerPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmware-vdiskmanager")
}
func (d *Fusion5Driver) vmrunPath() string {
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
}
func (d *Fusion5Driver) ToolsIsoPath(k string) string {
return filepath.Join(d.AppPath, "Contents", "Library", "isoimages", k+".iso")
}
func (d *Fusion5Driver) DhcpLeasesPath(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
return stdout.String(), stderr.String(), err
}
+168
View File
@@ -0,0 +1,168 @@
package vmware
import (
"bytes"
"errors"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
)
// Workstation9LinuxDriver is a driver that can run VMware Workstation 9
// on Linux.
type Workstation9LinuxDriver struct {
AppPath string
VdiskManagerPath string
VmrunPath string
}
func (d *Workstation9LinuxDriver) CompactDisk(diskPath string) error {
defragCmd := exec.Command(d.VdiskManagerPath, "-d", diskPath)
if _, _, err := d.runAndLog(defragCmd); err != nil {
return err
}
shrinkCmd := exec.Command(d.VdiskManagerPath, "-k", diskPath)
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) CreateDisk(output string, size string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) IsRunning(vmxPath string) (bool, error) {
vmxPath, err := filepath.Abs(vmxPath)
if err != nil {
return false, err
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "list")
stdout, _, err := d.runAndLog(cmd)
if err != nil {
return false, err
}
for _, line := range strings.Split(stdout, "\n") {
if line == vmxPath {
return true, nil
}
}
return false, nil
}
func (d *Workstation9LinuxDriver) Start(vmxPath string, headless bool) error {
guiArgument := "gui"
if headless {
guiArgument = "nogui"
}
cmd := exec.Command(d.VmrunPath, "-T", "ws", "start", vmxPath, guiArgument)
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) Stop(vmxPath string) error {
cmd := exec.Command(d.VmrunPath, "-T", "ws", "stop", vmxPath, "hard")
if _, _, err := d.runAndLog(cmd); err != nil {
return err
}
return nil
}
func (d *Workstation9LinuxDriver) Verify() error {
if err := d.findApp(); err != nil {
return fmt.Errorf("VMware Workstation application ('vmware') not found in path.")
}
if err := d.findVmrun(); err != nil {
return fmt.Errorf("Required application 'vmrun' not found in path.")
}
if err := d.findVdiskManager(); err != nil {
return fmt.Errorf("Required application 'vmware-vdiskmanager' not found in path.")
}
// Check to see if it APPEARS to be licensed.
matches, err := filepath.Glob("/etc/vmware/license-*")
if err != nil {
return fmt.Errorf("Error looking for VMware license: %s", err)
}
if len(matches) == 0 {
return errors.New("Workstation does not appear to be licensed. Please license it.")
}
return nil
}
func (d *Workstation9LinuxDriver) findApp() error {
path, err := exec.LookPath("vmware")
if err != nil {
return err
}
d.AppPath = path
return nil
}
func (d *Workstation9LinuxDriver) findVdiskManager() error {
path, err := exec.LookPath("vmware-vdiskmanager")
if err != nil {
return err
}
d.VdiskManagerPath = path
return nil
}
func (d *Workstation9LinuxDriver) findVmrun() error {
path, err := exec.LookPath("vmrun")
if err != nil {
return err
}
d.VmrunPath = path
return nil
}
func (d *Workstation9LinuxDriver) ToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func (d *Workstation9LinuxDriver) DhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
}
func (d *Workstation9LinuxDriver) runAndLog(cmd *exec.Cmd) (string, string, error) {
var stdout, stderr bytes.Buffer
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("VMware error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return stdout.String(), stderr.String(), err
}
+4 -2
View File
@@ -2,7 +2,6 @@ package vmware
import (
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
@@ -18,6 +17,9 @@ type GuestIPFinder interface {
// DHCPLeaseGuestLookup looks up the IP address of a guest using DHCP
// lease information from the VMware network devices.
type DHCPLeaseGuestLookup struct {
// Driver that is being used (to find leases path)
Driver Driver
// Device that the guest is connected to.
Device string
@@ -26,7 +28,7 @@ type DHCPLeaseGuestLookup struct {
}
func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
fh, err := os.Open(fmt.Sprintf("/var/db/vmware/vmnet-dhcpd-%s.leases", f.Device))
fh, err := os.Open(f.Driver.DhcpLeasesPath(f.Device))
if err != nil {
return "", err
}
+1 -1
View File
@@ -33,7 +33,7 @@ func (f *IfconfigIPFinder) HostIP() (string, error) {
return "", err
}
re := regexp.MustCompile(`inet\s*(.+?)\s`)
re := regexp.MustCompile(`inet\s*(?:addr:)?(.+?)\s`)
matches := re.FindStringSubmatch(stdout.String())
if matches == nil {
return "", errors.New("IP not found in ifconfig output...")
+2
View File
@@ -12,6 +12,7 @@ import (
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(*config)
driver := state["driver"].(Driver)
vmxPath := state["vmx_path"].(string)
log.Println("Lookup up IP information...")
@@ -37,6 +38,7 @@ func sshAddress(state map[string]interface{}) (string, error) {
}
ipLookup := &DHCPLeaseGuestLookup{
Driver: driver,
Device: "vmnet8",
MACAddress: macAddress,
}
+30 -10
View File
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"log"
"os"
"regexp"
"strings"
)
@@ -22,28 +23,47 @@ import (
type stepCleanVMX struct{}
func (s stepCleanVMX) Run(state map[string]interface{}) multistep.StepAction {
if _, ok := state["floppy_path"]; !ok {
return multistep.ActionContinue
}
isoPath := state["iso_path"].(string)
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
ui.Say("Cleaning VMX prior to finishing up...")
vmxData, err := s.readVMX(vmxPath)
if err != nil {
state["error"] = fmt.Errorf("Error reading VMX: %s", err)
return multistep.ActionHalt
}
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Say("Unmounting floppy from VMX...")
if _, ok := state["floppy_path"]; ok {
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Message("Unmounting floppy from VMX...")
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
}
}
vmxData["floppy0.present"] = "FALSE"
}
ui.Message("Detatching ISO from CD-ROM device...")
devRe := regexp.MustCompile(`^ide\d:\d\.`)
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
match := devRe.FindString(k)
if match == "" {
continue
}
filenameKey := match + ".filename"
if filename, ok := vmxData[filenameKey]; ok {
if filename == isoPath {
// Change the CD-ROM device back to auto-detect to eject
vmxData[filenameKey] = "auto detect"
vmxData[match+".deviceType"] = "cdrom-raw"
}
}
}
vmxData["floppy0.present"] = "FALSE"
// Rewrite the VMX
if err := WriteVMX(vmxPath, vmxData); err != nil {
+14
View File
@@ -95,8 +95,22 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
func vncSendString(c *vnc.ClientConn, original string) {
special := make(map[string]uint32)
special["<bs>"] = 0xFF08
special["<del>"] = 0xFFFF
special["<enter>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<f1>"] = 0xFFBE
special["<f2>"] = 0xFFBF
special["<f3>"] = 0xFFC0
special["<f4>"] = 0xFFC1
special["<f5>"] = 0xFFC2
special["<f6>"] = 0xFFC3
special["<f7>"] = 0xFFC4
special["<f8>"] = 0xFFC5
special["<f9>"] = 0xFFC6
special["<f10>"] = 0xFFC7
special["<f11>"] = 0xFFC8
special["<f12>"] = 0xFFC9
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
+5
View File
@@ -250,6 +250,11 @@ func (c Command) Run(env packer.Environment, args []string) int {
env.Ui().Say("\n==> Builds finished but no artifacts were created.")
}
if len(errors) > 0 {
// If any errors occurred, exit with a non-zero exit status
return 1
}
return 0
}
+48 -10
View File
@@ -1,6 +1,7 @@
package ssh
import (
"bufio"
"bytes"
"code.google.com/p/go.crypto/ssh"
"errors"
@@ -106,12 +107,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
// Set stderr/stdout to a bytes buffer
stderr := new(bytes.Buffer)
stdout := new(bytes.Buffer)
session.Stderr = stderr
session.Stdout = stdout
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
@@ -120,10 +115,26 @@ func (c *comm) Upload(path string, input io.Reader) error {
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash seperators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
log.Println("Starting remote scp process in sink mode")
@@ -143,11 +154,17 @@ func (c *comm) Upload(path string, input io.Reader) error {
// Start the protocol
log.Println("Beginning file upload...")
fmt.Fprintln(w, "C0644", input_memory.Len(), target_file)
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
io.Copy(w, input_memory)
fmt.Fprint(w, "\x00")
// TODO(mitchellh): Each step above results in a 0/1/2 being sent by
// the remote side to confirm. We should check for those confirmations.
err = checkSCPStatus(stdoutR)
if err != nil {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
@@ -178,7 +195,6 @@ func (c *comm) Upload(path string, input io.Reader) error {
return err
}
log.Printf("scp stdout (length %d): %#v", stdout.Len(), stdout.Bytes())
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
return nil
@@ -223,3 +239,25 @@ func (c *comm) reconnect() (err error) {
return
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("Error reading error message: %s", err)
}
return errors.New(string(message))
}
return nil
}
+1
View File
@@ -20,6 +20,7 @@ const defaultConfig = `
"builders": {
"amazon-ebs": "packer-builder-amazon-ebs",
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
+3
View File
@@ -46,6 +46,9 @@ func main() {
packer.Version, packer.VersionPrerelease, packer.GitCommit)
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
// Prepare stdin for plugin usage by switching it to a pipe
setupStdin()
config, err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
+21 -2
View File
@@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"sync"
)
@@ -14,6 +15,9 @@ type Cache interface {
// Packer guarantees that no other process will write to this file while
// the lock is held.
//
// If the key has an extension (e.g., file.ext), the resulting path
// will have that extension as well.
//
// The cache will block and wait for the lock.
Lock(string) string
@@ -44,7 +48,7 @@ func (f *FileCache) Lock(key string) string {
rw := f.rwLock(hashKey)
rw.Lock()
return filepath.Join(f.CacheDir, hashKey)
return f.cachePath(key, hashKey)
}
func (f *FileCache) Unlock(key string) {
@@ -58,7 +62,7 @@ func (f *FileCache) RLock(key string) (string, bool) {
rw := f.rwLock(hashKey)
rw.RLock()
return filepath.Join(f.CacheDir, hashKey), true
return f.cachePath(key, hashKey), true
}
func (f *FileCache) RUnlock(key string) {
@@ -67,6 +71,21 @@ func (f *FileCache) RUnlock(key string) {
rw.RUnlock()
}
func (f *FileCache) cachePath(key string, hashKey string) string {
suffix := ""
endIndex := strings.Index(key, "?")
if endIndex == -1 {
endIndex = len(key)
}
dotIndex := strings.LastIndex(key[0:endIndex], ".")
if dotIndex > -1 {
suffix = key[dotIndex:endIndex]
}
return filepath.Join(f.CacheDir, hashKey+suffix)
}
func (f *FileCache) hashKey(key string) string {
sha := sha256.New()
sha.Write([]byte(key))
+15 -4
View File
@@ -3,6 +3,7 @@ package packer
import (
"io/ioutil"
"os"
"strings"
"testing"
)
@@ -36,19 +37,29 @@ func TestFileCache(t *testing.T) {
defer os.RemoveAll(cacheDir)
cache := &FileCache{CacheDir: cacheDir}
path := cache.Lock("foo")
path := cache.Lock("foo.ext?foo=bar.foo")
defer cache.Unlock("foo.ext?foo=bar.foo")
if !strings.HasSuffix(path, ".ext") {
t.Fatalf("bad extension with question mark: %s", path)
}
path = cache.Lock("foo.iso")
if !strings.HasSuffix(path, ".iso") {
t.Fatalf("path doesn't end with suffix '%s': '%s'", ".iso", path)
}
err = ioutil.WriteFile(path, []byte("data"), 0666)
if err != nil {
t.Fatalf("error writing: %s", err)
}
cache.Unlock("foo")
cache.Unlock("foo.iso")
path, ok := cache.RLock("foo")
path, ok := cache.RLock("foo.iso")
if !ok {
t.Fatal("cache says key doesn't exist")
}
defer cache.RUnlock("foo")
defer cache.RUnlock("foo.iso")
data, err := ioutil.ReadFile(path)
if err != nil {
+68
View File
@@ -1,7 +1,9 @@
package packer
import (
"github.com/mitchellh/iochan"
"io"
"strings"
"time"
)
@@ -57,6 +59,72 @@ type Communicator interface {
Download(string, io.Writer) error
}
// StartWithUi runs the remote command and streams the output to any
// configured Writers for stdout/stderr, while also writing each line
// as it comes to a Ui.
func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
} else {
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
}
if r.Stderr == nil {
r.Stderr = stderr_w
} else {
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
}
// Start the command
if err := c.Start(r); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan int, 1)
stdoutCh := iochan.DelimReader(stdout_r, '\n')
stderrCh := iochan.DelimReader(stderr_r, '\n')
// Start the goroutine to watch for the exit
go func() {
defer stdout_w.Close()
defer stderr_w.Close()
r.Wait()
exitCh <- r.ExitStatus
}()
// Loop and get all our output
OutputLoop:
for {
select {
case output := <-stderrCh:
ui.Message(strings.TrimSpace(output))
case output := <-stdoutCh:
ui.Message(strings.TrimSpace(output))
case <-exitCh:
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel before finishing these first.
for output := range stdoutCh {
ui.Message(strings.TrimSpace(output))
}
for output := range stderrCh {
ui.Message(strings.TrimSpace(output))
}
return nil
}
// Wait waits for the remote command to complete.
func (r *RemoteCmd) Wait() {
for !r.Exited {
+71
View File
@@ -1,10 +1,81 @@
package packer
import (
"bytes"
"io"
"strings"
"testing"
"time"
)
type TestCommunicator struct {
Stderr io.Reader
Stdout io.Reader
}
func (c *TestCommunicator) Start(rc *RemoteCmd) error {
go func() {
if rc.Stdout != nil && c.Stdout != nil {
io.Copy(rc.Stdout, c.Stdout)
}
if rc.Stderr != nil && c.Stderr != nil {
io.Copy(rc.Stderr, c.Stderr)
}
}()
return nil
}
func (c *TestCommunicator) Upload(string, io.Reader) error {
return nil
}
func (c *TestCommunicator) Download(string, io.Writer) error {
return nil
}
func TestRemoteCmd_StartWithUi(t *testing.T) {
data := "hello\nworld\nthere"
originalOutput := new(bytes.Buffer)
rcOutput := new(bytes.Buffer)
uiOutput := new(bytes.Buffer)
rcOutput.WriteString(data)
testComm := &TestCommunicator{
Stdout: rcOutput,
}
testUi := &ReaderWriterUi{
Reader: new(bytes.Buffer),
Writer: uiOutput,
}
rc := &RemoteCmd{
Command: "test",
Stdout: originalOutput,
}
go func() {
time.Sleep(100 * time.Millisecond)
rc.Exited = true
}()
err := rc.StartWithUi(testComm, testUi)
if err != nil {
t.Fatalf("err: %s", err)
}
if uiOutput.String() != strings.TrimSpace(data)+"\n" {
t.Fatalf("bad output: '%s'", uiOutput.String())
}
if originalOutput.String() != data {
t.Fatalf("original is bad: '%s'", originalOutput.String())
}
}
func TestRemoteCmd_Wait(t *testing.T) {
var cmd RemoteCmd
+30
View File
@@ -22,3 +22,33 @@ func (e *MultiError) Error() string {
"%d error(s) occurred:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
// MultiErrorAppend is a helper function that will append more errors
// onto a MultiError in order to create a larger multi-error. If the
// original error is not a MultiError, it will be turned into one.
func MultiErrorAppend(err error, errs ...error) *MultiError {
if err == nil {
err = new(MultiError)
}
switch err := err.(type) {
case *MultiError:
if err == nil {
err = new(MultiError)
}
if err.Errors == nil {
err.Errors = make([]error, 0, len(errs))
}
err.Errors = append(err.Errors, errs...)
return err
default:
newErrs := make([]error, len(errs)+1)
newErrs[0] = err
copy(newErrs[1:], errs)
return &MultiError{
Errors: newErrs,
}
}
}
+25
View File
@@ -30,3 +30,28 @@ func TestMultiErrorError(t *testing.T) {
multi := &MultiError{errors}
assert.Equal(multi.Error(), expected, "should have proper error")
}
func TestMultiErrorAppend_MultiError(t *testing.T) {
original := &MultiError{
Errors: []error{errors.New("foo")},
}
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
original = &MultiError{}
result = MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 1 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}
func TestMultiErrorAppend_NonMultiError(t *testing.T) {
original := errors.New("foo")
result := MultiErrorAppend(original, errors.New("bar"))
if len(result.Errors) != 2 {
t.Fatalf("wrong len: %d", len(result.Errors))
}
}
+1
View File
@@ -216,6 +216,7 @@ func (c *Client) Start() (address string, err error) {
cmd := c.config.Cmd
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
cmd.Stdin = os.Stdin
cmd.Stderr = stderr
cmd.Stdout = stdout
+49
View File
@@ -1,6 +1,8 @@
package plugin
import (
"io/ioutil"
"os"
"testing"
"time"
)
@@ -47,3 +49,50 @@ func TestClient_Start_Timeout(t *testing.T) {
t.Fatal("err should not be nil")
}
}
func TestClient_Stdin(t *testing.T) {
// Overwrite stdin for this test with a temporary file
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err = tf.WriteString("hello"); err != nil {
t.Fatalf("error: %s", err)
}
if err = tf.Sync(); err != nil {
t.Fatalf("error: %s", err)
}
if _, err = tf.Seek(0, 0); err != nil {
t.Fatalf("error: %s", err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()
os.Stdin = tf
process := helperProcess("stdin")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
_, err = c.Start()
if err != nil {
t.Fatalf("error: %s", err)
}
for {
if c.Exited() {
break
}
time.Sleep(50 * time.Millisecond)
}
if !process.ProcessState.Success() {
t.Fatal("process didn't exit cleanly")
}
}
+14
View File
@@ -2,6 +2,7 @@ package plugin
import (
"fmt"
"log"
"os"
"os/exec"
"testing"
@@ -65,6 +66,19 @@ func TestHelperProcess(*testing.T) {
ServeProvisioner(new(helperProvisioner))
case "start-timeout":
time.Sleep(1 * time.Minute)
os.Exit(1)
case "stdin":
fmt.Println(":1234")
data := make([]byte, 5)
if _, err := os.Stdin.Read(data); err != nil {
log.Printf("stdin read error: %s", err)
os.Exit(100)
}
if string(data) == "hello" {
os.Exit(0)
}
os.Exit(1)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
-43
View File
@@ -1,43 +0,0 @@
package rpc
import (
"net"
"net/rpc"
)
// This starts a RPC server for the given interface listening on the
// given address. The RPC server is ready when "readyChan" receives a message
// and the RPC server will quit when "stopChan" receives a message.
//
// This function should be run in a goroutine.
func testRPCServer(laddr string, name string, iface interface{}, readyChan chan int, stopChan <-chan int) {
listener, err := net.Listen("tcp", laddr)
if err != nil {
panic(err)
}
// Close the listener when we exit so that the RPC server ends
defer listener.Close()
// Start the RPC server
server := rpc.NewServer()
server.RegisterName(name, iface)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
// If there is an error, just ignore it.
break
}
go server.ServeConn(conn)
}
}()
// We're ready!
readyChan <- 1
// Block on waiting to receive from the channel
<-stopChan
}
+4
View File
@@ -161,6 +161,10 @@ func ParseTemplate(data []byte) (t *Template, err error) {
continue
}
// Now that we have the name, remove it from the config - as the builder
// itself doesn't know about, and it will cause a validation error.
delete(v, "name")
raw.rawConfig = v
t.Builders[raw.Name] = raw
+13 -1
View File
@@ -128,6 +128,19 @@ func TestParseTemplate_BuilderWithName(t *testing.T) {
builder, ok := result.Builders["bob"]
assert.True(ok, "should have bob builder")
assert.Equal(builder.Type, "amazon-ebs", "builder should be amazon-ebs")
rawConfig := builder.rawConfig
if rawConfig == nil {
t.Fatal("missing builder raw config")
}
expected := map[string]interface{}{
"type": "amazon-ebs",
}
if !reflect.DeepEqual(rawConfig, expected) {
t.Fatalf("bad raw: %#v", rawConfig)
}
}
func TestParseTemplate_BuilderWithConflictingName(t *testing.T) {
@@ -484,7 +497,6 @@ func TestTemplate_Build(t *testing.T) {
`
expectedConfig := map[string]interface{}{
"name": "test1",
"type": "test-builder",
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
var GitCommit string
// The version of packer.
const Version = "0.2.0"
const Version = "0.2.1"
// Any pre-release marker for the version. If this is "" (empty string),
// then it means that it is a final release. Otherwise, this is the
+10
View File
@@ -0,0 +1,10 @@
package main
import (
"github.com/mitchellh/packer/builder/amazon/instance"
"github.com/mitchellh/packer/packer/plugin"
)
func main() {
plugin.ServeBuilder(new(instance.Builder))
}
+5 -1
View File
@@ -79,5 +79,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
}
defer f.Close()
return comm.Upload(p.config.Destination, f)
err = comm.Upload(p.config.Destination, f)
if err != nil {
ui.Error(fmt.Sprintf("Upload failed: %s", err))
}
return err
}
+4 -51
View File
@@ -7,10 +7,8 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/iochan"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
@@ -221,59 +219,14 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
t := template.Must(template.New("command").Parse(p.config.ExecuteCommand))
t.Execute(&command, &ExecuteCommandTemplate{flattendVars, p.config.RemotePath})
// Setup the remote command
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
var cmd packer.RemoteCmd
cmd.Command = command.String()
cmd.Stdout = stdout_w
cmd.Stderr = stderr_w
cmd := &packer.RemoteCmd{Command: command.String()}
log.Printf("Executing command: %s", cmd.Command)
err = comm.Start(&cmd)
if err != nil {
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Failed executing command: %s", err)
}
exitChan := make(chan int, 1)
stdoutChan := iochan.DelimReader(stdout_r, '\n')
stderrChan := iochan.DelimReader(stderr_r, '\n')
go func() {
defer stdout_w.Close()
defer stderr_w.Close()
cmd.Wait()
exitChan <- cmd.ExitStatus
}()
OutputLoop:
for {
select {
case output := <-stderrChan:
ui.Message(strings.TrimSpace(output))
case output := <-stdoutChan:
ui.Message(strings.TrimSpace(output))
case exitStatus := <-exitChan:
log.Printf("shell provisioner exited with status %d", exitStatus)
if exitStatus != 0 {
return fmt.Errorf("Script exited with non-zero exit status: %d", exitStatus)
}
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel first.
for output := range stdoutChan {
ui.Message(output)
}
for output := range stderrChan {
ui.Message(output)
if cmd.ExitStatus != 0 {
return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
}
}
+9 -2
View File
@@ -21,11 +21,18 @@ fi
echo "Version: ${VERSION} ${PREVERSION}"
# Determine the arch/os combos we're building for
XC_ARCH=${XC_ARCH:-"386 amd64 arm"}
XC_OS=${XC_OS:-linux darwin windows freebsd openbsd}
echo "Arch: ${XC_ARCH}"
echo "OS: ${XC_OS}"
# This function builds whatever directory we're in...
xc() {
goxc \
-arch="386 amd64 arm" \
-os="linux darwin windows freebsd openbsd" \
-arch="$XC_ARCH" \
-os="$XC_OS" \
-d="${DIR}/pkg" \
-pv="${VERSION}" \
-pr="${PREVERSION}" \
+5 -1
View File
@@ -15,8 +15,12 @@ func setupSignalHandlers(env packer.Environment) {
signal.Notify(ch, os.Interrupt)
go func() {
// First interrupt. We mostly ignore this because it allows the
// plugins time to cleanup.
<-ch
log.Println("First interrupt. Ignoring, will let plugins handle...")
log.Println("First interrupt. Ignoring to allow plugins to clean up.")
// Second interrupt. Go down hard.
<-ch
log.Println("Second interrupt. Exiting now.")
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"io"
"log"
"os"
"os/signal"
)
// setupStdin switches out stdin for a pipe. We do this so that we can
// close the writer end of the pipe when we receive an interrupt so plugins
// blocked on reading from stdin are unblocked.
func setupStdin() {
// Create the pipe and swap stdin for the reader end
r, w, _ := os.Pipe()
originalStdin := os.Stdin
os.Stdin = r
// Create a goroutine that copies data from the original stdin
// into the writer end of the pipe forever.
go func() {
defer w.Close()
io.Copy(w, originalStdin)
}()
// Register a signal handler for interrupt in order to close the
// writer end of our pipe so that readers get EOF downstream.
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
defer signal.Stop(ch)
defer w.Close()
<-ch
log.Println("Closing stdin because interrupt received.")
}()
}
+2 -2
View File
@@ -17,7 +17,7 @@ if !ENV["PACKER_DISABLE_DOWNLOAD_FETCH"]
response = http.request(req)
response.body.split("\n").each do |line|
next if line !~ /\/mitchellh\/packer\/(#{ENV["PACKER_VERSION"]}.+?)\?/
next if line !~ /\/mitchellh\/packer\/(#{Regexp.quote(ENV["PACKER_VERSION"])}.+?)'/
filename = $1.to_s
os = filename.split("_")[1]
next if os == "SHA256SUMS"
@@ -82,7 +82,7 @@ helpers do
end
def download_url(file)
"https://dl.bintray.com/mitchellh/packer/#{file}?direct"
"https://dl.bintray.com/mitchellh/packer/#{file}"
end
def latest_version
@@ -1,22 +1,17 @@
---
layout: "docs"
page_title: "Amazon AMI Builder (EBS backed)"
---
# Amazon AMI Builder
# AMI Builder (EBS backed)
Type: `amazon-ebs`
The `amazon-ebs` builder is able to create Amazon AMIs backed by EBS
volumes for use in [EC2](http://aws.amazon.com/ec2/). The builder takes
an initial source AMI, runs any provisioning necesary on the instance,
and snapshots it into a reusable AMI.
Amazon supports two types of AMIs: EBS-backed and instance-store. Instance
store AMIs are considerably harder to create, requiring many platform-specific
steps that can often take a very long time. EBS-backed AMIs, on the hand,
only require a source AMI to exist. This builder only builds EBS-backed
instances, because they are easier to create, especially across many
platforms running Packer.
volumes for use in [EC2](http://aws.amazon.com/ec2/). For more information
on the difference betwen EBS-backed instances and instance-store backed
instances, see the
["storage for the root device" section in the EC2 documentation](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ComponentsAMIs.html#storage-for-the-root-device).
This builder builds an AMI by launching an EC2 instance from a source AMI,
provisioning that running machine, and then creating an AMI from that machine.
@@ -76,6 +71,12 @@ Optional:
before timing out. The format of this value is a duration such as "5s"
or "5m". The default SSH timeout is "1m", or one minute.
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC.
## Basic Example
Here is a basic example. It is completely valid except for the access keys:
@@ -0,0 +1,217 @@
---
layout: "docs"
page_title: "Amazon AMI Builder (instance-store)"
---
# AMI Builder (instance-store)
Type: `amazon-instance`
The `amazon-instance` builder is able to create Amazon AMIs backed by
instance storage as the root device. For more information on the difference
between instance storage and EBS-backed instances, see the
["storage for the root device" section in the EC2 documentation](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ComponentsAMIs.html#storage-for-the-root-device).
This builder builds an AMI by launching an EC2 instance from an existing
instance-storage backed AMI, provisioning that running machine, and then
bundling and creating a new AMI from that machine.
This is all done in your own AWS account. The builder will create temporary
keypairs, security group rules, etc. that provide it temporary access to
the instance while the image is being created. This simplifies configuration
quite a bit.
The builder does _not_ manage AMIs. Once it creates an AMI and stores it
in your account, it is up to you to use, delete, etc. the AMI.
## Configuration Reference
There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
Required:
* `access_key` (string) - The access key used to communicate with AWS.
If not specified, Packer will attempt to read this from environmental
variables `AWS_ACCESS_KEY_ID` or `AWS_ACCESS_KEY` (in that order).
* `account_id` (string) - Your AWS account ID. This is required for bundling
the AMI. This is _not the same_ as the access key. You can find your
account ID in the security credentials page of your AWS account.
* `ami_name` (string) - The name of the resulting AMI that will appear
when managing AMIs in the AWS console or via APIs. This must be unique.
To help make this unique, certain template parameters are available for
this value, which are documented below.
* `instance_type` (string) - The EC2 instance type to use while building
the AMI, such as "m1.small".
* `region` (string) - The name of the region, such as "us-east-1", in which
to launch the EC2 instance to create the AMI.
* `s3_bucket` (string) - The name of the S3 bucket to upload the AMI.
This bucket will be created if it doesn't exist.
* `secret_key` (string) - The secret key used to communicate with AWS.
If not specified, Packer will attempt to read this from environmental
variables `AWS_SECRET_ACCESS_KEY` or `AWS_SECRET_KEY` (in that order).
* `source_ami` (string) - The initial AMI used as a base for the newly
created machine.
* `ssh_username` (string) - The username to use in order to communicate
over SSH to the running machine.
* `x509_cert_path` (string) - The local path to a valid X509 certificate for
your AWS account. This is used for bundling the AMI. This X509 certificate
must be registered with your account from the security credentials page
in the AWS console.
* `x509_key_path` (string) - The local path to the private key for the X509
certificate specified by `x509_cert_path`. This is used for bundling the AMI.
Optional:
* `bundle_destination` (string) - The directory on the running instance
where the bundled AMI will be saved prior to uploading. By default this is
"/tmp". This directory must exist and be writable.
* `bundle_prefix` (string) - The prefix for files created from bundling
the root volume. By default this is "image-{{.Createtime}}". The `CreateTime`
variable should be used to make sure this is unique, otherwise it can
collide with other created AMIs by Packer in your account.
* `bundle_upload_command` (string) - The command to use to upload the
bundled volume. See the "custom bundle commands" section below for more
information.
* `bundle_vol_command` (string) - The command to use to bundle the volume.
See the "custom bundle commands" section below for more information.
* `security_group_id` (string) - The ID (_not_ the name) of the security
group to assign to the instance. By default this is not set and Packer
will automatically create a new temporary security group to allow SSH
access. Note that if this is specified, you must be sure the security
group allows access to the `ssh_port` given below.
* `ssh_port` (int) - The port that SSH will be available on. This defaults
to port 22.
* `ssh_timeout` (string) - The time to wait for SSH to become available
before timing out. The format of this value is a duration such as "5s"
or "5m". The default SSH timeout is "1m", or one minute.
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC.
* `x509_upload_path` (string) - The path on the remote machine where the
X509 certificate will be uploaded. This path must already exist and be
writable. X509 certificates are uploaded after provisioning is run, so
it is perfectly okay to create this directory as part of the provisioning
process.
## Basic Example
Here is a basic example. It is completely valid except for the access keys:
<pre class="prettyprint">
{
"type": "amazon-instance",
"access_key": "YOUR KEY HERE",
"secret_key": "YOUR SECRET KEY HERE",
"region": "us-east-1",
"source_ami": "ami-d9d6a6b0",
"instance_type": "m1.small",
"ssh_username": "ubuntu",
"account_id": "0123-4567-0890",
"s3_bucket": "packer-images",
"x509_cert_path": "x509.cert",
"x509_key_path": "x509.key",
"x509_upload_path": "/tmp",
"ami_name": "packer-quick-start {{.CreateTime}}"
}
</pre>
<div class="alert alert-block alert-info">
<strong>Note:</strong> Packer can also read the access key and secret
access key from environmental variables. See the configuration reference in
the section above for more information on what environmental variables Packer
will look for.
</div>
## AMI Name Variables
The AMI name specified by the `ami_name` configuration variable is actually
treated as a [configuration template](/docs/templates/configuration-templates.html).
Packer provides a set of variables that it will replace
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
The available variables are shown below:
* `CreateTime` - This will be replaced with the Unix timestamp of when
the AMI was built.
## Custom Bundle Commands
A lot of the process required for creating an instance-store backed AMI
involves commands being run on the actual source instance. Specifically, the
`ec2-bundle-vol` and `ec2-upload-bundle` commands must be used to bundle
the root filesystem and upload it, respectively.
Each of these commands have a lot of available flags. Instead of exposing each
possible flag as a template configuration option, the instance-store AMI
builder for Packer lets you customize the entire command used to bundle
and upload the AMI.
These are configured with `bundle_vol_command` and `bundle_upload_command`.
Both of these configurations are
[configuration templates](/docs/templates/configuration-templates.html)
and have support for their own set of template variables.
### Bundle Volume Command
The default value for `bundle_vol_command` is shown below. It is split
across multiple lines for convenience of reading. The bundle volume command
is responsible for executing `ec2-bundle-vol` in order to store and image
of the root filesystem to use to create the AMI.
```
sudo -n ec2-bundle-vol \
-k {{.KeyPath}} \
-u {{.AccountId}} \
-c {{.CertPath}} \
-r {{.Architecture}} \
-e {{.PrivatePath}} \
-d {{.Destination}} \
-p {{.Prefix}} \
--batch
```
The available template variables should be self-explanatory based on the
parameters they're used to satisfy the `ec2-bundle-vol` command.
### Bundle Upload Command
The default value for `bundle_upload_command` is shown below. It is split
across multiple lines for convenience of reading. The bundle upload command
is responsible for taking the bundled volume and uploading it to S3.
```
sudo -n ec2-upload-bundle \
-b {{.BucketName}} \
-m {{.ManifestPath}} \
-a {{.AccessKey}} \
-s {{.SecretKey}} \
-d {{.BundleDirectory}} \
--batch \
--retry
```
The available template variables should be self-explanatory based on the
parameters they're used to satisfy the `ec2-upload-bundle` command.
@@ -0,0 +1,25 @@
---
layout: "docs"
page_title: "Amazon AMI Builder"
---
# Amazon AMI Builder
Packer is able to create Amazon AMIs. To achieve this, Packer comes with
multiple builders depending on the strategy you want to use to build the
AMI. Packer supports the following builders at the moment:
* [amazon-ebs](/docs/builders/amazon-ebs.html) - Create EBS-backed AMIs
by launching a source instance and re-packaging it into a new AMI after
provisioning. If in doubt, use this builder, which is the easiest to get
started with.
* [amazon-instance](/docs/builders/amazon-instance.html) - Create
instance-store AMIs by launching and provisioning a source instance, then
rebundling it and uploading it to S3.
<div class="alert alert-block alert-info">
<strong>Don't know which builder to use?</strong> If in doubt, use the
<a href="/docs/builders/amazon-ebs.html">amazon-ebs builder</a>. It is
much easier to use and Amazon generally recommends EBS-backed images nowadays.
</div>
+8
View File
@@ -37,5 +37,13 @@ page_title: "Downloads"
</div>
</div>
<% end %>
<div class="row">
<div class="span8 offset2 poweredby">
<a href='http://www.bintray.com'>
<img src='http://www.bintray.com/docs/images/poweredByBintray_ColorTransparent.png'>
</a>
</div>
</div>
</div>
</section>
@@ -42,6 +42,7 @@ block below.
"provisioners": [{
"type": "shell",
"inline": [
"sleep 30",
"sudo apt-get update",
"sudo apt-get install -y redis-server"
]
@@ -49,6 +50,13 @@ block below.
}
</pre>
<div class="alert alert-block alert-info">
<strong>Note:</strong> The <code>sleep 30</code> in the example above is
very important. Because Packer is able to detect and SSH into the instance
as soon as SSH is available, Ubuntu actually doesn't get proper amounts
of time to initialize. The sleep makes sure that the OS properly initializes.
</div>
Hopefully it is obvious, but the `builders` section shouldn't actually
contain "...", it should be the contents setup in the previous page
of the getting started guide.
+1 -1
View File
@@ -27,7 +27,7 @@
<ul>
<li><h4>Builders</h4></li>
<li><a href="/docs/builders/amazon-ebs.html">Amazon EC2 (AMI)</a></li>
<li><a href="/docs/builders/amazon.html">Amazon EC2 (AMI)</a></li>
<li><a href="/docs/builders/digitalocean.html">DigitalOcean</a></li>
<li><a href="/docs/builders/virtualbox.html">VirtualBox</a></li>
<li><a href="/docs/builders/vmware.html">VMware</a></li>
@@ -118,6 +118,17 @@ header .header {
margin-bottom: -3px;
}
}
.poweredby {
margin-top: 20px;
margin-bottom: 20px;
img {
display: block;
margin: 0 auto;
width: 122px;
}
}
}
.marketting {