Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 801a13dd38 | |||
| e5b14ac450 | |||
| e6557c53c2 | |||
| b455d70346 | |||
| 48b08fe71a | |||
| 74ef543d5c | |||
| 9f38f4cb47 | |||
| 7a00b240c1 | |||
| f5f4266371 | |||
| 1ec2eacb92 | |||
| 88dcb43a3f | |||
| c5aad085f3 | |||
| 41df9fa2e8 | |||
| 029dabaa49 | |||
| 4857faa363 | |||
| 0130b1310c | |||
| 30a4bf7687 | |||
| 8c3281405d | |||
| 072d7a647a | |||
| 77b92cd048 | |||
| 17f6914fc1 | |||
| 5e6695264a | |||
| ad1015f35c | |||
| 801541714e | |||
| 3f543dddaa | |||
| b5552c00f3 | |||
| c6358b0475 | |||
| 544b4b1429 | |||
| 3efdb009d7 | |||
| b9b5de5684 | |||
| a5546fa0f3 | |||
| b11b60746b | |||
| c39299c21f | |||
| d11cabf893 | |||
| caaa6667ed | |||
| b91fb5e1a0 | |||
| a780d0e0dd | |||
| 5a4f5c3f6f | |||
| 5c7c0a6ee2 | |||
| 51f7db35f4 | |||
| 579eaaabc5 | |||
| f8319f8881 | |||
| e67e4cfa16 | |||
| 5921492c8e | |||
| d489f99aad | |||
| ae864b3efa | |||
| 9fb778c012 | |||
| 45d50279f3 | |||
| 6fdf237c15 | |||
| 8f649af165 | |||
| 9348f481dd | |||
| 17acee653b | |||
| 1c5e9848b5 | |||
| 479855a4f1 | |||
| 3641c1a708 | |||
| 2e82358d2a | |||
| 32c84c17b1 | |||
| bd1fe21718 | |||
| 6422fabd19 | |||
| 54fb31b7a3 | |||
| 51b63efe50 | |||
| 282554b2b0 | |||
| 38ae1a0ba9 | |||
| e3478c38ef | |||
| 7cab4c4058 | |||
| 9be941ec3e | |||
| e51e9cd56e | |||
| 9c393742df | |||
| f64fd019e5 | |||
| cf9bc2c819 | |||
| 9349c81631 | |||
| f97148bc22 | |||
| cff38c6aea | |||
| 6028a3c3de | |||
| d179d1ad00 | |||
| 20d3946eae | |||
| bc4b2d079d | |||
| 27da543a41 | |||
| fd1ec31a1d | |||
| 7e6e699593 | |||
| 7d1f799ced | |||
| 40ec63d349 | |||
| 5dbae2efde | |||
| d1ecd89635 | |||
| 6f6a27b008 | |||
| 5b8c372fdc | |||
| c80db71361 | |||
| abf4cfecb2 | |||
| 27fc4da5f9 | |||
| 0784f7961b | |||
| 71c0b2c1d5 | |||
| 9f93ca7d3b | |||
| bf3e137a5a | |||
| f226bd13af | |||
| dca4fabd1b | |||
| a31fd29b8b | |||
| 0f1bc9ee6f | |||
| ed75091bd4 | |||
| a8a53fb0b4 | |||
| 104178080c | |||
| 923a541d8c | |||
| bca05ddd5b | |||
| 04b8ff89a6 | |||
| b9f0b81821 | |||
| 56402f9432 | |||
| cbddd413f1 | |||
| 229011def4 | |||
| d7e5a3bb1d | |||
| 8cb035cc65 | |||
| e6dcb739d7 |
@@ -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:
|
||||
|
||||
@@ -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 ./...
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+33
-14
@@ -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)
|
||||
}
|
||||
+29
-17
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}) {}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}" \
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user