Commit 9c5845e3 authored by Chris Bednarski's avatar Chris Bednarski

Merge branch 'master' into f-file-builder

parents e60b22d4 523a3342
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
BACKWARDS INCOMPATIBILITIES: BACKWARDS INCOMPATIBILITIES:
* core: SSH connection will no longer request a PTY by default. This
can be enabled per builder.
* builder/digitalocean: no longer supports the v1 API which has been * builder/digitalocean: no longer supports the v1 API which has been
deprecated for some time. Most configurations should continue to deprecated for some time. Most configurations should continue to
work as long as you use the `api_token` field for auth. work as long as you use the `api_token` field for auth.
...@@ -11,12 +13,32 @@ BACKWARDS INCOMPATIBILITIES: ...@@ -11,12 +13,32 @@ BACKWARDS INCOMPATIBILITIES:
FEATURES: FEATURES:
* **WinRM:** You can now connect via WinRM with almost every builder.
See the docs for more info. [GH-2239]
* **Windows AWS Support:** Windows AMIs can now be built without any
external plugins: Packer will start a Windows instance, get the
admin password, and can use WinRM (above) to connect through. [GH-2240]
* **Disable SSH:** Set `communicator` to "none" in any builder to disable SSH
connections. Note that provisioners won't work if this is done. [GH-1591]
* **SSH Agent Forwarding:** SSH Agent Forwarding will now be enabled
to allow access to remote servers such as private git repos. [GH-1066]
* **Docker builder supports SSH**: The Docker builder now supports containers
with SSH, just set `communicator` to "ssh" [GH-2244]
* **New config function: `build_name`**: The name of the currently running
build. [GH-2232]
* **New config function: `build_type`**: The type of the currently running
builder. This is useful for provisioners. [GH-2232]
* **New config function: `template_dir`**: The directory to the template * **New config function: `template_dir`**: The directory to the template
being built. This should be used for template-relative paths. [GH-54] being built. This should be used for template-relative paths. [GH-54]
IMPROVEMENTS: IMPROVEMENTS:
* core: Interrupt handling for SIGTERM signal as well. [GH-1858] * core: Interrupt handling for SIGTERM signal as well. [GH-1858]
* builder/*: Add `ssh_handshake_attempts` to configure the number of
handshake attempts done before failure [GH-2237]
* builder/amazon: Add `force_deregister` option for automatic AMI
deregistration [GH-2221]
* builder/amazon: Now applies tags to EBS snapshots [GH-2212]
* builder/digitalocean: Save SSH key to pwd if debug mode is on. [GH-1829] * builder/digitalocean: Save SSH key to pwd if debug mode is on. [GH-1829]
* builder/digitalocean: User data support [GH-2113] * builder/digitalocean: User data support [GH-2113]
* builder/parallels: Support Parallels Desktop 11 [GH-2199] * builder/parallels: Support Parallels Desktop 11 [GH-2199]
...@@ -26,10 +48,14 @@ IMPROVEMENTS: ...@@ -26,10 +48,14 @@ IMPROVEMENTS:
have prohibitive firewalls have prohibitive firewalls
* builder/openstack: Flavor names can be used as well as refs * builder/openstack: Flavor names can be used as well as refs
* builder/openstack: Add `availability_zone` [GH-2016] * builder/openstack: Add `availability_zone` [GH-2016]
* builder/openstack: Machine will be stopped prior to imaging if the
cluster supports the `startstop` extension. [GH-2223]
* builder/openstack: Support for user data [GH-2224]
* builder/virtualbox: Added option: `ssh_skip_nat_mapping` to skip the * builder/virtualbox: Added option: `ssh_skip_nat_mapping` to skip the
automatic port forward for SSH and to use the guest port directly. [GH-1078] automatic port forward for SSH and to use the guest port directly. [GH-1078]
* builder/virtualbox: Added SCSI support * builder/virtualbox: Added SCSI support
* builder/vmware: Support for additional disks [GH-1382] * builder/vmware: Support for additional disks [GH-1382]
* command/fix: After fixing, the template is validated [GH-2228]
* command/push: Add `-name` flag for specifying name from CLI [GH-2042] * command/push: Add `-name` flag for specifying name from CLI [GH-2042]
* command/push: Push configuration in templates supports variables [GH-1861] * command/push: Push configuration in templates supports variables [GH-1861]
* post-processor/docker-save: Can be chained [GH-2179] * post-processor/docker-save: Can be chained [GH-2179]
...@@ -39,6 +65,7 @@ IMPROVEMENTS: ...@@ -39,6 +65,7 @@ IMPROVEMENTS:
BUG FIXES: BUG FIXES:
* core: Fix potential panic for post-processor plugin exits [GH-2098] * core: Fix potential panic for post-processor plugin exits [GH-2098]
* core: `PACKER_CONFIG` may point to a non-existent file [GH-2226]
* builder/amazon: Allow spaces in AMI names when using `clean_ami_name` [GH-2182] * builder/amazon: Allow spaces in AMI names when using `clean_ami_name` [GH-2182]
* builder/amazon: Remove deprecated ec2-upload-bundle paramger [GH-1931] * builder/amazon: Remove deprecated ec2-upload-bundle paramger [GH-1931]
* builder/amazon: Use IAM Profile to upload bundle if provided [GH-1985] * builder/amazon: Use IAM Profile to upload bundle if provided [GH-1985]
...@@ -66,13 +93,18 @@ BUG FIXES: ...@@ -66,13 +93,18 @@ BUG FIXES:
* builder/docker: Fix crash that could occur at certain timed ctrl-c [GH-1838] * builder/docker: Fix crash that could occur at certain timed ctrl-c [GH-1838]
* builder/docker: validate that `export_path` is not a directory [GH-2105] * builder/docker: validate that `export_path` is not a directory [GH-2105]
* builder/google: `ssh_timeout` is respected [GH-1781] * builder/google: `ssh_timeout` is respected [GH-1781]
* builder/openstack: `ssh_interface` can be used to specify the interface
to retrieve the SSH IP from. [GH-2220]
* builder/qemu: Add `disk_discard` option [GH-2120] * builder/qemu: Add `disk_discard` option [GH-2120]
* builder/qemu: Use proper SSH port, not hardcoded to 22. [GH-2236]
* builder/virtualbox: Bind HTTP server to IPv4, which is more compatible with * builder/virtualbox: Bind HTTP server to IPv4, which is more compatible with
OS installers. [GH-1709] OS installers. [GH-1709]
* builder/virtualbox: Remove the floppy controller in addition to the * builder/virtualbox: Remove the floppy controller in addition to the
floppy disk. [GH-1879] floppy disk. [GH-1879]
* builder/virtualbox: Fixed regression where downloading ISO without a * builder/virtualbox: Fixed regression where downloading ISO without a
".iso" extension didn't work. [GH-1839] ".iso" extension didn't work. [GH-1839]
* builder/virtualbox: Output dir is verified at runtime, not template
validation time. [GH-2233]
* builder/vmware: Add 100ms delay between keystrokes to avoid subtle * builder/vmware: Add 100ms delay between keystrokes to avoid subtle
timing issues in most cases. [GH-1663] timing issues in most cases. [GH-1663]
* builder/vmware: Bind HTTP server to IPv4, which is more compatible with * builder/vmware: Bind HTTP server to IPv4, which is more compatible with
...@@ -80,6 +112,10 @@ BUG FIXES: ...@@ -80,6 +112,10 @@ BUG FIXES:
* builder/vmware: Case-insensitive match of MAC address to find IP [GH-1989] * builder/vmware: Case-insensitive match of MAC address to find IP [GH-1989]
* builder/vmware: More robust IP parsing from ifconfig output [GH-1999] * builder/vmware: More robust IP parsing from ifconfig output [GH-1999]
* builder/vmware: Nested output directories for ESXi work [GH-2174] * builder/vmware: Nested output directories for ESXi work [GH-2174]
* builder/vmware: Output dir is verified at runtime, not template
validation time. [GH-2233]
* command/fix: For the `virtualbox` to `virtualbox-iso` builder rename,
provisioner overrides are now also fixed [GH-2231]
* command/validate: don't crash for invalid builds [GH-2139] * command/validate: don't crash for invalid builds [GH-2139]
* post-processor/atlas: Find common archive prefix for Windows [GH-1874] * post-processor/atlas: Find common archive prefix for Windows [GH-1874]
* post-processor/atlas: Fix index out of range panic [GH-1959] * post-processor/atlas: Fix index out of range panic [GH-1959]
...@@ -89,6 +125,7 @@ BUG FIXES: ...@@ -89,6 +125,7 @@ BUG FIXES:
* provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call * provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call
* provisioner/shell: chmod executable script to 0755, not 0777 [GH-1708] * provisioner/shell: chmod executable script to 0755, not 0777 [GH-1708]
* provisioner/shell: inline commands failing will fail the provisioner [GH-2069] * provisioner/shell: inline commands failing will fail the provisioner [GH-2069]
* provisioner/shell: single quotes in env vars are escaped [GH-2229]
## 0.7.5 (December 9, 2014) ## 0.7.5 (December 9, 2014)
......
...@@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&StepInstanceInfo{}, &StepInstanceInfo{},
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
...@@ -164,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -164,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepChrootProvision{}, &StepChrootProvision{},
&StepEarlyCleanup{}, &StepEarlyCleanup{},
&StepSnapshot{}, &StepSnapshot{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{}, &StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig, AccessConfig: &b.config.AccessConfig,
......
...@@ -17,6 +17,7 @@ type AMIConfig struct { ...@@ -17,6 +17,7 @@ type AMIConfig struct {
AMIRegions []string `mapstructure:"ami_regions"` AMIRegions []string `mapstructure:"ami_regions"`
AMITags map[string]string `mapstructure:"tags"` AMITags map[string]string `mapstructure:"tags"`
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"` AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
AMIForceDeregister bool `mapstructure:"force_deregister"`
} }
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error { func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
...@@ -21,40 +22,32 @@ type RunConfig struct { ...@@ -21,40 +22,32 @@ type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"` SourceAmi string `mapstructure:"source_ami"`
SpotPrice string `mapstructure:"spot_price"` SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"` SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"` SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"` SecurityGroupIds []string `mapstructure:"security_group_ids"`
SubnetId string `mapstructure:"subnet_id"` SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"` TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
UserData string `mapstructure:"user_data"` UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"` UserDataFile string `mapstructure:"user_data_file"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
VpcId string `mapstructure:"vpc_id"` VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others // Communicator settings
sshTimeout time.Duration Comm communicator.Config `mapstructure:",squash"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
} }
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.TemporaryKeyPairName == "" { if c.TemporaryKeyPairName == "" {
c.TemporaryKeyPairName = fmt.Sprintf( c.TemporaryKeyPairName = fmt.Sprintf(
"packer %s", uuid.TimeOrderedUUID()) "packer %s", uuid.TimeOrderedUUID())
} }
if c.WindowsPasswordTimeout == 0 {
c.WindowsPasswordTimeout = 10 * time.Minute
}
// Validation // Validation
var errs []error errs := c.Comm.Prepare(ctx)
if c.SourceAmi == "" { if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified")) errs = append(errs, errors.New("A source_ami must be specified"))
} }
...@@ -70,10 +63,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -70,10 +63,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
} }
} }
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
if c.UserData != "" && c.UserDataFile != "" { if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified.")) errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" { } else if c.UserDataFile != "" {
...@@ -91,15 +80,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -91,15 +80,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
} }
} }
var err error
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs return errs
} }
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func init() { func init() {
...@@ -19,7 +21,10 @@ func testConfig() *RunConfig { ...@@ -19,7 +21,10 @@ func testConfig() *RunConfig {
return &RunConfig{ return &RunConfig{
SourceAmi: "abcd", SourceAmi: "abcd",
InstanceType: "m1.small", InstanceType: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) { ...@@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig() c := testConfig()
c.SSHPort = 0 c.Comm.SSHPort = 0
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
c.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if c.SSHPort != 44 { if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort) t.Fatalf("invalid value: %d", c.Comm.SSHPort)
} }
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) { c.Comm.SSHPort = 44
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
c.RawSSHTimeout = "bad" if c.Comm.SSHPort != 44 {
if err := c.Prepare(nil); len(err) != 1 { t.Fatalf("invalid value: %d", c.Comm.SSHPort)
t.Fatalf("err: %s", err)
} }
} }
func TestRunConfigPrepare_SSHUsername(t *testing.T) { func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig() c := testConfig()
c.SSHUsername = "" c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 1 { if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
......
...@@ -10,9 +10,9 @@ import ( ...@@ -10,9 +10,9 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// SSHAddress returns a function that can be given to the SSH communicator // SSHHost returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name. // for determining the SSH address based on the instance DNS name.
func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (string, error) { func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
var host string var host string
...@@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st ...@@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st
} }
if host != "" { if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil return host, nil
} }
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{ r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{
......
...@@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction { ...@@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
var ec2Tags []*ec2.Tag var ec2Tags []*ec2.Tag
for key, value := range s.Tags { for key, value := range s.Tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value)) ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
ec2Tags = append(ec2Tags, &ec2.Tag{Key: &key, Value: &value}) ec2Tags = append(ec2Tags, &ec2.Tag{
Key: aws.String(key),
Value: aws.String(value),
})
}
// Declare list of resources to tag
resourceIds := []*string{&ami}
// Retrieve image list for given AMI
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIDs: resourceIds,
})
if err != nil {
err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(imageResp.Images) == 0 {
err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
image := imageResp.Images[0]
// Add only those with a Snapshot ID, i.e. not Ephemeral
for _, device := range image.BlockDeviceMappings {
if device.EBS != nil && device.EBS.SnapshotID != nil {
ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.EBS.SnapshotID))
resourceIds = append(resourceIds, device.EBS.SnapshotID)
}
} }
regionconn := ec2.New(&aws.Config{ regionconn := ec2.New(&aws.Config{
Credentials: ec2conn.Config.Credentials, Credentials: ec2conn.Config.Credentials,
Region: region, Region: region,
}) })
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{&ami}, _, err = regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: resourceIds,
Tags: ec2Tags, Tags: ec2Tags,
}) })
if err != nil { if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err) err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
......
package common
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepDeregisterAMI struct {
ForceDeregister bool
AMIName string
}
func (s *StepDeregisterAMI) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
// check for force deregister
if s.ForceDeregister {
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
Filters: []*ec2.Filter{&ec2.Filter{
Name: aws.String("name"),
Values: []*string{aws.String(s.AMIName)},
}}})
if err != nil {
err := fmt.Errorf("Error creating AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// deregister image(s) by that name
for _, i := range resp.Images {
_, err := ec2conn.DeregisterImage(&ec2.DeregisterImageInput{
ImageID: i.ImageID,
})
if err != nil {
err := fmt.Errorf("Error deregistering existing AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Deregistered AMI %s, id: %s", s.AMIName, *i.ImageID))
}
}
return multistep.ActionContinue
}
func (s *StepDeregisterAMI) Cleanup(state multistep.StateBag) {
}
package common
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
// StepGetPassword reads the password from a Windows server and sets it
// on the WinRM config.
type StepGetPassword struct {
Comm *communicator.Config
Timeout time.Duration
}
func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
image := state.Get("source_image").(*ec2.Image)
// Skip if we're not Windows...
if image.Platform == nil || *image.Platform != "windows" {
log.Printf("[INFO] Not Windows, skipping get password...")
return multistep.ActionContinue
}
// If we already have a password, skip it
if s.Comm.WinRMPassword != "" {
ui.Say("Skipping waiting for password since WinRM password set...")
return multistep.ActionContinue
}
// Get the password
var password string
var err error
cancel := make(chan struct{})
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for auto-generated password for instance...")
ui.Message(
"It is normal for this process to take up to 15 minutes,\n" +
"but it usually takes around 5. Please wait.")
password, err = s.waitForPassword(state, cancel)
waitDone <- true
}()
timeout := time.After(s.Timeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for password: %s", err))
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message(fmt.Sprintf(" \nPassword retrieved!"))
s.Comm.WinRMPassword = password
break WaitLoop
case <-timeout:
err := fmt.Errorf("Timeout waiting for password.")
state.Put("error", err)
ui.Error(err.Error())
close(cancel)
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
// The step sequence was cancelled, so cancel waiting for password
// and just start the halting process.
close(cancel)
log.Println("[WARN] Interrupt detected, quitting waiting for password.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepGetPassword) Cleanup(multistep.StateBag) {}
func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-chan struct{}) (string, error) {
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
privateKey := state.Get("privateKey").(string)
for {
select {
case <-cancel:
log.Println("[INFO] Retrieve password wait cancelled. Exiting loop.")
return "", errors.New("Retrieve password wait cancelled")
case <-time.After(5 * time.Second):
}
resp, err := ec2conn.GetPasswordData(&ec2.GetPasswordDataInput{
InstanceID: instance.InstanceID,
})
if err != nil {
err := fmt.Errorf("Error retrieving auto-generated instance password: %s", err)
return "", err
}
if resp.PasswordData != nil && *resp.PasswordData != "" {
decryptedPassword, err := decryptPasswordDataWithPrivateKey(
*resp.PasswordData, []byte(privateKey))
if err != nil {
err := fmt.Errorf("Error decrypting auto-generated instance password: %s", err)
return "", err
}
return decryptedPassword, nil
}
log.Printf("[DEBUG] Password is blank, will retry...")
}
}
func decryptPasswordDataWithPrivateKey(passwordData string, pemBytes []byte) (string, error) {
encryptedPasswd, err := base64.StdEncoding.DecodeString(passwordData)
if err != nil {
return "", err
}
block, _ := pem.Decode(pemBytes)
var asn1Bytes []byte
if _, ok := block.Headers["DEK-Info"]; ok {
return "", errors.New("encrypted private key isn't yet supported")
/*
asn1Bytes, err = x509.DecryptPEMBlock(block, password)
if err != nil {
return "", err
}
*/
} else {
asn1Bytes = block.Bytes
}
key, err := x509.ParsePKCS1PrivateKey(asn1Bytes)
if err != nil {
return "", err
}
out, err := rsa.DecryptPKCS1v15(nil, key, encryptedPasswd)
if err != nil {
return "", err
}
return string(out), nil
}
...@@ -14,11 +14,17 @@ import ( ...@@ -14,11 +14,17 @@ import (
// //
type StepPreValidate struct { type StepPreValidate struct {
DestAmiName string DestAmiName string
ForceDeregister bool
} }
func (s *StepPreValidate) Run(state multistep.StateBag) multistep.StepAction { func (s *StepPreValidate) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
if s.ForceDeregister {
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
return multistep.ActionContinue
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui.Say("Prevalidating AMI Name...") ui.Say("Prevalidating AMI Name...")
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
......
package common package common
import ( import (
"encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt return multistep.ActionHalt
} }
// Test if it is encoded already, and if not, encode it
if _, err := base64.StdEncoding.DecodeString(string(contents)); err != nil {
log.Printf("[DEBUG] base64 encoding user data...")
contents = []byte(base64.StdEncoding.EncodeToString(contents))
}
userData = string(contents) userData = string(contents)
} }
ui.Say("Launching a source AWS instance...") ui.Say("Launching a source AWS instance...")
...@@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
ImageID: &s.SourceAMI, ImageID: &s.SourceAMI,
InstanceType: &s.InstanceType, InstanceType: &s.InstanceType,
UserData: &userData, UserData: &userData,
SecurityGroupIDs: securityGroupIds,
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile}, IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
SubnetID: &s.SubnetId,
NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{ NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
&ec2.InstanceNetworkInterfaceSpecification{AssociatePublicIPAddress: &s.AssociatePublicIpAddress}, &ec2.InstanceNetworkInterfaceSpecification{
DeviceIndex: aws.Long(0),
AssociatePublicIPAddress: &s.AssociatePublicIpAddress,
SubnetID: &s.SubnetId,
Groups: securityGroupIds,
DeleteOnTermination: aws.Boolean(true),
},
}, },
Placement: &ec2.SpotPlacement{ Placement: &ec2.SpotPlacement{
AvailabilityZone: &availabilityZone, AvailabilityZone: &availabilityZone,
......
...@@ -9,12 +9,13 @@ import ( ...@@ -9,12 +9,13 @@ import (
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type StepSecurityGroup struct { type StepSecurityGroup struct {
CommConfig *communicator.Config
SecurityGroupIds []string SecurityGroupIds []string
SSHPort int
VpcId string VpcId string
createdGroupId string createdGroupId string
...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
if s.SSHPort == 0 { port := s.CommConfig.Port()
panic("SSHPort must be set to a non-zero value.") if port == 0 {
panic("port must be set to a non-zero value.")
} }
// Create the group // Create the group
...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
req := &ec2.AuthorizeSecurityGroupIngressInput{ req := &ec2.AuthorizeSecurityGroupIngressInput{
GroupID: groupResp.GroupID, GroupID: groupResp.GroupID,
IPProtocol: aws.String("tcp"), IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(s.SSHPort)), FromPort: aws.Long(int64(port)),
ToPort: aws.Long(int64(s.SSHPort)), ToPort: aws.Long(int64(port)),
CIDRIP: aws.String("0.0.0.0/0"), CIDRIP: aws.String("0.0.0.0/0"),
} }
// We loop and retry this a few times because sometimes the security // We loop and retry this a few times because sometimes the security
// group isn't available immediately because AWS resources are eventaully // group isn't available immediately because AWS resources are eventaully
// consistent. // consistent.
ui.Say("Authorizing SSH access on the temporary security group...") ui.Say(fmt.Sprintf(
"Authorizing access to port %d the temporary security group...",
port))
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
_, err = ec2conn.AuthorizeSecurityGroupIngress(req) _, err = ec2conn.AuthorizeSecurityGroupIngress(req)
if err == nil { if err == nil {
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common" awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -80,6 +81,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -80,6 +81,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{ &awscommon.StepPreValidate{
DestAmiName: b.config.AMIName, DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
}, },
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
...@@ -89,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -89,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName, KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile, PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort, CommConfig: &b.config.RunConfig.Comm,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -112,16 +114,26 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -112,16 +114,26 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &awscommon.StepGetPassword{
SSHAddress: awscommon.SSHAddress( Comm: &b.config.RunConfig.Comm,
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp), Timeout: b.config.WindowsPasswordTimeout,
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), },
SSHWaitTimeout: b.config.SSHTimeout(), &communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&stepStopInstance{SpotPrice: b.config.SpotPrice}, &stepStopInstance{SpotPrice: b.config.SpotPrice},
// TODO(mitchellh): verify works with spots // TODO(mitchellh): verify works with spots
&stepModifyInstance{}, &stepModifyInstance{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&stepCreateAMI{}, &stepCreateAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig, AccessConfig: &b.config.AccessConfig,
......
...@@ -28,6 +28,22 @@ func TestBuilderAcc_regionCopy(t *testing.T) { ...@@ -28,6 +28,22 @@ func TestBuilderAcc_regionCopy(t *testing.T) {
}) })
} }
func TestBuilderAcc_forceDeregister(t *testing.T) {
// Build the same AMI name twice, with force_deregister on the second run
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: buildForceDeregisterConfig("false", "dereg"),
SkipArtifactTeardown: true,
})
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: buildForceDeregisterConfig("true", "dereg"),
})
}
func checkRegionCopy(regions []string) builderT.TestCheckFunc { func checkRegionCopy(regions []string) builderT.TestCheckFunc {
return func(artifacts []packer.Artifact) error { return func(artifacts []packer.Artifact) error {
if len(artifacts) > 1 { if len(artifacts) > 1 {
...@@ -107,3 +123,21 @@ const testBuilderAccRegionCopy = ` ...@@ -107,3 +123,21 @@ const testBuilderAccRegionCopy = `
}] }]
} }
` `
const testBuilderAccForceDeregister = `
{
"builders": [{
"type": "test",
"region": "us-east-1",
"instance_type": "m3.medium",
"source_ami": "ami-76b2a71e",
"ssh_username": "ubuntu",
"force_deregister": "%s",
"ami_name": "packer-test-%s"
}]
}
`
func buildForceDeregisterConfig(name, flag string) string {
return fmt.Sprintf(testBuilderAccForceDeregister, name, flag)
}
package ebs
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/packer/builder/amazon/common"
builderT "github.com/mitchellh/packer/helper/builder/testing"
"github.com/mitchellh/packer/packer"
)
func TestBuilderTagsAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderTagsAccBasic,
Check: checkTags(),
})
}
func checkTags() builderT.TestCheckFunc {
return func(artifacts []packer.Artifact) error {
if len(artifacts) > 1 {
return fmt.Errorf("more than 1 artifact")
}
tags := make(map[string]string)
tags["OS_Version"] = "Ubuntu"
tags["Release"] = "Latest"
// Get the actual *Artifact pointer so we can access the AMIs directly
artifactRaw := artifacts[0]
artifact, ok := artifactRaw.(*common.Artifact)
if !ok {
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
}
// describe the image, get block devices with a snapshot
ec2conn, _ := testEC2Conn()
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIDs: []*string{aws.String(artifact.Amis["us-east-1"])},
})
if err != nil {
return fmt.Errorf("Error retrieving details for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
if len(imageResp.Images) == 0 {
return fmt.Errorf("No images found for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
image := imageResp.Images[0]
// Check only those with a Snapshot ID, i.e. not Ephemeral
var snapshots []*string
for _, device := range image.BlockDeviceMappings {
if device.EBS != nil && device.EBS.SnapshotID != nil {
snapshots = append(snapshots, device.EBS.SnapshotID)
}
}
// grab matching snapshot info
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
SnapshotIDs: snapshots,
})
if err != nil {
return fmt.Errorf("Error retreiving Snapshots for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
if len(resp.Snapshots) == 0 {
return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact)
}
// grab the snapshots, check the tags
for _, s := range resp.Snapshots {
expected := len(tags)
for _, t := range s.Tags {
for key, value := range tags {
if key == *t.Key && value == *t.Value {
expected--
}
}
}
if expected > 0 {
return fmt.Errorf("Not all tags found")
}
}
return nil
}
}
const testBuilderTagsAccBasic = `
{
"builders": [
{
"type": "test",
"region": "us-east-1",
"source_ami": "ami-9eaa1cf6",
"instance_type": "t2.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-tags-testing-{{timestamp}}",
"tags": {
"OS_Version": "Ubuntu",
"Release": "Latest"
}
}
]
}
`
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common" awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -167,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -167,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking, EnhancedNetworking: b.config.AMIEnhancedNetworking,
...@@ -175,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -175,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName, KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile, PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
CommConfig: &b.config.RunConfig.Comm,
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -197,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -197,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &awscommon.StepGetPassword{
SSHAddress: awscommon.SSHAddress( Comm: &b.config.RunConfig.Comm,
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp), Timeout: b.config.WindowsPasswordTimeout,
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), },
SSHWaitTimeout: b.config.SSHTimeout(), &communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&StepUploadX509Cert{}, &StepUploadX509Cert{},
...@@ -211,6 +222,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -211,6 +222,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepUploadBundle{ &StepUploadBundle{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
}, },
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{}, &StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig, AccessConfig: &b.config.AccessConfig,
......
...@@ -6,11 +6,11 @@ package digitalocean ...@@ -6,11 +6,11 @@ package digitalocean
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/digitalocean/godo" "github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
...@@ -53,10 +53,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -53,10 +53,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
new(stepCreateDroplet), new(stepCreateDroplet),
new(stepDropletInfo), new(stepDropletInfo),
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: sshAddress, Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig, SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
}, },
new(common.StepProvision), new(common.StepProvision),
new(stepShutdown), new(stepShutdown),
......
...@@ -3,6 +3,7 @@ package digitalocean ...@@ -3,6 +3,7 @@ package digitalocean
import ( import (
"strconv" "strconv"
"testing" "testing"
"time"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -163,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) { ...@@ -163,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SSHUsername != "root" { if b.config.Comm.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.SSHUsername) t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
} }
// Test set // Test set
...@@ -178,52 +179,11 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) { ...@@ -178,52 +179,11 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SSHUsername != "foo" { if b.config.Comm.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername) t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
} }
} }
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawSSHTimeout != "1m" {
t.Errorf("invalid: %s", b.config.RawSSHTimeout)
}
// Test set
config["ssh_timeout"] = "30s"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["ssh_timeout"] = "tubes"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) { func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()
...@@ -237,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) { ...@@ -237,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.RawStateTimeout != "6m" { if b.config.StateTimeout != 6*time.Minute {
t.Errorf("invalid: %s", b.config.RawStateTimeout) t.Errorf("invalid: %s", b.config.StateTimeout)
} }
// Test set // Test set
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -16,6 +17,7 @@ import ( ...@@ -16,6 +17,7 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
APIToken string `mapstructure:"api_token"` APIToken string `mapstructure:"api_token"`
...@@ -25,18 +27,9 @@ type Config struct { ...@@ -25,18 +27,9 @@ type Config struct {
PrivateNetworking bool `mapstructure:"private_networking"` PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"` SnapshotName string `mapstructure:"snapshot_name"`
StateTimeout time.Duration `mapstructure:"state_timeout"`
DropletName string `mapstructure:"droplet_name"` DropletName string `mapstructure:"droplet_name"`
UserData string `mapstructure:"user_data"` UserData string `mapstructure:"user_data"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context ctx *interpolate.Context
} }
...@@ -79,29 +72,22 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -79,29 +72,22 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
} }
if c.SSHUsername == "" { if c.Comm.SSHUsername == "" {
// Default to "root". You can override this if your // Default to "root". You can override this if your
// SourceImage has a different user account then the DO default // SourceImage has a different user account then the DO default
c.SSHUsername = "root" c.Comm.SSHUsername = "root"
}
if c.SSHPort == 0 {
// Default to port 22 per DO default
c.SSHPort = 22
} }
if c.RawSSHTimeout == "" { if c.StateTimeout == 0 {
// Default to 1 minute timeouts
c.RawSSHTimeout = "1m"
}
if c.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for // Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active // desired state. i.e waiting for droplet to become active
c.RawStateTimeout = "6m" c.StateTimeout = 6 * time.Minute
} }
var errs *packer.MultiError var errs *packer.MultiError
if es := c.Comm.Prepare(c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.APIToken == "" { if c.APIToken == "" {
// Required configurations that will display errors if not set // Required configurations that will display errors if not set
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
...@@ -123,20 +109,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -123,20 +109,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("image is required")) errs, errors.New("image is required"))
} }
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
c.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
c.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 { if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs return nil, nil, errs
} }
......
...@@ -2,14 +2,14 @@ package digitalocean ...@@ -2,14 +2,14 @@ package digitalocean
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
) )
func sshAddress(state multistep.StateBag) (string, error) { func commHost(state multistep.StateBag) (string, error) {
config := state.Get("config").(Config)
ipAddress := state.Get("droplet_ip").(string) ipAddress := state.Get("droplet_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil return ipAddress, nil
} }
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
...@@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { ...@@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: config.SSHUsername, User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer), ssh.PublicKeys(signer),
}, },
......
...@@ -18,7 +18,7 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { ...@@ -18,7 +18,7 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
ui.Say("Waiting for droplet to become active...") ui.Say("Waiting for droplet to become active...")
err := waitForDropletState("active", dropletId, client, c.stateTimeout) err := waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err) err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err) state.Put("error", err)
......
...@@ -42,7 +42,7 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -42,7 +42,7 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Println("Waiting for poweroff event to complete...") log.Println("Waiting for poweroff event to complete...")
err = waitForDropletState("off", dropletId, client, c.stateTimeout) err = waitForDropletState("off", dropletId, client, c.StateTimeout)
if err != nil { if err != nil {
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
......
...@@ -41,7 +41,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -41,7 +41,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
// With the pending state over, verify that we're in the active state // With the pending state over, verify that we're in the active state
ui.Say("Waiting for snapshot to complete...") ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout) err = waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err) err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state.Put("error", err) state.Put("error", err)
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepTempDir{}, &StepTempDir{},
&StepPull{}, &StepPull{},
&StepRun{}, &StepRun{},
&StepProvision{}, &communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig(&b.config.Comm),
CustomConnect: map[string]multistep.Step{
"docker": &StepConnectDocker{},
},
},
&common.StepProvision{},
} }
if b.config.Commit { if b.config.Commit {
......
package docker
import (
"fmt"
"io/ioutil"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/helper/communicator"
gossh "golang.org/x/crypto/ssh"
)
func commHost(state multistep.StateBag) (string, error) {
containerId := state.Get("container_id").(string)
driver := state.Get("driver").(Driver)
return driver.IPAddress(containerId)
}
func sshConfig(comm *communicator.Config) func(state multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
if comm.SSHPrivateKey != "" {
// key based auth
bytes, err := ioutil.ReadFile(comm.SSHPrivateKey)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
privateKey := string(bytes)
signer, err := gossh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: comm.SSHUsername,
Auth: []gossh.AuthMethod{
gossh.PublicKeys(signer),
},
}, nil
} else {
// password based auth
return &gossh.ClientConfig{
User: comm.SSHUsername,
Auth: []gossh.AuthMethod{
gossh.Password(comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(comm.SSHPassword)),
},
}, nil
}
}
}
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -13,6 +14,7 @@ import ( ...@@ -13,6 +14,7 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Commit bool Commit bool
ExportPath string `mapstructure:"export_path"` ExportPath string `mapstructure:"export_path"`
...@@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.Pull = true c.Pull = true
} }
// Default to the normal Docker type
if c.Comm.Type == "" {
c.Comm.Type = "docker"
}
var errs *packer.MultiError var errs *packer.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Image == "" { if c.Image == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("image must be specified")) fmt.Errorf("image must be specified"))
......
...@@ -22,6 +22,10 @@ type Driver interface { ...@@ -22,6 +22,10 @@ type Driver interface {
// Import imports a container from a tar file // Import imports a container from a tar file
Import(path, repo string) (string, error) Import(path, repo string) (string, error)
// IPAddress returns the address of the container that can be used
// for external access.
IPAddress(id string) (string, error)
// Login. This will lock the driver from performing another Login // Login. This will lock the driver from performing another Login
// until Logout is called. Therefore, any users MUST call Logout. // until Logout is called. Therefore, any users MUST call Logout.
Login(repo, email, username, password string) error Login(repo, email, username, password string) error
......
...@@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) { ...@@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
return strings.TrimSpace(stdout.String()), nil return strings.TrimSpace(stdout.String()), nil
} }
func (d *DockerDriver) IPAddress(id string) (string, error) {
var stderr, stdout bytes.Buffer
cmd := exec.Command(
"docker",
"inspect",
"--format",
"{{ .NetworkSettings.IPAddress }}",
id)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("Error: %s\n\nStderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Login(repo, email, user, pass string) error { func (d *DockerDriver) Login(repo, email, user, pass string) error {
d.l.Lock() d.l.Lock()
......
...@@ -23,6 +23,11 @@ type MockDriver struct { ...@@ -23,6 +23,11 @@ type MockDriver struct {
ImportId string ImportId string
ImportErr error ImportErr error
IPAddressCalled bool
IPAddressID string
IPAddressResult string
IPAddressErr error
LoginCalled bool LoginCalled bool
LoginEmail string LoginEmail string
LoginUsername string LoginUsername string
...@@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) { ...@@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
return d.ImportId, d.ImportErr return d.ImportId, d.ImportErr
} }
func (d *MockDriver) IPAddress(id string) (string, error) {
d.IPAddressCalled = true
d.IPAddressID = id
return d.IPAddressResult, d.IPAddressErr
}
func (d *MockDriver) Login(r, e, u, p string) error { func (d *MockDriver) Login(r, e, u, p string) error {
d.LoginCalled = true d.LoginCalled = true
d.LoginRepo = r d.LoginRepo = r
......
...@@ -2,12 +2,11 @@ package docker ...@@ -2,12 +2,11 @@ package docker
import ( import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
) )
type StepProvision struct{} type StepConnectDocker struct{}
func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
containerId := state.Get("container_id").(string) containerId := state.Get("container_id").(string)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
tempDir := state.Get("temp_dir").(string) tempDir := state.Get("temp_dir").(string)
...@@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { ...@@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
Version: version, Version: version,
} }
prov := common.StepProvision{Comm: comm} state.Put("communicator", comm)
return prov.Run(state) return multistep.ActionContinue
} }
func (s *StepProvision) Cleanup(state multistep.StateBag) {} func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {}
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepInstanceInfo{ &StepInstanceInfo{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: sshAddress, Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig, SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshTimeout,
}, },
new(common.StepProvision), new(common.StepProvision),
new(StepTeardownInstance), new(StepTeardownInstance),
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -17,6 +18,7 @@ import ( ...@@ -17,6 +18,7 @@ import (
// state of the config object. // state of the config object.
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"` AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"` ProjectId string `mapstructure:"project_id"`
...@@ -31,16 +33,12 @@ type Config struct { ...@@ -31,16 +33,12 @@ type Config struct {
Network string `mapstructure:"network"` Network string `mapstructure:"network"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"` SourceImageProjectId string `mapstructure:"source_image_project_id"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"` RawStateTimeout string `mapstructure:"state_timeout"`
Tags []string `mapstructure:"tags"` Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
account accountFile account accountFile
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration
stateTimeout time.Duration stateTimeout time.Duration
ctx *interpolate.Context ctx *interpolate.Context
} }
...@@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.MachineType = "n1-standard-1" c.MachineType = "n1-standard-1"
} }
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.RawStateTimeout == "" { if c.RawStateTimeout == "" {
c.RawStateTimeout = "5m" c.RawStateTimeout = "5m"
} }
if c.SSHUsername == "" { if c.Comm.SSHUsername == "" {
c.SSHUsername = "root" c.Comm.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
} }
var errs *packer.MultiError var errs *packer.MultiError
...@@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a zone must be specified")) errs, errors.New("a zone must be specified"))
} }
// Process timeout settings.
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
c.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(c.RawStateTimeout) stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil { if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
......
...@@ -2,15 +2,14 @@ package googlecompute ...@@ -2,15 +2,14 @@ package googlecompute
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// sshAddress returns the ssh address. func commHost(state multistep.StateBag) (string, error) {
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config)
ipAddress := state.Get("instance_ip").(string) ipAddress := state.Get("instance_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil return ipAddress, nil
} }
// sshConfig returns the ssh configuration. // sshConfig returns the ssh configuration.
...@@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { ...@@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: config.SSHUsername, User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer), ssh.PublicKeys(signer),
}, },
......
...@@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string ...@@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
// Merge any existing ssh keys with our public key // Merge any existing ssh keys with our public key
sshMetaKey := "sshKeys" sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey) sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists { if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys) sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
} }
......
package null package null
import ( import (
"log"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"time"
) )
const BuilderId = "fnoeding.null" const BuilderId = "fnoeding.null"
...@@ -27,10 +28,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -27,10 +28,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
steps := []multistep.Step{ steps := []multistep.Step{
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: SSHAddress(b.config.Host, b.config.Port), Config: &b.config.CommConfig,
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile), Host: CommHost(b.config.CommConfig.SSHHost),
SSHWaitTimeout: 1 * time.Minute, SSHConfig: SSHConfig(
b.config.CommConfig.SSHUsername,
b.config.CommConfig.SSHPassword,
b.config.CommConfig.SSHPrivateKey),
}, },
&common.StepProvision{}, &common.StepProvision{},
} }
......
...@@ -2,7 +2,9 @@ package null ...@@ -2,7 +2,9 @@ package null
import ( import (
"fmt" "fmt"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -11,49 +13,40 @@ import ( ...@@ -11,49 +13,40 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Host string `mapstructure:"host"` CommConfig communicator.Config `mapstructure:",squash"`
Port int `mapstructure:"port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) var c Config
err := config.Decode(c, &config.DecodeOpts{ err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{},
Exclude: []string{
"run_command",
},
},
}, raws...) }, raws...)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if c.Port == 0 {
c.Port = 22
}
var errs *packer.MultiError var errs *packer.MultiError
if c.Host == "" { if es := c.CommConfig.Prepare(nil); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.CommConfig.SSHHost == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("host must be specified")) fmt.Errorf("host must be specified"))
} }
if c.SSHUsername == "" { if c.CommConfig.SSHUsername == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("ssh_username must be specified")) fmt.Errorf("ssh_username must be specified"))
} }
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" { if c.CommConfig.SSHPassword == "" && c.CommConfig.SSHPrivateKey == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified")) fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified"))
} }
if c.SSHPassword != "" && c.SSHPrivateKeyFile != "" { if c.CommConfig.SSHPassword != "" && c.CommConfig.SSHPrivateKey != "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified")) fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified"))
} }
...@@ -62,5 +55,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -62,5 +55,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs return nil, nil, errs
} }
return c, nil, nil return &c, nil, nil
} }
package null package null
import ( import (
"os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func testConfig() map[string]interface{} { func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"host": "foo", "ssh_host": "foo",
"ssh_username": "bar", "ssh_username": "bar",
"ssh_password": "baz", "ssh_password": "baz",
} }
...@@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) { ...@@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) {
// default port should be 22 // default port should be 22
delete(raw, "port") delete(raw, "port")
c, warns, errs := NewConfig(raw) c, warns, errs := NewConfig(raw)
if c.Port != 22 { if c.CommConfig.SSHPort != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.Port) t.Fatalf("bad: port should default to 22, not %d", c.CommConfig.SSHPort)
} }
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
} }
...@@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) { ...@@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) {
raw := testConfig() raw := testConfig()
// No host // No host
delete(raw, "host") delete(raw, "ssh_host")
_, warns, errs := NewConfig(raw) _, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs) testConfigErr(t, warns, errs)
// Good host // Good host
raw["host"] = "good" raw["ssh_host"] = "good"
_, warns, errs = NewConfig(raw) _, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
} }
...@@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) { ...@@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) {
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
// only ssh_private_key_file // only ssh_private_key_file
raw["ssh_private_key_file"] = "good" testFile := communicator.TestPEM(t)
defer os.Remove(testFile)
raw["ssh_private_key_file"] = testFile
delete(raw, "ssh_password") delete(raw, "ssh_password")
_, warns, errs = NewConfig(raw) _, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
......
...@@ -8,11 +8,9 @@ import ( ...@@ -8,11 +8,9 @@ import (
"io/ioutil" "io/ioutil"
) )
// SSHAddress returns a function that can be given to the SSH communicator func CommHost(host string) func(multistep.StateBag) (string, error) {
// for determining the SSH address
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
return fmt.Sprintf("%s:%d", host, port), nil return host, nil
} }
} }
......
...@@ -5,10 +5,11 @@ package openstack ...@@ -5,10 +5,11 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"log" "log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -19,6 +20,7 @@ const BuilderId = "mitchellh.openstack" ...@@ -19,6 +20,7 @@ const BuilderId = "mitchellh.openstack"
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"` AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"` ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"` RunConfig `mapstructure:",squash"`
...@@ -67,6 +69,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -67,6 +69,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&StepLoadExtensions{},
&StepLoadFlavor{ &StepLoadFlavor{
Flavor: b.config.Flavor, Flavor: b.config.Flavor,
}, },
...@@ -80,6 +83,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -80,6 +83,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SecurityGroups: b.config.SecurityGroups, SecurityGroups: b.config.SecurityGroups,
Networks: b.config.Networks, Networks: b.config.Networks,
AvailabilityZone: b.config.AvailabilityZone, AvailabilityZone: b.config.AvailabilityZone,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
}, },
&StepWaitForRackConnect{ &StepWaitForRackConnect{
Wait: b.config.RackconnectWait, Wait: b.config.RackconnectWait,
...@@ -88,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -88,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
FloatingIpPool: b.config.FloatingIpPool, FloatingIpPool: b.config.FloatingIpPool,
FloatingIp: b.config.FloatingIp, FloatingIp: b.config.FloatingIp,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, b.config.SSHPort), Config: &b.config.RunConfig.Comm,
SSHConfig: SSHConfig(b.config.SSHUsername), Host: CommHost(
SSHWaitTimeout: b.config.SSHTimeout(), computeClient,
b.config.SSHInterface),
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&StepStopServer{},
&stepCreateImage{}, &stepCreateImage{},
} }
......
...@@ -2,48 +2,37 @@ package openstack ...@@ -2,48 +2,37 @@ package openstack
import ( import (
"errors" "errors"
"fmt"
"time"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
// RunConfig contains configuration for running an instance from a source // RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image. // image and details on how to access that launched image.
type RunConfig struct { type RunConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHInterface string `mapstructure:"ssh_interface"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
Flavor string `mapstructure:"flavor"` Flavor string `mapstructure:"flavor"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SSHInterface string `mapstructure:"ssh_interface"`
AvailabilityZone string `mapstructure:"availability_zone"` AvailabilityZone string `mapstructure:"availability_zone"`
RackconnectWait bool `mapstructure:"rackconnect_wait"` RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIpPool string `mapstructure:"floating_ip_pool"` FloatingIpPool string `mapstructure:"floating_ip_pool"`
FloatingIp string `mapstructure:"floating_ip"` FloatingIp string `mapstructure:"floating_ip"`
SecurityGroups []string `mapstructure:"security_groups"` SecurityGroups []string `mapstructure:"security_groups"`
Networks []string `mapstructure:"networks"` Networks []string `mapstructure:"networks"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
// Not really used, but here for BC // Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"` OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"` UseFloatingIp bool `mapstructure:"use_floating_ip"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
} }
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults // Defaults
if c.SSHUsername == "" { if c.Comm.SSHUsername == "" {
c.SSHUsername = "root" c.Comm.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
} }
if c.UseFloatingIp && c.FloatingIpPool == "" { if c.UseFloatingIp && c.FloatingIpPool == "" {
...@@ -51,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -51,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
} }
// Validation // Validation
var err error errs := c.Comm.Prepare(ctx)
errs := make([]error, 0)
if c.SourceImage == "" { if c.SourceImage == "" {
errs = append(errs, errors.New("A source_image must be specified")) errs = append(errs, errors.New("A source_image must be specified"))
} }
...@@ -61,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -61,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, errors.New("A flavor must be specified")) errs = append(errs, errors.New("A flavor must be specified"))
} }
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs return errs
} }
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
...@@ -3,6 +3,8 @@ package openstack ...@@ -3,6 +3,8 @@ package openstack
import ( import (
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func init() { func init() {
...@@ -17,7 +19,10 @@ func testRunConfig() *RunConfig { ...@@ -17,7 +19,10 @@ func testRunConfig() *RunConfig {
return &RunConfig{ return &RunConfig{
SourceImage: "abcd", SourceImage: "abcd",
Flavor: "m1.small", Flavor: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) { ...@@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testRunConfig() c := testRunConfig()
c.SSHPort = 0 c.Comm.SSHPort = 0
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
c.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if c.SSHPort != 44 { if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort) t.Fatalf("invalid value: %d", c.Comm.SSHPort)
} }
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) { c.Comm.SSHPort = 44
c := testRunConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
c.RawSSHTimeout = "bad" if c.Comm.SSHPort != 44 {
if err := c.Prepare(nil); len(err) != 1 { t.Fatalf("invalid value: %d", c.Comm.SSHPort)
t.Fatalf("err: %s", err)
} }
} }
func TestRunConfigPrepare_SSHUsername(t *testing.T) { func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testRunConfig() c := testRunConfig()
c.SSHUsername = "" c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
......
...@@ -28,7 +28,7 @@ type StateChangeConf struct { ...@@ -28,7 +28,7 @@ type StateChangeConf struct {
Pending []string Pending []string
Refresh StateRefreshFunc Refresh StateRefreshFunc
StepState multistep.StateBag StepState multistep.StateBag
Target string Target []string
} }
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch // ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
...@@ -65,9 +65,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) { ...@@ -65,9 +65,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
return return
} }
if currentState == conf.Target { for _, t := range conf.Target {
if currentState == t {
return return
} }
}
if conf.StepState != nil { if conf.StepState != nil {
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok { if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
......
...@@ -13,49 +13,33 @@ import ( ...@@ -13,49 +13,33 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// SSHAddress returns a function that can be given to the SSH communicator // CommHost looks up the host for the communicator.
// for determining the SSH address based on the server AccessIPv4 setting.. func CommHost(
func SSHAddress(
client *gophercloud.ServiceClient, client *gophercloud.ServiceClient,
sshinterface string, port int) func(multistep.StateBag) (string, error) { sshinterface string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
s := state.Get("server").(*servers.Server) s := state.Get("server").(*servers.Server)
// If we have a specific interface, try that
if sshinterface != "" {
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
return addr, nil
}
}
// If we have a floating IP, use that // If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingip.FloatingIP) ip := state.Get("access_ip").(*floatingip.FloatingIP)
if ip != nil && ip.IP != "" { if ip != nil && ip.IP != "" {
return fmt.Sprintf("%s:%d", ip.IP, port), nil return ip.IP, nil
} }
if s.AccessIPv4 != "" { if s.AccessIPv4 != "" {
return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil return s.AccessIPv4, nil
} }
// Get all the addresses associated with this server. This // Try to get it from the requested interface
// was taken directly from Terraform. if addr := sshAddrFromPool(s, sshinterface); addr != "" {
for _, networkAddresses := range s.Addresses { return addr, nil
elements, ok := networkAddresses.([]interface{})
if !ok {
log.Printf(
"[ERROR] Unknown return type for address field: %#v",
networkAddresses)
continue
}
for _, element := range elements {
var addr string
address := element.(map[string]interface{})
if address["OS-EXT-IPS:type"] == "floating" {
addr = address["addr"].(string)
} else {
if address["version"].(float64) == 4 {
addr = address["addr"].(string)
}
}
if addr != "" {
return fmt.Sprintf("%s:%d", addr, port), nil
}
}
} }
s, err := servers.Get(client, s.ID).Extract() s, err := servers.Get(client, s.ID).Extract()
...@@ -90,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err ...@@ -90,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err
}, nil }, nil
} }
} }
func sshAddrFromPool(s *servers.Server, desired string) string {
// Get all the addresses associated with this server. This
// was taken directly from Terraform.
for pool, networkAddresses := range s.Addresses {
// If we have an SSH interface specified, skip it if no match
if desired != "" && pool != desired {
log.Printf(
"[INFO] Skipping pool %s, doesn't match requested %s",
pool, desired)
continue
}
elements, ok := networkAddresses.([]interface{})
if !ok {
log.Printf(
"[ERROR] Unknown return type for address field: %#v",
networkAddresses)
continue
}
for _, element := range elements {
var addr string
address := element.(map[string]interface{})
if address["OS-EXT-IPS:type"] == "floating" {
addr = address["addr"].(string)
} else {
if address["version"].(float64) == 4 {
addr = address["addr"].(string)
}
}
if addr != "" {
return addr
}
}
}
return ""
}
package openstack
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions"
"github.com/rackspace/gophercloud/pagination"
)
// StepLoadExtensions gets the FlavorRef from a Flavor. It first assumes
// that the Flavor is a ref and verifies it. Otherwise, it tries to find
// the flavor by name.
type StepLoadExtensions struct{}
func (s *StepLoadExtensions) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Discovering enabled extensions...")
result := make(map[string]struct{}, 15)
pager := extensions.List(client)
err = pager.EachPage(func(p pagination.Page) (bool, error) {
// Extract the extensions from this page
exts, err := extensions.ExtractExtensions(p)
if err != nil {
return false, err
}
for _, ext := range exts {
log.Printf("[DEBUG] Discovered extension: %s", ext.Alias)
result[ext.Alias] = struct{}{}
}
return true, nil
})
if err != nil {
err = fmt.Errorf("Error loading extensions: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("extensions", result)
return multistep.ActionContinue
}
func (s *StepLoadExtensions) Cleanup(state multistep.StateBag) {
}
...@@ -2,6 +2,7 @@ package openstack ...@@ -2,6 +2,7 @@ package openstack
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
...@@ -16,6 +17,8 @@ type StepRunSourceServer struct { ...@@ -16,6 +17,8 @@ type StepRunSourceServer struct {
SecurityGroups []string SecurityGroups []string
Networks []string Networks []string
AvailabilityZone string AvailabilityZone string
UserData string
UserDataFile string
server *servers.Server server *servers.Server
} }
...@@ -39,6 +42,16 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction ...@@ -39,6 +42,16 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
networks[i].UUID = networkUuid networks[i].UUID = networkUuid
} }
userData := []byte(s.UserData)
if s.UserDataFile != "" {
userData, err = ioutil.ReadFile(s.UserDataFile)
if err != nil {
err = fmt.Errorf("Error reading user data file: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
}
ui.Say("Launching server...") ui.Say("Launching server...")
s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{ s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{
CreateOptsBuilder: servers.CreateOpts{ CreateOptsBuilder: servers.CreateOpts{
...@@ -48,6 +61,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction ...@@ -48,6 +61,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
SecurityGroups: s.SecurityGroups, SecurityGroups: s.SecurityGroups,
Networks: networks, Networks: networks,
AvailabilityZone: s.AvailabilityZone, AvailabilityZone: s.AvailabilityZone,
UserData: userData,
}, },
KeyName: keyName, KeyName: keyName,
...@@ -65,7 +79,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction ...@@ -65,7 +79,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
ui.Say("Waiting for server to become ready...") ui.Say("Waiting for server to become ready...")
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"BUILD"}, Pending: []string{"BUILD"},
Target: "ACTIVE", Target: []string{"ACTIVE"},
Refresh: ServerStateRefreshFunc(computeClient, s.server), Refresh: ServerStateRefreshFunc(computeClient, s.server),
StepState: state, StepState: state,
} }
...@@ -105,9 +119,9 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) { ...@@ -105,9 +119,9 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) {
} }
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED", "SHUTOFF", "STOPPED"},
Refresh: ServerStateRefreshFunc(computeClient, s.server), Refresh: ServerStateRefreshFunc(computeClient, s.server),
Target: "DELETED", Target: []string{"DELETED"},
} }
WaitForState(&stateChange) WaitForState(&stateChange)
......
package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepStopServer struct{}
func (s *StepStopServer) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
extensions := state.Get("extensions").(map[string]struct{})
server := state.Get("server").(*servers.Server)
// Verify we have the extension
if _, ok := extensions["os-server-start-stop"]; !ok {
ui.Say("OpenStack cluster doesn't support stop, skipping...")
return multistep.ActionContinue
}
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Stopping server...")
if err := startstop.Stop(client, server.ID).ExtractErr(); err != nil {
err = fmt.Errorf("Error stopping server: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message("Waiting for server to stop...")
stateChange := StateChangeConf{
Pending: []string{"ACTIVE"},
Target: []string{"SHUTOFF", "STOPPED"},
Refresh: ServerStateRefreshFunc(client, server),
StepState: state,
}
if _, err := WaitForState(&stateChange); err != nil {
err := fmt.Errorf("Error waiting for server (%s) to stop: %s", server.ID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepStopServer) Cleanup(state multistep.StateBag) {}
package common package common
import ( import (
"fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh" commonssh "github.com/mitchellh/packer/common/ssh"
packerssh "github.com/mitchellh/packer/communicator/ssh" packerssh "github.com/mitchellh/packer/communicator/ssh"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func SSHAddress(state multistep.StateBag) (string, error) { func CommHost(state multistep.StateBag) (string, error) {
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
...@@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) { ...@@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) {
return "", err return "", err
} }
return fmt.Sprintf("%s:22", ip), nil return ip, nil
} }
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) { func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
return func(state multistep.StateBag) (*ssh.ClientConfig, error) { return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
auth := []ssh.AuthMethod{ auth := []ssh.AuthMethod{
ssh.Password(config.SSHPassword), ssh.Password(config.Comm.SSHPassword),
ssh.KeyboardInteractive( ssh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.SSHPassword)), packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
} }
if config.SSHKeyPath != "" { if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig ...@@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: config.SSHUser, User: config.Comm.SSHUsername,
Auth: auth, Auth: auth,
}, nil }, nil
} }
......
package common package common
import ( import (
"errors"
"fmt"
"os"
"time" "time"
commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
type SSHConfig struct { type SSHConfig struct {
SSHKeyPath string `mapstructure:"ssh_key_path"` Comm communicator.Config `mapstructure:",squash"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHWaitTimeout time.Duration // These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
} }
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error { func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
if c.SSHPort == 0 { // TODO: backwards compatibility, write fixer instead
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
if c.SSHKeyPath != "" { if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil { c.Comm.SSHPrivateKey = c.SSHKeyPath
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
} }
if c.SSHWaitTimeout != 0 {
if c.SSHUser == "" { c.Comm.SSHTimeout = c.SSHWaitTimeout
errs = append(errs, errors.New("An ssh_username must be specified."))
}
var err error
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
} }
return errs return c.Comm.Prepare(ctx)
} }
...@@ -4,11 +4,15 @@ import ( ...@@ -4,11 +4,15 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func testSSHConfig() *SSHConfig { func testSSHConfig() *SSHConfig {
return &SSHConfig{ return &SSHConfig{
SSHUser: "foo", Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) { ...@@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Fatalf("err: %#v", errs) t.Fatalf("err: %#v", errs)
} }
if c.SSHPort != 22 { if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort) t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
} }
} }
...@@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) { ...@@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error var errs []error
c = testSSHConfig() c = testSSHConfig()
c.SSHUser = "" c.Comm.SSHUsername = ""
errs = c.Prepare(testConfigTemplate(t)) errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 { if len(errs) == 0 {
t.Fatalf("should have error") t.Fatalf("should have error")
} }
c = testSSHConfig() c = testSSHConfig()
c.SSHUser = "exists" c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
var c *SSHConfig
var errs []error
// Defaults
c = testSSHConfig()
c.RawSSHWaitTimeout = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
if c.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
}
// Test with a bad value
c = testSSHConfig()
c.RawSSHWaitTimeout = "this is not good"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatal("should have error")
}
// Test with a good one
c = testSSHConfig()
c.RawSSHWaitTimeout = "5s"
errs = c.Prepare(testConfigTemplate(t)) errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 { if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs) t.Fatalf("should not have error: %#v", errs)
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common" parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: parallelscommon.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
}, },
&parallelscommon.StepUploadVersion{ &parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile, Path: b.config.PrlctlVersionFile,
......
...@@ -3,11 +3,13 @@ package pvm ...@@ -3,11 +3,13 @@ package pvm
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common" parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
) )
// Builder implements packer.Builder and builds the actual Parallels // Builder implements packer.Builder and builds the actual Parallels
...@@ -80,10 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -80,10 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: parallelscommon.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
}, },
&parallelscommon.StepUploadVersion{ &parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile, Path: b.config.PrlctlVersionFile,
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -78,6 +78,7 @@ type Builder struct { ...@@ -78,6 +78,7 @@ type Builder struct {
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Accelerator string `mapstructure:"accelerator"` Accelerator string `mapstructure:"accelerator"`
BootCommand []string `mapstructure:"boot_command"` BootCommand []string `mapstructure:"boot_command"`
...@@ -103,25 +104,24 @@ type Config struct { ...@@ -103,25 +104,24 @@ type Config struct {
ShutdownCommand string `mapstructure:"shutdown_command"` ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
VNCPortMin uint `mapstructure:"vnc_port_min"` VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"` VNCPortMax uint `mapstructure:"vnc_port_max"`
VMName string `mapstructure:"vm_name"` VMName string `mapstructure:"vm_name"`
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
// TODO(mitchellh): deprecate // TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"` RunOnce bool `mapstructure:"run_once"`
RawBootWait string `mapstructure:"boot_wait"` RawBootWait string `mapstructure:"boot_wait"`
RawSingleISOUrl string `mapstructure:"iso_url"` RawSingleISOUrl string `mapstructure:"iso_url"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"` RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration `` bootWait time.Duration ``
shutdownTimeout time.Duration `` shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
ctx interpolate.Context ctx interpolate.Context
} }
...@@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
return nil, err return nil, err
} }
var errs *packer.MultiError
warnings := make([]string, 0)
if b.config.DiskSize == 0 { if b.config.DiskSize == 0 {
b.config.DiskSize = 40000 b.config.DiskSize = 40000
} }
...@@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.SSHHostPortMax = 4444 b.config.SSHHostPortMax = 4444
} }
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.VNCPortMin == 0 { if b.config.VNCPortMin == 0 {
b.config.VNCPortMin = 5900 b.config.VNCPortMin = 5900
} }
...@@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskInterface = "virtio" b.config.DiskInterface = "virtio"
} }
// TODO: backwards compatibility, write fixer instead
if b.config.SSHKeyPath != "" {
b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath
}
if b.config.SSHWaitTimeout != 0 {
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
}
var errs *packer.MultiError
warnings := make([]string, 0)
if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if !(b.config.Format == "qcow2" || b.config.Format == "raw") { if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
...@@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.RawShutdownTimeout = "5m" b.config.RawShutdownTimeout = "5m"
} }
if b.config.RawSSHWaitTimeout == "" {
b.config.RawSSHWaitTimeout = "20m"
}
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil { if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
} }
if b.config.SSHKeyPath != "" {
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax { if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
} }
if b.config.SSHUser == "" {
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 = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
if b.config.VNCPortMin > b.config.VNCPortMax { if b.config.VNCPortMin > b.config.VNCPortMax {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
...@@ -409,10 +392,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -409,10 +392,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
steprun, steprun,
&stepBootWait{}, &stepBootWait{},
&stepTypeBootCommand{}, &stepTypeBootCommand{},
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: sshAddress, Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig, SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout, SSHPort: commPort,
}, },
new(common.StepProvision), new(common.StepProvision),
new(stepShutdown), new(stepShutdown),
......
...@@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) { ...@@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax) t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
} }
if b.config.SSHPort != 22 { if b.config.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.SSHPort) t.Errorf("bad ssh port: %d", b.config.Comm.SSHPort)
} }
if b.config.VMName != "packer-foo" { if b.config.VMName != "packer-foo" {
...@@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) { ...@@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if b.config.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
}
// Test with a bad value // Test with a bad value
config["ssh_wait_timeout"] = "this is not good" config["ssh_wait_timeout"] = "this is not good"
b = Builder{} b = Builder{}
......
package qemu package qemu
import ( import (
"fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh" commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
func sshAddress(state multistep.StateBag) (string, error) { func commHost(state multistep.StateBag) (string, error) {
return "127.0.0.1", nil
}
func commPort(state multistep.StateBag) (int, error) {
sshHostPort := state.Get("sshHostPort").(uint) sshHostPort := state.Get("sshHostPort").(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil return int(sshHostPort), nil
} }
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
auth := []gossh.AuthMethod{ auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword), gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive( gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)), ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
} }
if config.SSHKeyPath != "" { if config.Comm.SSHPrivateKey != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { ...@@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
} }
return &gossh.ClientConfig{ return &gossh.ClientConfig{
User: config.SSHUser, User: config.Comm.SSHUsername,
Auth: auth, Auth: auth,
}, nil }, nil
} }
...@@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error ...@@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
defaultArgs["-name"] = vmName defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort) defaultArgs["-netdev"] = fmt.Sprintf(
"user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice) defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard) defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard)
if !config.DiskImage { if !config.DiskImage {
......
...@@ -2,7 +2,6 @@ package common ...@@ -2,7 +2,6 @@ package common
import ( import (
"fmt" "fmt"
"os"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig ...@@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName) c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
} }
var errs []error return nil
if !pc.PackerForce {
if _, err := os.Stat(c.OutputDir); err == nil {
errs = append(errs, fmt.Errorf(
"Output directory '%s' already exists. It must not exist.", c.OutputDir))
}
}
return errs
} }
...@@ -39,27 +39,7 @@ func TestOutputConfigPrepare_exists(t *testing.T) { ...@@ -39,27 +39,7 @@ func TestOutputConfigPrepare_exists(t *testing.T) {
PackerForce: false, PackerForce: false,
} }
errs := c.Prepare(testConfigTemplate(t), pc) errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) == 0 { if len(errs) != 0 {
t.Fatal("should have errors")
}
}
func TestOutputConfigPrepare_forceExists(t *testing.T) {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
c := new(OutputConfig)
c.OutputDir = td
pc := &common.PackerConfig{
PackerBuildName: "foo",
PackerForce: true,
}
errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) > 0 {
t.Fatal("should not have errors") t.Fatal("should not have errors")
} }
} }
package common package common
import ( import (
"fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh" commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
func SSHAddress(state multistep.StateBag) (string, error) { func CommHost(state multistep.StateBag) (string, error) {
return "127.0.0.1", nil
}
func SSHPort(state multistep.StateBag) (int, error) {
sshHostPort := state.Get("sshHostPort").(uint) sshHostPort := state.Get("sshHostPort").(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil return int(sshHostPort), nil
} }
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) { func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) { return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
auth := []gossh.AuthMethod{ auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword), gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive( gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)), ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
} }
if config.SSHKeyPath != "" { if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf ...@@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
} }
return &gossh.ClientConfig{ return &gossh.ClientConfig{
User: config.SSHUser, User: config.Comm.SSHUsername,
Auth: auth, Auth: auth,
}, nil }, nil
} }
......
...@@ -2,25 +2,23 @@ package common ...@@ -2,25 +2,23 @@ package common
import ( import (
"errors" "errors"
"fmt"
"os"
"time" "time"
commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
type SSHConfig struct { type SSHConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"` SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
SSHWaitTimeout time.Duration // These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
} }
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error { func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
...@@ -32,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -32,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
c.SSHHostPortMax = 4444 c.SSHHostPortMax = 4444
} }
if c.SSHPort == 0 { // TODO: backwards compatibility, write fixer instead
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
if c.SSHKeyPath != "" { if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil { c.Comm.SSHPrivateKey = c.SSHKeyPath
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} }
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
} }
errs := c.Comm.Prepare(ctx)
if c.SSHHostPortMin > c.SSHHostPortMax { if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs, errs = append(errs,
errors.New("ssh_host_port_min must be less than ssh_host_port_max")) errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
} }
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
var err error
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
return errs return errs
} }
...@@ -4,11 +4,15 @@ import ( ...@@ -4,11 +4,15 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func testSSHConfig() *SSHConfig { func testSSHConfig() *SSHConfig {
return &SSHConfig{ return &SSHConfig{
SSHUser: "foo", Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) { ...@@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax) t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
} }
if c.SSHPort != 22 { if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort) t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
} }
} }
...@@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) { ...@@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error var errs []error
c = testSSHConfig() c = testSSHConfig()
c.SSHUser = "" c.Comm.SSHUsername = ""
errs = c.Prepare(testConfigTemplate(t)) errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 { if len(errs) == 0 {
t.Fatalf("should have error") t.Fatalf("should have error")
} }
c = testSSHConfig() c = testSSHConfig()
c.SSHUser = "exists" c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
var c *SSHConfig
var errs []error
// Defaults
c = testSSHConfig()
c.RawSSHWaitTimeout = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
if c.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
}
// Test with a bad value
c = testSSHConfig()
c.RawSSHWaitTimeout = "this is not good"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatal("should have error")
}
// Test with a good one
c = testSSHConfig()
c.RawSSHWaitTimeout = "5s"
errs = c.Prepare(testConfigTemplate(t)) errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 { if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs) t.Fatalf("should not have error: %#v", errs)
......
...@@ -2,11 +2,13 @@ package common ...@@ -2,11 +2,13 @@ package common
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"math/rand" "math/rand"
"net" "net"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
) )
// This step adds a NAT port forwarding definition so that SSH is available // This step adds a NAT port forwarding definition so that SSH is available
...@@ -19,7 +21,7 @@ import ( ...@@ -19,7 +21,7 @@ import (
// //
// Produces: // Produces:
type StepForwardSSH struct { type StepForwardSSH struct {
GuestPort uint CommConfig *communicator.Config
HostPortMin uint HostPortMin uint
HostPortMax uint HostPortMax uint
SkipNatMapping bool SkipNatMapping bool
...@@ -30,20 +32,21 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,20 +32,21 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
sshHostPort := s.GuestPort guestPort := s.CommConfig.Port()
sshHostPort := guestPort
if !s.SkipNatMapping { if !s.SkipNatMapping {
log.Printf("Looking for available SSH port between %d and %d", log.Printf("Looking for available SSH port between %d and %d",
s.HostPortMin, s.HostPortMax) s.HostPortMin, s.HostPortMax)
var offset uint = 0 offset := 0
portRange := int(s.HostPortMax - s.HostPortMin) portRange := int(s.HostPortMax - s.HostPortMin)
if portRange > 0 { if portRange > 0 {
// Have to check if > 0 to avoid a panic // Have to check if > 0 to avoid a panic
offset = uint(rand.Intn(portRange)) offset = rand.Intn(portRange)
} }
for { for {
sshHostPort = offset + s.HostPortMin sshHostPort = offset + int(s.HostPortMin)
log.Printf("Trying port: %d", sshHostPort) log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
if err == nil { if err == nil {
...@@ -57,7 +60,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { ...@@ -57,7 +60,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
command := []string{ command := []string{
"modifyvm", vmName, "modifyvm", vmName,
"--natpf1", "--natpf1",
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, s.GuestPort), fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, guestPort),
} }
if err := driver.VBoxManage(command...); err != nil { if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error creating port forwarding rule: %s", err) err := fmt.Errorf("Error creating port forwarding rule: %s", err)
......
...@@ -22,7 +22,16 @@ type StepOutputDir struct { ...@@ -22,7 +22,16 @@ type StepOutputDir struct {
func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction { func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
if _, err := os.Stat(s.Path); err == nil && s.Force { if _, err := os.Stat(s.Path); err == nil {
if !s.Force {
err := fmt.Errorf(
"Output directory exists: %s\n\n"+
"Use the force flag to delete it prior to building.",
s.Path)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Deleting previous output directory...") ui.Say("Deleting previous output directory...")
os.RemoveAll(s.Path) os.RemoveAll(s.Path)
} }
......
...@@ -45,6 +45,30 @@ func TestStepOutputDir(t *testing.T) { ...@@ -45,6 +45,30 @@ func TestStepOutputDir(t *testing.T) {
} }
} }
func TestStepOutputDir_exists(t *testing.T) {
state := testState(t)
step := testStepOutputDir(t)
// Make the dir
if err := os.MkdirAll(step.Path, 0755); err != nil {
t.Fatalf("bad: %s", err)
}
// Test the run
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
// Test the cleanup
step.Cleanup(state)
if _, err := os.Stat(step.Path); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestStepOutputDir_cancelled(t *testing.T) { func TestStepOutputDir_cancelled(t *testing.T) {
state := testState(t) state := testState(t)
step := testStepOutputDir(t) step := testStepOutputDir(t)
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common" vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -253,7 +254,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -253,7 +254,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
new(vboxcommon.StepAttachFloppy), new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{ &vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort, CommConfig: &b.config.SSHConfig.Comm,
HostPortMin: b.config.SSHHostPortMin, HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax, HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping, SkipNatMapping: b.config.SSHSkipNatMapping,
...@@ -271,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -271,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: vboxcommon.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout, SSHPort: vboxcommon.SSHPort,
}, },
&vboxcommon.StepUploadVersion{ &vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile, Path: b.config.VBoxVersionFile,
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common" vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -82,7 +83,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -82,7 +83,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
new(vboxcommon.StepAttachFloppy), new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{ &vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort, CommConfig: &b.config.SSHConfig.Comm,
HostPortMin: b.config.SSHHostPortMin, HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax, HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping, SkipNatMapping: b.config.SSHSkipNatMapping,
...@@ -100,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -100,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: vboxcommon.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout, SSHPort: vboxcommon.SSHPort,
}, },
&vboxcommon.StepUploadVersion{ &vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile, Path: b.config.VBoxVersionFile,
......
...@@ -29,9 +29,9 @@ type Driver interface { ...@@ -29,9 +29,9 @@ type Driver interface {
// Checks if the VMX file at the given path is running. // Checks if the VMX file at the given path is running.
IsRunning(string) (bool, error) IsRunning(string) (bool, error)
// SSHAddress returns the SSH address for the VM that is being // CommHost returns the host address for the VM that is being
// managed by this driver. // managed by this driver.
SSHAddress(multistep.StateBag) (string, error) CommHost(multistep.StateBag) (string, error)
// Start starts a VM specified by the path to the VMX given. // Start starts a VM specified by the path to the VMX given.
Start(string, bool) error Start(string, bool) error
......
...@@ -69,8 +69,8 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) { ...@@ -69,8 +69,8 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Fusion5Driver) SSHAddress(state multistep.StateBag) (string, error) { func (d *Fusion5Driver) CommHost(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state) return CommHost(d.SSHConfig)(state)
} }
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error { func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
......
...@@ -29,10 +29,10 @@ type DriverMock struct { ...@@ -29,10 +29,10 @@ type DriverMock struct {
IsRunningResult bool IsRunningResult bool
IsRunningErr error IsRunningErr error
SSHAddressCalled bool CommHostCalled bool
SSHAddressState multistep.StateBag CommHostState multistep.StateBag
SSHAddressResult string CommHostResult string
SSHAddressErr error CommHostErr error
StartCalled bool StartCalled bool
StartPath string StartPath string
...@@ -92,10 +92,10 @@ func (d *DriverMock) IsRunning(path string) (bool, error) { ...@@ -92,10 +92,10 @@ func (d *DriverMock) IsRunning(path string) (bool, error) {
return d.IsRunningResult, d.IsRunningErr return d.IsRunningResult, d.IsRunningErr
} }
func (d *DriverMock) SSHAddress(state multistep.StateBag) (string, error) { func (d *DriverMock) CommHost(state multistep.StateBag) (string, error) {
d.SSHAddressCalled = true d.CommHostCalled = true
d.SSHAddressState = state d.CommHostState = state
return d.SSHAddressResult, d.SSHAddressErr return d.CommHostResult, d.CommHostErr
} }
func (d *DriverMock) Start(path string, headless bool) error { func (d *DriverMock) Start(path string, headless bool) error {
......
...@@ -97,8 +97,8 @@ func (d *Player5Driver) IsRunning(vmxPath string) (bool, error) { ...@@ -97,8 +97,8 @@ func (d *Player5Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Player5Driver) SSHAddress(state multistep.StateBag) (string, error) { func (d *Player5Driver) CommHost(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state) return CommHost(d.SSHConfig)(state)
} }
func (d *Player5Driver) Start(vmxPath string, headless bool) error { func (d *Player5Driver) Start(vmxPath string, headless bool) error {
......
...@@ -70,8 +70,8 @@ func (d *Workstation9Driver) IsRunning(vmxPath string) (bool, error) { ...@@ -70,8 +70,8 @@ func (d *Workstation9Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Workstation9Driver) SSHAddress(state multistep.StateBag) (string, error) { func (d *Workstation9Driver) CommHost(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state) return CommHost(d.SSHConfig)(state)
} }
func (d *Workstation9Driver) Start(vmxPath string, headless bool) error { func (d *Workstation9Driver) Start(vmxPath string, headless bool) error {
......
...@@ -2,7 +2,6 @@ package common ...@@ -2,7 +2,6 @@ package common
import ( import (
"fmt" "fmt"
"os"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig ...@@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName) c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
} }
var errs []error return nil
if !pc.PackerForce {
if _, err := os.Stat(c.OutputDir); err == nil {
errs = append(errs, fmt.Errorf(
"Output directory '%s' already exists. It must not exist.", c.OutputDir))
}
}
return errs
} }
package common package common
import ( import (
"github.com/mitchellh/packer/common"
"io/ioutil"
"os"
"testing" "testing"
"github.com/mitchellh/packer/common"
) )
func TestOutputConfigPrepare(t *testing.T) { func TestOutputConfigPrepare(t *testing.T) {
...@@ -23,43 +22,3 @@ func TestOutputConfigPrepare(t *testing.T) { ...@@ -23,43 +22,3 @@ func TestOutputConfigPrepare(t *testing.T) {
t.Fatal("should have output dir") t.Fatal("should have output dir")
} }
} }
func TestOutputConfigPrepare_exists(t *testing.T) {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
c := new(OutputConfig)
c.OutputDir = td
pc := &common.PackerConfig{
PackerBuildName: "foo",
PackerForce: false,
}
errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) == 0 {
t.Fatal("should have errors")
}
}
func TestOutputConfigPrepare_forceExists(t *testing.T) {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
c := new(OutputConfig)
c.OutputDir = td
pc := &common.PackerConfig{
PackerBuildName: "foo",
PackerForce: true,
}
errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) > 0 {
t.Fatal("should not have errors")
}
}
...@@ -13,13 +13,13 @@ import ( ...@@ -13,13 +13,13 @@ import (
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
func SSHAddressFunc(config *SSHConfig) func(multistep.StateBag) (string, error) { func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
vmxPath := state.Get("vmx_path").(string) vmxPath := state.Get("vmx_path").(string)
if config.SSHHost != "" { if config.Comm.SSHHost != "" {
return fmt.Sprintf("%s:%d", config.SSHHost, config.SSHPort), nil return config.Comm.SSHHost, nil
} }
log.Println("Lookup up IP information...") log.Println("Lookup up IP information...")
...@@ -62,20 +62,20 @@ func SSHAddressFunc(config *SSHConfig) func(multistep.StateBag) (string, error) ...@@ -62,20 +62,20 @@ func SSHAddressFunc(config *SSHConfig) func(multistep.StateBag) (string, error)
} }
log.Printf("Detected IP: %s", ipAddress) log.Printf("Detected IP: %s", ipAddress)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil return ipAddress, nil
} }
} }
func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) { func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) { return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
auth := []gossh.AuthMethod{ auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword), gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive( gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)), ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
} }
if config.SSHKeyPath != "" { if config.Comm.SSHPrivateKey != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -84,7 +84,7 @@ func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientCon ...@@ -84,7 +84,7 @@ func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientCon
} }
return &gossh.ClientConfig{ return &gossh.ClientConfig{
User: config.SSHUser, User: config.Comm.SSHUsername,
Auth: auth, Auth: auth,
}, nil }, nil
} }
......
package common package common
import ( import (
"errors"
"fmt"
"net"
"os"
"time" "time"
commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
type SSHConfig struct { type SSHConfig struct {
SSHUser string `mapstructure:"ssh_username"` Comm communicator.Config `mapstructure:",squash"`
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"` SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHHost string `mapstructure:"ssh_host"`
SSHPort uint `mapstructure:"ssh_port"`
SSHSkipRequestPty bool `mapstructure:"ssh_skip_request_pty"` SSHSkipRequestPty bool `mapstructure:"ssh_skip_request_pty"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
SSHWaitTimeout time.Duration
} }
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error { func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
if c.SSHPort == 0 { // TODO: backwards compatibility, write fixer instead
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
if c.SSHKeyPath != "" { if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil { c.Comm.SSHPrivateKey = c.SSHKeyPath
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} }
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
} }
if c.SSHSkipRequestPty {
if c.SSHHost != "" { c.Comm.SSHPty = false
if ip := net.ParseIP(c.SSHHost); ip == nil {
if _, err := net.LookupHost(c.SSHHost); err != nil {
errs = append(errs, errors.New("ssh_host is an invalid IP or hostname"))
}
}
}
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
var err error
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
} }
return errs return c.Comm.Prepare(ctx)
} }
...@@ -4,11 +4,15 @@ import ( ...@@ -4,11 +4,15 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func testSSHConfig() *SSHConfig { func testSSHConfig() *SSHConfig {
return &SSHConfig{ return &SSHConfig{
SSHUser: "foo", Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) { ...@@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Fatalf("err: %#v", errs) t.Fatalf("err: %#v", errs)
} }
if c.SSHPort != 22 { if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort) t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
} }
} }
...@@ -73,57 +77,6 @@ func TestSSHConfigPrepare_SSHKeyPath(t *testing.T) { ...@@ -73,57 +77,6 @@ func TestSSHConfigPrepare_SSHKeyPath(t *testing.T) {
} }
} }
func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var c *SSHConfig
var errs []error
c = testSSHConfig()
c.SSHUser = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatalf("should have error")
}
c = testSSHConfig()
c.SSHUser = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
var c *SSHConfig
var errs []error
// Defaults
c = testSSHConfig()
c.RawSSHWaitTimeout = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
if c.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
}
// Test with a bad value
c = testSSHConfig()
c.RawSSHWaitTimeout = "this is not good"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatal("should have error")
}
// Test with a good one
c = testSSHConfig()
c.RawSSHWaitTimeout = "5s"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
const testPem = ` const testPem = `
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
vmwcommon "github.com/mitchellh/packer/builder/vmware/common" vmwcommon "github.com/mitchellh/packer/builder/vmware/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -298,11 +299,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -298,11 +299,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: driver.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig), SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
NoPty: b.config.SSHSkipRequestPty,
}, },
&vmwcommon.StepUploadTools{ &vmwcommon.StepUploadTools{
RemoteType: b.config.RemoteType, RemoteType: b.config.RemoteType,
......
package iso package iso
import ( import (
"github.com/mitchellh/packer/packer"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/mitchellh/packer/packer"
) )
func testConfig() map[string]interface{} { func testConfig() map[string]interface{} {
...@@ -138,10 +138,6 @@ func TestBuilderPrepare_Defaults(t *testing.T) { ...@@ -138,10 +138,6 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad Version: %s", b.config.Version) t.Errorf("bad Version: %s", b.config.Version)
} }
if b.config.SSHWaitTimeout != (20 * time.Minute) {
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
}
if b.config.VMName != "packer-foo" { if b.config.VMName != "packer-foo" {
t.Errorf("bad vm name: %s", b.config.VMName) t.Errorf("bad vm name: %s", b.config.VMName)
} }
...@@ -353,8 +349,8 @@ func TestBuilderPrepare_OutputDir(t *testing.T) { ...@@ -353,8 +349,8 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
if len(warns) > 0 { if len(warns) > 0 {
t.Fatalf("bad: %#v", warns) t.Fatalf("bad: %#v", warns)
} }
if err == nil { if err != nil {
t.Fatal("should have error") t.Fatalf("err: %s", err)
} }
// Test with a good one // Test with a good one
......
...@@ -218,7 +218,7 @@ func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) { ...@@ -218,7 +218,7 @@ func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) {
return d.Host, vncPort, nil return d.Host, vncPort, nil
} }
func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) { func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
if address, ok := state.GetOk("vm_address"); ok { if address, ok := state.GetOk("vm_address"); ok {
...@@ -253,7 +253,7 @@ func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) { ...@@ -253,7 +253,7 @@ func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) {
return "", errors.New("VM network port found, but no IP address") return "", errors.New("VM network port found, but no IP address")
} }
address := fmt.Sprintf("%s:%d", record["IPAddress"], config.SSHPort) address := record["IPAddress"]
state.Put("vm_address", address) state.Put("vm_address", address)
return address, nil return address, nil
} }
...@@ -335,7 +335,6 @@ func (d *ESX5Driver) connect() error { ...@@ -335,7 +335,6 @@ func (d *ESX5Driver) connect() error {
User: d.Username, User: d.Username,
Auth: auth, Auth: auth,
}, },
NoPty: true,
} }
comm, err := ssh.New(address, sshConfig) comm, err := ssh.New(address, sshConfig)
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
vmwcommon "github.com/mitchellh/packer/builder/vmware/common" vmwcommon "github.com/mitchellh/packer/builder/vmware/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -90,11 +91,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -90,11 +91,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName, VMName: b.config.VMName,
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: driver.SSHAddress, Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig), SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
NoPty: b.config.SSHSkipRequestPty,
}, },
&vmwcommon.StepUploadTools{ &vmwcommon.StepUploadTools{
RemoteType: b.config.RemoteType, RemoteType: b.config.RemoteType,
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/mitchellh/packer/fix" "github.com/mitchellh/packer/fix"
"github.com/mitchellh/packer/template"
) )
type FixCommand struct { type FixCommand struct {
...@@ -16,7 +17,9 @@ type FixCommand struct { ...@@ -16,7 +17,9 @@ type FixCommand struct {
} }
func (c *FixCommand) Run(args []string) int { func (c *FixCommand) Run(args []string) int {
var flagValidate bool
flags := c.Meta.FlagSet("fix", FlagSetNone) flags := c.Meta.FlagSet("fix", FlagSetNone)
flags.BoolVar(&flagValidate, "validate", true, "")
flags.Usage = func() { c.Ui.Say(c.Help()) } flags.Usage = func() { c.Ui.Say(c.Help()) }
if err := flags.Parse(args); err != nil { if err := flags.Parse(args); err != nil {
return 1 return 1
...@@ -80,6 +83,28 @@ func (c *FixCommand) Run(args []string) int { ...@@ -80,6 +83,28 @@ func (c *FixCommand) Run(args []string) int {
result = strings.Replace(result, `\u003c`, "<", -1) result = strings.Replace(result, `\u003c`, "<", -1)
result = strings.Replace(result, `\u003e`, ">", -1) result = strings.Replace(result, `\u003e`, ">", -1)
c.Ui.Say(result) c.Ui.Say(result)
if flagValidate {
// Attemot to parse and validate the template
tpl, err := template.Parse(strings.NewReader(result))
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error! Fixed template fails to parse: %s\n\n"+
"This is usually caused by an error in the input template.\n"+
"Please fix the error and try again.",
err))
return 1
}
if err := tpl.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error! Fixed template failed to validate: %s\n\n"+
"This is usually caused by an error in the input template.\n"+
"Please fix the error and try again.",
err))
return 1
}
}
return 0 return 0
} }
...@@ -102,6 +127,10 @@ Fixes that are run: ...@@ -102,6 +127,10 @@ Fixes that are run:
pp-vagrant-override Replaces old-style provider overrides for the Vagrant pp-vagrant-override Replaces old-style provider overrides for the Vagrant
post-processor to new-style as of Packer 0.5.0. post-processor to new-style as of Packer 0.5.0.
virtualbox-rename Updates "virtualbox" builders to "virtualbox-iso" virtualbox-rename Updates "virtualbox" builders to "virtualbox-iso"
Options:
-validate=true If true (default), validates the fixed template.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
......
package command
import (
"path/filepath"
"testing"
)
func TestFix_noArgs(t *testing.T) {
c := &PushCommand{Meta: testMeta(t)}
code := c.Run(nil)
if code != 1 {
t.Fatalf("bad: %#v", code)
}
}
func TestFix_multiArgs(t *testing.T) {
c := &PushCommand{Meta: testMeta(t)}
code := c.Run([]string{"one", "two"})
if code != 1 {
t.Fatalf("bad: %#v", code)
}
}
func TestFix(t *testing.T) {
c := &FixCommand{
Meta: testMeta(t),
}
args := []string{filepath.Join(testFixture("fix"), "template.json")}
if code := c.Run(args); code != 0 {
fatalCommand(t, c.Meta)
}
}
func TestFix_invalidTemplate(t *testing.T) {
c := &FixCommand{
Meta: testMeta(t),
}
args := []string{filepath.Join(testFixture("fix-invalid"), "template.json")}
if code := c.Run(args); code != 1 {
fatalCommand(t, c.Meta)
}
}
func TestFix_invalidTemplateDisableValidation(t *testing.T) {
c := &FixCommand{
Meta: testMeta(t),
}
args := []string{
"-validate=false",
filepath.Join(testFixture("fix-invalid"), "template.json"),
}
if code := c.Run(args); code != 0 {
fatalCommand(t, c.Meta)
}
}
{
"builders": [{"type": "dummy"}],
"push": {
"name": "foo/bar"
}
}
...@@ -99,6 +99,14 @@ func DownloadableURL(original string) (string, error) { ...@@ -99,6 +99,14 @@ func DownloadableURL(original string) (string, error) {
// Make sure it is lowercased // Make sure it is lowercased
url.Scheme = strings.ToLower(url.Scheme) url.Scheme = strings.ToLower(url.Scheme)
// This is to work around issue #5927. This can safely be removed once
// we distribute with a version of Go that fixes that bug.
//
// See: https://code.google.com/p/go/issues/detail?id=5927
if url.Path != "" && url.Path[0] != '/' {
url.Path = "/" + url.Path
}
// Verify that the scheme is something we support in our common downloader. // Verify that the scheme is something we support in our common downloader.
supported := []string{"file", "http", "https"} supported := []string{"file", "http", "https"}
found := false found := false
......
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepConnectSSH_Impl(t *testing.T) {
var raw interface{}
raw = new(StepConnectSSH)
if _, ok := raw.(multistep.Step); !ok {
t.Fatalf("connect ssh should be a step")
}
}
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
...@@ -33,8 +34,8 @@ type Config struct { ...@@ -33,8 +34,8 @@ type Config struct {
// case an error occurs. // case an error occurs.
Connection func() (net.Conn, error) Connection func() (net.Conn, error)
// NoPty, if true, will not request a pty from the remote end. // Pty, if true, will request a pty from the remote end.
NoPty bool Pty bool
} }
// Creates a new packer.Communicator implementation over SSH. This takes // Creates a new packer.Communicator implementation over SSH. This takes
...@@ -65,7 +66,7 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) { ...@@ -65,7 +66,7 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
session.Stdout = cmd.Stdout session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr session.Stderr = cmd.Stderr
if !c.config.NoPty { if c.config.Pty {
// Request a PTY // Request a PTY
termModes := ssh.TerminalModes{ termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo ssh.ECHO: 0, // do not echo
...@@ -226,10 +227,59 @@ func (c *comm) reconnect() (err error) { ...@@ -226,10 +227,59 @@ func (c *comm) reconnect() (err error) {
if sshConn != nil { if sshConn != nil {
c.client = ssh.NewClient(sshConn, sshChan, req) c.client = ssh.NewClient(sshConn, sshChan, req)
} }
c.connectToAgent()
return return
} }
func (c *comm) connectToAgent() {
if c.client == nil {
return
}
// open connection to the local agent
socketLocation := os.Getenv("SSH_AUTH_SOCK")
if socketLocation == "" {
log.Printf("[INFO] no local agent socket, will not connect agent")
return
}
agentConn, err := net.Dial("unix", socketLocation)
if err != nil {
log.Printf("[ERROR] could not connect to local agent socket: %s", socketLocation)
return
}
// create agent and add in auth
forwardingAgent := agent.NewClient(agentConn)
if forwardingAgent == nil {
log.Printf("[ERROR] Could not create agent client")
agentConn.Close()
return
}
// add callback for forwarding agent to SSH config
// XXX - might want to handle reconnects appending multiple callbacks
auth := ssh.PublicKeysCallback(forwardingAgent.Signers)
c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth)
agent.ForwardToAgent(c.client, forwardingAgent)
// Setup a session to request agent forwarding
session, err := c.newSession()
if err != nil {
return
}
defer session.Close()
err = agent.RequestAgentForwarding(session)
if err != nil {
log.Printf("[ERROR] RequestAgentForwarding: %#v", err)
return
}
log.Printf("[INFO] agent forwarding enabled")
return
}
func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error { func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.newSession() session, err := c.newSession()
if err != nil { if err != nil {
......
package winrm
import (
"fmt"
"io"
"log"
"os"
"github.com/masterzen/winrm/winrm"
"github.com/mitchellh/packer/packer"
"github.com/packer-community/winrmcp/winrmcp"
// This import is a bit strange, but it's needed so `make updatedeps`
// can see and download it
_ "github.com/dylanmei/winrmtest"
)
// Communicator represents the WinRM communicator
type Communicator struct {
config *Config
client *winrm.Client
endpoint *winrm.Endpoint
}
// New creates a new communicator implementation over WinRM.
func New(config *Config) (*Communicator, error) {
endpoint := &winrm.Endpoint{
Host: config.Host,
Port: config.Port,
/*
TODO
HTTPS: connInfo.HTTPS,
Insecure: connInfo.Insecure,
CACert: connInfo.CACert,
*/
}
// Create the client
params := winrm.DefaultParameters()
params.Timeout = formatDuration(config.Timeout)
client, err := winrm.NewClientWithParameters(
endpoint, config.Username, config.Password, params)
if err != nil {
return nil, err
}
// Create the shell to verify the connection
log.Printf("[DEBUG] connecting to remote shell using WinRM")
shell, err := client.CreateShell()
if err != nil {
log.Printf("[ERROR] connection error: %s", err)
return nil, err
}
if err := shell.Close(); err != nil {
log.Printf("[ERROR] error closing connection: %s", err)
return nil, err
}
return &Communicator{
config: config,
client: client,
endpoint: endpoint,
}, nil
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(rc *packer.RemoteCmd) error {
shell, err := c.client.CreateShell()
if err != nil {
return err
}
log.Printf("[INFO] starting remote command: %s", rc.Command)
cmd, err := shell.Execute(rc.Command)
if err != nil {
return err
}
go runCommand(shell, cmd, rc)
return nil
}
func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *packer.RemoteCmd) {
defer shell.Close()
go io.Copy(rc.Stdout, cmd.Stdout)
go io.Copy(rc.Stderr, cmd.Stderr)
cmd.Wait()
rc.SetExited(cmd.ExitCode())
}
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader, _ *os.FileInfo) error {
wcp, err := c.newCopyClient()
if err != nil {
return err
}
log.Printf("Uploading file to '%s'", path)
return wcp.Write(path, input)
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
log.Printf("Uploading dir '%s' to '%s'", src, dst)
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Copy(src, dst)
}
func (c *Communicator) Download(src string, dst io.Writer) error {
panic("download not implemented")
}
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
return winrmcp.New(addr, &winrmcp.Config{
Auth: winrmcp.Auth{
User: c.config.Username,
Password: c.config.Password,
},
OperationTimeout: c.config.Timeout,
MaxOperationsPerShell: 15, // lowest common denominator
})
}
package winrm
import (
"bytes"
"io"
"testing"
"time"
"github.com/dylanmei/winrmtest"
"github.com/mitchellh/packer/packer"
)
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
wrm := winrmtest.NewRemote()
wrm.CommandFunc(
winrmtest.MatchText("echo foo"),
func(out, err io.Writer) int {
out.Write([]byte("foo"))
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^echo c29tZXRoaW5n >> ".*"$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^powershell.exe -EncodedCommand .*$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchText("powershell"),
func(out, err io.Writer) int {
return 0
})
return wrm
}
func TestStart(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
var cmd packer.RemoteCmd
stdout := new(bytes.Buffer)
cmd.Command = "echo foo"
cmd.Stdout = stdout
err = c.Start(&cmd)
if err != nil {
t.Fatalf("error executing remote command: %s", err)
}
cmd.Wait()
if stdout.String() != "foo" {
t.Fatalf("bad command response: expected %q, got %q", "foo", stdout.String())
}
}
func TestUpload(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
err = c.Upload("C:/Temp/terraform.cmd", bytes.NewReader([]byte("something")), nil)
if err != nil {
t.Fatalf("error uploading file: %s", err)
}
}
package winrm
import (
"time"
)
// Config is used to configure the WinRM connection
type Config struct {
Host string
Port int
Username string
Password string
Timeout time.Duration
}
package winrm
import (
"fmt"
"time"
)
// formatDuration formats the given time.Duration into an ISO8601
// duration string.
func formatDuration(duration time.Duration) string {
// We're not supporting negative durations
if duration.Seconds() <= 0 {
return "PT0S"
}
h := int(duration.Hours())
m := int(duration.Minutes()) - (h * 60)
s := int(duration.Seconds()) - (h*3600 + m*60)
res := "PT"
if h > 0 {
res = fmt.Sprintf("%s%dH", res, h)
}
if m > 0 {
res = fmt.Sprintf("%s%dM", res, m)
}
if s > 0 {
res = fmt.Sprintf("%s%dS", res, s)
}
return res
}
package winrm
import (
"testing"
"time"
)
func TestFormatDuration(t *testing.T) {
// Test complex duration with hours, minutes, seconds
d := time.Duration(3701) * time.Second
s := formatDuration(d)
if s != "PT1H1M41S" {
t.Fatalf("bad ISO 8601 duration string: %s", s)
}
// Test only minutes duration
d = time.Duration(20) * time.Minute
s = formatDuration(d)
if s != "PT20M" {
t.Fatalf("bad ISO 8601 duration string for 20M: %s", s)
}
// Test only seconds
d = time.Duration(1) * time.Second
s = formatDuration(d)
if s != "PT1S" {
t.Fatalf("bad ISO 8601 duration string for 1S: %s", s)
}
// Test negative duration (unsupported)
d = time.Duration(-1) * time.Second
s = formatDuration(d)
if s != "PT0S" {
t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
}
}
...@@ -8,14 +8,14 @@ import ( ...@@ -8,14 +8,14 @@ import (
type FixerVirtualBoxRename struct{} type FixerVirtualBoxRename struct{}
func (FixerVirtualBoxRename) Fix(input map[string]interface{}) (map[string]interface{}, error) { func (FixerVirtualBoxRename) Fix(input map[string]interface{}) (map[string]interface{}, error) {
// The type we'll decode into; we only care about builders
type template struct { type template struct {
Builders []map[string]interface{} Builders []map[string]interface{}
Provisioners []interface{}
} }
// Decode the input into our structure, if we can // Decode the input into our structure, if we can
var tpl template var tpl template
if err := mapstructure.Decode(input, &tpl); err != nil { if err := mapstructure.WeakDecode(input, &tpl); err != nil {
return nil, err return nil, err
} }
...@@ -37,7 +37,39 @@ func (FixerVirtualBoxRename) Fix(input map[string]interface{}) (map[string]inter ...@@ -37,7 +37,39 @@ func (FixerVirtualBoxRename) Fix(input map[string]interface{}) (map[string]inter
builder["type"] = "virtualbox-iso" builder["type"] = "virtualbox-iso"
} }
for i, raw := range tpl.Provisioners {
var m map[string]interface{}
if err := mapstructure.WeakDecode(raw, &m); err != nil {
// Ignore errors, could be a non-map
continue
}
raw, ok := m["override"]
if !ok {
continue
}
var override map[string]interface{}
if err := mapstructure.WeakDecode(raw, &override); err != nil {
return nil, err
}
if raw, ok := override["virtualbox"]; ok {
override["virtualbox-iso"] = raw
delete(override, "virtualbox")
// Set the change
m["override"] = override
tpl.Provisioners[i] = m
}
}
if len(tpl.Builders) > 0 {
input["builders"] = tpl.Builders input["builders"] = tpl.Builders
}
if len(tpl.Provisioners) > 0 {
input["provisioners"] = tpl.Provisioners
}
return input, nil return input, nil
} }
......
...@@ -46,3 +46,45 @@ func TestFixerVirtualBoxRename_Fix(t *testing.T) { ...@@ -46,3 +46,45 @@ func TestFixerVirtualBoxRename_Fix(t *testing.T) {
} }
} }
} }
func TestFixerVirtualBoxRenameFix_provisionerOverride(t *testing.T) {
cases := []struct {
Input map[string]interface{}
Expected map[string]interface{}
}{
{
Input: map[string]interface{}{
"provisioners": []interface{}{
map[string]interface{}{
"override": map[string]interface{}{
"virtualbox": map[string]interface{}{},
},
},
},
},
Expected: map[string]interface{}{
"provisioners": []interface{}{
map[string]interface{}{
"override": map[string]interface{}{
"virtualbox-iso": map[string]interface{}{},
},
},
},
},
},
}
for _, tc := range cases {
var f FixerVirtualBoxRename
output, err := f.Fix(tc.Input)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(output, tc.Expected) {
t.Fatalf("unexpected:\n\n%#v\nexpected:\n\n%#v\n", output, tc.Expected)
}
}
}
...@@ -41,6 +41,10 @@ type TestCase struct { ...@@ -41,6 +41,10 @@ type TestCase struct {
// in the case that the test can't guarantee all resources were // in the case that the test can't guarantee all resources were
// properly cleaned up. // properly cleaned up.
Teardown TestTeardownFunc Teardown TestTeardownFunc
// If SkipArtifactTeardown is true, we will not attempt to destroy the
// artifact created in this test run.
SkipArtifactTeardown bool
} }
// TestCheckFunc is the callback used for Check in TestStep. // TestCheckFunc is the callback used for Check in TestStep.
...@@ -163,6 +167,7 @@ func Test(t TestT, c TestCase) { ...@@ -163,6 +167,7 @@ func Test(t TestT, c TestCase) {
} }
TEARDOWN: TEARDOWN:
if !c.SkipArtifactTeardown {
// Delete all artifacts // Delete all artifacts
for _, a := range artifacts { for _, a := range artifacts {
if err := a.Destroy(); err != nil { if err := a.Destroy(); err != nil {
...@@ -171,6 +176,7 @@ TEARDOWN: ...@@ -171,6 +176,7 @@ TEARDOWN:
a.String(), err)) a.String(), err))
} }
} }
}
// Teardown // Teardown
if c.Teardown != nil { if c.Teardown != nil {
......
package communicator
import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/packer/template/interpolate"
)
// Config is the common configuration that communicators allow within
// a builder.
type Config struct {
Type string `mapstructure:"communicator"`
// SSH
SSHHost string `mapstructure:"ssh_host"`
SSHPort int `mapstructure:"ssh_port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKey string `mapstructure:"ssh_private_key_file"`
SSHPty bool `mapstructure:"ssh_pty"`
SSHTimeout time.Duration `mapstructure:"ssh_timeout"`
SSHHandshakeAttempts int `mapstructure:"ssh_handshake_attempts"`
// WinRM
WinRMUser string `mapstructure:"winrm_username"`
WinRMPassword string `mapstructure:"winrm_password"`
WinRMHost string `mapstructure:"winrm_host"`
WinRMPort int `mapstructure:"winrm_port"`
WinRMTimeout time.Duration `mapstructure:"winrm_timeout"`
}
// Port returns the port that will be used for access based on config.
func (c *Config) Port() int {
switch c.Type {
case "ssh":
return c.SSHPort
case "winrm":
return c.WinRMPort
default:
return 0
}
}
func (c *Config) Prepare(ctx *interpolate.Context) []error {
if c.Type == "" {
c.Type = "ssh"
}
var errs []error
switch c.Type {
case "ssh":
if es := c.prepareSSH(ctx); len(es) > 0 {
errs = append(errs, es...)
}
case "winrm":
if es := c.prepareWinRM(ctx); len(es) > 0 {
errs = append(errs, es...)
}
}
return errs
}
func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.SSHTimeout == 0 {
c.SSHTimeout = 5 * time.Minute
}
if c.SSHHandshakeAttempts == 0 {
c.SSHHandshakeAttempts = 10
}
// Validation
var errs []error
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
if c.SSHPrivateKey != "" {
if _, err := os.Stat(c.SSHPrivateKey); err != nil {
errs = append(errs, fmt.Errorf(
"ssh_private_key_file is invalid: %s", err))
} else if _, err := SSHFileSigner(c.SSHPrivateKey); err != nil {
errs = append(errs, fmt.Errorf(
"ssh_private_key_file is invalid: %s", err))
}
}
return errs
}
func (c *Config) prepareWinRM(ctx *interpolate.Context) []error {
if c.WinRMPort == 0 {
c.WinRMPort = 5985
}
if c.WinRMTimeout == 0 {
c.WinRMTimeout = 30 * time.Minute
}
var errs []error
if c.WinRMUser == "" {
errs = append(errs, errors.New("winrm_username must be specified."))
}
return errs
}
package communicator
import (
"testing"
"github.com/mitchellh/packer/template/interpolate"
)
func testConfig() *Config {
return &Config{
SSHUsername: "root",
}
}
func TestConfigType(t *testing.T) {
c := testConfig()
if err := c.Prepare(testContext(t)); len(err) > 0 {
t.Fatalf("bad: %#v", err)
}
if c.Type != "ssh" {
t.Fatalf("bad: %#v", c)
}
}
func TestConfig_none(t *testing.T) {
c := &Config{Type: "none"}
if err := c.Prepare(testContext(t)); len(err) > 0 {
t.Fatalf("bad: %#v", err)
}
}
func testContext(t *testing.T) *interpolate.Context {
return nil
}
package communicator
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"golang.org/x/crypto/ssh"
)
// SSHFileSigner returns an ssh.Signer for a key file.
func SSHFileSigner(path string) (ssh.Signer, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
// We parse the private key on our own first so that we can
// show a nicer error if the private key has a password.
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, fmt.Errorf(
"Failed to read key '%s': no key found", path)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return nil, fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"+
"not supported. Please decrypt the key prior to use.", path)
}
signer, err := ssh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return signer, nil
}
package communicator
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
gossh "golang.org/x/crypto/ssh"
)
// StepConnect is a multistep Step implementation that connects to
// the proper communicator and stores it in the "communicator" key in the
// state bag.
type StepConnect struct {
// Config is the communicator config struct
Config *Config
// Host should return a host that can be connected to for communicator
// connections.
Host func(multistep.StateBag) (string, error)
// The fields below are callbacks to assist with connecting to SSH.
//
// SSHConfig should return the default configuration for
// connecting via SSH.
SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error)
SSHPort func(multistep.StateBag) (int, error)
// The fields below are callbacks to assist with connecting to WinRM.
//
// WinRMConfig should return the default configuration for
// connecting via WinRM.
WinRMConfig func(multistep.StateBag) (*WinRMConfig, error)
// CustomConnect can be set to have custom connectors for specific
// types. These take highest precedence so you can also override
// existing types.
CustomConnect map[string]multistep.Step
substep multistep.Step
}
func (s *StepConnect) Run(state multistep.StateBag) multistep.StepAction {
typeMap := map[string]multistep.Step{
"none": nil,
"ssh": &StepConnectSSH{
Config: s.Config,
Host: s.Host,
SSHConfig: s.SSHConfig,
SSHPort: s.SSHPort,
},
"winrm": &StepConnectWinRM{
Config: s.Config,
Host: s.Host,
WinRMConfig: s.WinRMConfig,
},
}
for k, v := range s.CustomConnect {
typeMap[k] = v
}
step, ok := typeMap[s.Config.Type]
if !ok {
state.Put("error", fmt.Errorf("unknown communicator type: %s", s.Config.Type))
return multistep.ActionHalt
}
if step == nil {
log.Printf("[INFO] communicator disabled, will not connect")
return multistep.ActionContinue
}
s.substep = step
return s.substep.Run(state)
}
func (s *StepConnect) Cleanup(state multistep.StateBag) {
if s.substep != nil {
s.substep.Cleanup(state)
}
}
package common package communicator
import ( import (
"errors" "errors"
...@@ -13,32 +13,15 @@ import ( ...@@ -13,32 +13,15 @@ import (
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
// StepConnectSSH is a multistep Step implementation that waits for SSH // StepConnectSSH is a step that only connects to SSH.
// to become available. It gets the connection information from a single
// configuration when creating the step.
// //
// Uses: // In general, you should use StepConnect.
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
type StepConnectSSH struct { type StepConnectSSH struct {
// SSHAddress is a function that returns the TCP address to connect to // All the fields below are documented on StepConnect
// for SSH. This is a function so that you can query information Config *Config
// if necessary for this address. Host func(multistep.StateBag) (string, error)
SSHAddress func(multistep.StateBag) (string, error)
// SSHConfig is a function that returns the proper client configuration
// for SSH access.
SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error) SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error)
SSHPort func(multistep.StateBag) (int, error)
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
// NoPty, if true, will not request a Pty from the remote end.
NoPty bool
comm packer.Communicator
} }
func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction { func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction {
...@@ -55,8 +38,8 @@ func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction { ...@@ -55,8 +38,8 @@ func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction {
waitDone <- true waitDone <- true
}() }()
log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout) log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout)
timeout := time.After(s.SSHWaitTimeout) timeout := time.After(s.Config.SSHTimeout)
WaitLoop: WaitLoop:
for { for {
// Wait for either SSH to become available, a timeout to occur, // Wait for either SSH to become available, a timeout to occur,
...@@ -70,7 +53,6 @@ WaitLoop: ...@@ -70,7 +53,6 @@ WaitLoop:
} }
ui.Say("Connected to SSH!") ui.Say("Connected to SSH!")
s.comm = comm
state.Put("communicator", comm) state.Put("communicator", comm)
break WaitLoop break WaitLoop
case <-timeout: case <-timeout:
...@@ -84,7 +66,7 @@ WaitLoop: ...@@ -84,7 +66,7 @@ WaitLoop:
// The step sequence was cancelled, so cancel waiting for SSH // The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process. // and just start the halting process.
close(cancel) close(cancel)
log.Println("Interrupt detected, quitting waiting for SSH.") log.Println("[WARN] Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt return multistep.ActionHalt
} }
} }
...@@ -106,7 +88,7 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru ...@@ -106,7 +88,7 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
if !first { if !first {
select { select {
case <-cancel: case <-cancel:
log.Println("SSH wait cancelled. Exiting loop.") log.Println("[DEBUG] SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled") return nil, errors.New("SSH wait cancelled")
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
} }
...@@ -114,24 +96,34 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru ...@@ -114,24 +96,34 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
first = false first = false
// First we request the TCP connection information // First we request the TCP connection information
address, err := s.SSHAddress(state) host, err := s.Host(state)
if err != nil { if err != nil {
log.Printf("Error getting SSH address: %s", err) log.Printf("[DEBUG] Error getting SSH address: %s", err)
continue continue
} }
port := s.Config.SSHPort
if s.SSHPort != nil {
port, err = s.SSHPort(state)
if err != nil {
log.Printf("[DEBUG] Error getting SSH port: %s", err)
continue
}
}
// Retrieve the SSH configuration // Retrieve the SSH configuration
sshConfig, err := s.SSHConfig(state) sshConfig, err := s.SSHConfig(state)
if err != nil { if err != nil {
log.Printf("Error getting SSH config: %s", err) log.Printf("[DEBUG] Error getting SSH config: %s", err)
continue continue
} }
address := fmt.Sprintf("%s:%d", host, port)
// Attempt to connect to SSH port // Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address) connFunc := ssh.ConnectFunc("tcp", address)
nc, err := connFunc() nc, err := connFunc()
if err != nil { if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err) log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err)
continue continue
} }
nc.Close() nc.Close()
...@@ -140,24 +132,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru ...@@ -140,24 +132,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
config := &ssh.Config{ config := &ssh.Config{
Connection: connFunc, Connection: connFunc,
SSHConfig: sshConfig, SSHConfig: sshConfig,
NoPty: s.NoPty, Pty: s.Config.SSHPty,
} }
log.Println("Attempting SSH connection...") log.Println("[INFO] Attempting SSH connection...")
comm, err = ssh.New(address, config) comm, err = ssh.New(address, config)
if err != nil { if err != nil {
log.Printf("SSH handshake err: %s", err) log.Printf("[DEBUG] SSH handshake err: %s", err)
// Only count this as an attempt if we were able to attempt // Only count this as an attempt if we were able to attempt
// to authenticate. Note this is very brittle since it depends // to authenticate. Note this is very brittle since it depends
// on the string of the error... but I don't see any other way. // on the string of the error... but I don't see any other way.
if strings.Contains(err.Error(), "authenticate") { if strings.Contains(err.Error(), "authenticate") {
log.Printf("Detected authentication error. Increasing handshake attempts.") log.Printf(
"[DEBUG] Detected authentication error. Increasing handshake attempts.")
handshakeAttempts += 1 handshakeAttempts += 1
} }
if handshakeAttempts < 10 { if handshakeAttempts < s.Config.SSHHandshakeAttempts {
// Try to connect via SSH a handful of times // Try to connect via SSH a handful of times. We sleep here
// so we don't get a ton of authentication errors back to back.
time.Sleep(2 * time.Second)
continue continue
} }
......
package communicator
import (
"bytes"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepConnect_impl(t *testing.T) {
var _ multistep.Step = new(StepConnect)
}
func TestStepConnect_none(t *testing.T) {
state := testState(t)
step := &StepConnect{
Config: &Config{
Type: "none",
},
}
defer step.Cleanup(state)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
}
func testState(t *testing.T) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("hook", &packer.MockHook{})
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
return state
}
package communicator
import (
"errors"
"fmt"
"log"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/winrm"
"github.com/mitchellh/packer/packer"
)
// StepConnectWinRM is a multistep Step implementation that waits for WinRM
// to become available. It gets the connection information from a single
// configuration when creating the step.
//
// Uses:
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
type StepConnectWinRM struct {
// All the fields below are documented on StepConnect
Config *Config
Host func(multistep.StateBag) (string, error)
WinRMConfig func(multistep.StateBag) (*WinRMConfig, error)
}
func (s *StepConnectWinRM) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
var comm packer.Communicator
var err error
cancel := make(chan struct{})
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for WinRM to become available...")
comm, err = s.waitForWinRM(state, cancel)
waitDone <- true
}()
log.Printf("Waiting for WinRM, up to timeout: %s", s.Config.WinRMTimeout)
timeout := time.After(s.Config.WinRMTimeout)
WaitLoop:
for {
// Wait for either WinRM to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for WinRM: %s", err))
return multistep.ActionHalt
}
ui.Say("Connected to WinRM!")
state.Put("communicator", comm)
break WaitLoop
case <-timeout:
err := fmt.Errorf("Timeout waiting for WinRM.")
state.Put("error", err)
ui.Error(err.Error())
close(cancel)
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
// The step sequence was cancelled, so cancel waiting for WinRM
// and just start the halting process.
close(cancel)
log.Println("Interrupt detected, quitting waiting for WinRM.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepConnectWinRM) Cleanup(multistep.StateBag) {
}
func (s *StepConnectWinRM) waitForWinRM(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) {
var comm packer.Communicator
for {
select {
case <-cancel:
log.Println("[INFO] WinRM wait cancelled. Exiting loop.")
return nil, errors.New("WinRM wait cancelled")
case <-time.After(5 * time.Second):
}
host, err := s.Host(state)
if err != nil {
log.Printf("[DEBUG] Error getting WinRM host: %s", err)
continue
}
port := s.Config.WinRMPort
user := s.Config.WinRMUser
password := s.Config.WinRMPassword
if s.WinRMConfig != nil {
config, err := s.WinRMConfig(state)
if err != nil {
log.Printf("[DEBUG] Error getting WinRM config: %s", err)
continue
}
if config.Username != "" {
user = config.Username
}
if config.Password != "" {
password = config.Password
}
}
log.Println("[INFO] Attempting WinRM connection...")
comm, err = winrm.New(&winrm.Config{
Host: host,
Port: port,
Username: user,
Password: password,
Timeout: s.Config.WinRMTimeout,
})
if err != nil {
log.Printf("[ERROR] WinRM connection err: %s", err)
continue
}
break
}
return comm, nil
}
package communicator
import (
"io/ioutil"
"testing"
)
func TestPEM(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Write([]byte(TestPEMContents))
tf.Close()
return tf.Name()
}
const TestPEMContents = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
-----END RSA PRIVATE KEY-----
`
package communicator
// WinRMConfig is configuration that can be returned at runtime to
// dynamically configure WinRM.
type WinRMConfig struct {
Username string
Password string
}
...@@ -42,6 +42,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { ...@@ -42,6 +42,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
if config.InterpolateContext == nil { if config.InterpolateContext == nil {
config.InterpolateContext = ctx config.InterpolateContext = ctx
} else { } else {
config.InterpolateContext.TemplatePath = ctx.TemplatePath
config.InterpolateContext.UserVariables = ctx.UserVariables config.InterpolateContext.UserVariables = ctx.UserVariables
} }
ctx = config.InterpolateContext ctx = config.InterpolateContext
...@@ -66,6 +67,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { ...@@ -66,6 +67,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
DecodeHook: mapstructure.ComposeDecodeHookFunc( DecodeHook: mapstructure.ComposeDecodeHookFunc(
uint8ToStringHook, uint8ToStringHook,
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
mapstructure.StringToTimeDurationHookFunc(),
), ),
}) })
if err != nil { if err != nil {
...@@ -104,6 +106,8 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { ...@@ -104,6 +106,8 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
// detecting things like user variables from the raw configuration params. // detecting things like user variables from the raw configuration params.
func DetectContext(raws ...interface{}) (*interpolate.Context, error) { func DetectContext(raws ...interface{}) (*interpolate.Context, error) {
var s struct { var s struct {
BuildName string `mapstructure:"packer_build_name"`
BuildType string `mapstructure:"packer_builder_type"`
TemplatePath string `mapstructure:"packer_template_path"` TemplatePath string `mapstructure:"packer_template_path"`
Vars map[string]string `mapstructure:"packer_user_variables"` Vars map[string]string `mapstructure:"packer_user_variables"`
} }
...@@ -115,6 +119,8 @@ func DetectContext(raws ...interface{}) (*interpolate.Context, error) { ...@@ -115,6 +119,8 @@ func DetectContext(raws ...interface{}) (*interpolate.Context, error) {
} }
return &interpolate.Context{ return &interpolate.Context{
BuildName: s.BuildName,
BuildType: s.BuildType,
TemplatePath: s.TemplatePath, TemplatePath: s.TemplatePath,
UserVariables: s.Vars, UserVariables: s.Vars,
}, nil }, nil
......
...@@ -3,6 +3,7 @@ package config ...@@ -3,6 +3,7 @@ package config
import ( import (
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
...@@ -11,6 +12,7 @@ func TestDecode(t *testing.T) { ...@@ -11,6 +12,7 @@ func TestDecode(t *testing.T) {
type Target struct { type Target struct {
Name string Name string
Address string Address string
Time time.Duration
} }
cases := map[string]struct { cases := map[string]struct {
...@@ -22,10 +24,12 @@ func TestDecode(t *testing.T) { ...@@ -22,10 +24,12 @@ func TestDecode(t *testing.T) {
[]interface{}{ []interface{}{
map[string]interface{}{ map[string]interface{}{
"name": "bar", "name": "bar",
"time": "5s",
}, },
}, },
&Target{ &Target{
Name: "bar", Name: "bar",
Time: 5 * time.Second,
}, },
nil, nil,
}, },
......
...@@ -217,12 +217,10 @@ func loadConfig() (*config, error) { ...@@ -217,12 +217,10 @@ func loadConfig() (*config, error) {
return nil, err return nil, err
} }
mustExist := true
configFilePath := os.Getenv("PACKER_CONFIG") configFilePath := os.Getenv("PACKER_CONFIG")
if configFilePath == "" { if configFilePath == "" {
var err error var err error
configFilePath, err = configFile() configFilePath, err = configFile()
mustExist = false
if err != nil { if err != nil {
log.Printf("Error detecting default config file path: %s", err) log.Printf("Error detecting default config file path: %s", err)
...@@ -240,11 +238,7 @@ func loadConfig() (*config, error) { ...@@ -240,11 +238,7 @@ func loadConfig() (*config, error) {
return nil, err return nil, err
} }
if mustExist { log.Printf("[WARN] Config file doesn't exist: %s", configFilePath)
return nil, err
}
log.Println("File doesn't exist, but doesn't need to. Ignoring.")
return &config, nil return &config, nil
} }
defer f.Close() defer f.Close()
......
...@@ -142,6 +142,64 @@ func TestCoreBuild_env(t *testing.T) { ...@@ -142,6 +142,64 @@ func TestCoreBuild_env(t *testing.T) {
} }
} }
func TestCoreBuild_buildNameVar(t *testing.T) {
config := TestCoreConfig(t)
testCoreTemplate(t, config, fixtureDir("build-var-build-name.json"))
b := TestBuilder(t, config, "test")
core := TestCore(t, config)
b.ArtifactId = "hello"
build, err := core.Build("test")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := build.Prepare(); err != nil {
t.Fatalf("err: %s", err)
}
// Interpolate the config
var result map[string]interface{}
err = configHelper.Decode(&result, nil, b.PrepareConfig...)
if err != nil {
t.Fatalf("err: %s", err)
}
if result["value"] != "test" {
t.Fatalf("bad: %#v", result)
}
}
func TestCoreBuild_buildTypeVar(t *testing.T) {
config := TestCoreConfig(t)
testCoreTemplate(t, config, fixtureDir("build-var-build-type.json"))
b := TestBuilder(t, config, "test")
core := TestCore(t, config)
b.ArtifactId = "hello"
build, err := core.Build("test")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := build.Prepare(); err != nil {
t.Fatalf("err: %s", err)
}
// Interpolate the config
var result map[string]interface{}
err = configHelper.Decode(&result, nil, b.PrepareConfig...)
if err != nil {
t.Fatalf("err: %s", err)
}
if result["value"] != "test" {
t.Fatalf("bad: %#v", result)
}
}
func TestCoreBuild_nonExist(t *testing.T) { func TestCoreBuild_nonExist(t *testing.T) {
config := TestCoreConfig(t) config := TestCoreConfig(t)
testCoreTemplate(t, config, fixtureDir("build-basic.json")) testCoreTemplate(t, config, fixtureDir("build-basic.json"))
......
{
"builders": [{
"type": "test",
"value": "{{build_name}}"
}]
}
{
"builders": [{
"type": "test",
"value": "{{build_type}}"
}]
}
package atlas package atlas
import ( import (
"path/filepath"
"testing" "testing"
) )
func TestLongestCommonPrefix(t *testing.T) { func TestLongestCommonPrefix(t *testing.T) {
sep := string(filepath.Separator)
cases := []struct { cases := []struct {
Input []string Input []string
Output string Output string
...@@ -18,12 +20,12 @@ func TestLongestCommonPrefix(t *testing.T) { ...@@ -18,12 +20,12 @@ func TestLongestCommonPrefix(t *testing.T) {
"", "",
}, },
{ {
[]string{"foo/", "foo/bar"}, []string{"foo" + sep, "foo" + sep + "bar"},
"foo/", "foo" + sep,
}, },
{ {
[]string{"/foo/", "/bar"}, []string{sep + "foo" + sep, sep + "bar"},
"/", sep,
}, },
} }
......
...@@ -145,6 +145,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { ...@@ -145,6 +145,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
} else { } else {
// Replace single quotes so they parse
vs[1] = strings.Replace(vs[1], "'", `'"'"'`, -1)
// Single quote env var values // Single quote env var values
p.config.Vars[idx] = fmt.Sprintf("%s='%s'", vs[0], vs[1]) p.config.Vars[idx] = fmt.Sprintf("%s='%s'", vs[0], vs[1])
} }
......
...@@ -24,6 +24,8 @@ func init() { ...@@ -24,6 +24,8 @@ func init() {
// Funcs are the interpolation funcs that are available within interpolations. // Funcs are the interpolation funcs that are available within interpolations.
var FuncGens = map[string]FuncGenerator{ var FuncGens = map[string]FuncGenerator{
"build_name": funcGenBuildName,
"build_type": funcGenBuildType,
"env": funcGenEnv, "env": funcGenEnv,
"isotime": funcGenIsotime, "isotime": funcGenIsotime,
"pwd": funcGenPwd, "pwd": funcGenPwd,
...@@ -56,6 +58,26 @@ func Funcs(ctx *Context) template.FuncMap { ...@@ -56,6 +58,26 @@ func Funcs(ctx *Context) template.FuncMap {
return template.FuncMap(result) return template.FuncMap(result)
} }
func funcGenBuildName(ctx *Context) interface{} {
return func() (string, error) {
if ctx == nil || ctx.BuildName == "" {
return "", errors.New("build_name not available")
}
return ctx.BuildName, nil
}
}
func funcGenBuildType(ctx *Context) interface{} {
return func() (string, error) {
if ctx == nil || ctx.BuildType == "" {
return "", errors.New("build_name not available")
}
return ctx.BuildType, nil
}
}
func funcGenEnv(ctx *Context) interface{} { func funcGenEnv(ctx *Context) interface{} {
return func(k string) (string, error) { return func(k string) (string, error) {
if !ctx.EnableEnv { if !ctx.EnableEnv {
......
...@@ -8,6 +8,56 @@ import ( ...@@ -8,6 +8,56 @@ import (
"time" "time"
) )
func TestFuncBuildName(t *testing.T) {
cases := []struct {
Input string
Output string
}{
{
`{{build_name}}`,
"foo",
},
}
ctx := &Context{BuildName: "foo"}
for _, tc := range cases {
i := &I{Value: tc.Input}
result, err := i.Render(ctx)
if err != nil {
t.Fatalf("Input: %s\n\nerr: %s", tc.Input, err)
}
if result != tc.Output {
t.Fatalf("Input: %s\n\nGot: %s", tc.Input, result)
}
}
}
func TestFuncBuildType(t *testing.T) {
cases := []struct {
Input string
Output string
}{
{
`{{build_type}}`,
"foo",
},
}
ctx := &Context{BuildType: "foo"}
for _, tc := range cases {
i := &I{Value: tc.Input}
result, err := i.Render(ctx)
if err != nil {
t.Fatalf("Input: %s\n\nerr: %s", tc.Input, err)
}
if result != tc.Output {
t.Fatalf("Input: %s\n\nGot: %s", tc.Input, result)
}
}
}
func TestFuncEnv(t *testing.T) { func TestFuncEnv(t *testing.T) {
cases := []struct { cases := []struct {
Input string Input string
......
...@@ -14,16 +14,23 @@ type Context struct { ...@@ -14,16 +14,23 @@ type Context struct {
// Funcs are extra functions available in the template // Funcs are extra functions available in the template
Funcs map[string]interface{} Funcs map[string]interface{}
// TemplatePath is the path to the template that this is being
// rendered within.
TemplatePath string
// UserVariables is the mapping of user variables that the // UserVariables is the mapping of user variables that the
// "user" function reads from. // "user" function reads from.
UserVariables map[string]string UserVariables map[string]string
// EnableEnv enables the env function // EnableEnv enables the env function
EnableEnv bool EnableEnv bool
// All the fields below are used for built-in functions.
//
// BuildName and BuildType are the name and type, respectively,
// of the builder being used.
//
// TemplatePath is the path to the template that this is being
// rendered within.
BuildName string
BuildType string
TemplatePath string
} }
// Render is shorthand for constructing an I and calling Render. // Render is shorthand for constructing an I and calling Render.
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"sort" "sort"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
...@@ -291,11 +292,16 @@ func Parse(r io.Reader) (*Template, error) { ...@@ -291,11 +292,16 @@ func Parse(r io.Reader) (*Template, error) {
if len(md.Unused) > 0 { if len(md.Unused) > 0 {
sort.Strings(md.Unused) sort.Strings(md.Unused)
for _, unused := range md.Unused { for _, unused := range md.Unused {
// Ignore keys starting with '_' as comments
if unused[0] == '_' {
continue
}
err = multierror.Append(err, fmt.Errorf( err = multierror.Append(err, fmt.Errorf(
"Unknown root level key in template: '%s'", unused)) "Unknown root level key in template: '%s'", unused))
} }
}
// Return early for these errors if err != nil {
return nil, err return nil, err
} }
...@@ -317,6 +323,13 @@ func ParseFile(path string) (*Template, error) { ...@@ -317,6 +323,13 @@ func ParseFile(path string) (*Template, error) {
return nil, err return nil, err
} }
if !filepath.IsAbs(path) {
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
}
tpl.Path = path tpl.Path = path
return tpl, nil return tpl, nil
} }
package template package template
import ( import (
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
...@@ -303,10 +304,23 @@ func TestParse(t *testing.T) { ...@@ -303,10 +304,23 @@ func TestParse(t *testing.T) {
}, },
false, false,
}, },
{
"parse-comment.json",
&Template{
Builders: map[string]*Builder{
"something": &Builder{
Name: "something",
Type: "something",
},
},
},
false,
},
} }
for _, tc := range cases { for _, tc := range cases {
path := fixtureDir(tc.File) path, _ := filepath.Abs(fixtureDir(tc.File))
tpl, err := ParseFile(fixtureDir(tc.File)) tpl, err := ParseFile(fixtureDir(tc.File))
if (err != nil) != tc.Err { if (err != nil) != tc.Err {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
......
{
"_info": "foo",
"builders": [{"type": "something"}]
}
#!/usr/bin/env bats
#
# This tests the amazon-ebs builder. The teardown function will automatically
# delete any AMIs with a tag of `packer-test` being equal to "true" so
# be sure any test cases set this.
load test_helper
fixtures amazon-ebs
# This counts how many AMIs were copied to another region
aws_ami_region_copy_count() {
aws ec2 describe-images --region $1 --owners self --output text \
--filters 'Name=tag:packer-id,Values=ami_region_copy' \
--query "Images[*].ImageId" \
| wc -l
}
teardown() {
aws_ami_cleanup 'us-east-1'
aws_ami_cleanup 'us-west-1'
aws_ami_cleanup 'us-west-2'
}
@test "amazon-ebs: build minimal.json" {
run packer build $FIXTURE_ROOT/minimal.json
[ "$status" -eq 0 ]
}
# @unit-testable
@test "amazon-ebs: AMI region copy" {
run packer build $FIXTURE_ROOT/ami_region_copy.json
[ "$status" -eq 0 ]
[ "$(aws_ami_region_copy_count 'us-east-1')" -eq "1" ]
[ "$(aws_ami_region_copy_count 'us-west-1')" -eq "1" ]
[ "$(aws_ami_region_copy_count 'us-west-2')" -eq "1" ]
}
...@@ -124,6 +124,9 @@ each category, the available configuration keys are alphabetized. ...@@ -124,6 +124,9 @@ each category, the available configuration keys are alphabetized.
* `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on * `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on
HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy. HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy.
* `force_deregister` (boolean) – Force Packer to first deregister an existing
AMI if one with the same name already exists. Default `false`.
* `mount_path` (string) - The path where the volume will be mounted. This is * `mount_path` (string) - The path where the volume will be mounted. This is
where the chroot environment will be. This defaults to where the chroot environment will be. This defaults to
`packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration `packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration
......
...@@ -96,6 +96,9 @@ each category, the available configuration keys are alphabetized. ...@@ -96,6 +96,9 @@ each category, the available configuration keys are alphabetized.
* `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on * `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on
HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy. HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy.
* `force_deregister` (boolean) – Force Packer to first deregister an existing
AMI if one with the same name already exists. Default `false`.
* `iam_instance_profile` (string) - The name of an * `iam_instance_profile` (string) - The name of an
[IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html) [IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html)
to launch the EC2 instance with. to launch the EC2 instance with.
...@@ -147,7 +150,8 @@ each category, the available configuration keys are alphabetized. ...@@ -147,7 +150,8 @@ each category, the available configuration keys are alphabetized.
"subnet-12345def", where Packer will launch the EC2 instance. This field is "subnet-12345def", where Packer will launch the EC2 instance. This field is
required if you are using an non-default VPC. required if you are using an non-default VPC.
* `tags` (object of key/value strings) - Tags applied to the AMI. * `tags` (object of key/value strings) - Tags applied to the AMI and
relevant snapshots.
* `temporary_key_pair_name` (string) - The name of the temporary keypair * `temporary_key_pair_name` (string) - The name of the temporary keypair
to generate. By default, Packer generates a name with a UUID. to generate. By default, Packer generates a name with a UUID.
...@@ -168,6 +172,10 @@ each category, the available configuration keys are alphabetized. ...@@ -168,6 +172,10 @@ each category, the available configuration keys are alphabetized.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the * `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. VPC ID in order to create a temporary security group within the VPC.
* `windows_password_timeout` (string) - The timeout for waiting for
a Windows password for Windows instances. Defaults to 20 minutes.
Example value: "10m"
## Basic Example ## Basic Example
Here is a basic example. It is completely valid except for the access keys: Here is a basic example. It is completely valid except for the access keys:
......
...@@ -136,6 +136,9 @@ each category, the available configuration keys are alphabetized. ...@@ -136,6 +136,9 @@ each category, the available configuration keys are alphabetized.
* `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on * `enhanced_networking` (boolean) - Enable enhanced networking (SriovNetSupport) on
HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy. HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your AWS IAM policy.
* `force_deregister` (boolean) – Force Packer to first deregister an existing
AMI if one with the same name already exists. Default `false`.
* `iam_instance_profile` (string) - The name of an * `iam_instance_profile` (string) - The name of an
[IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html) [IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html)
to launch the EC2 instance with. to launch the EC2 instance with.
...@@ -209,6 +212,10 @@ each category, the available configuration keys are alphabetized. ...@@ -209,6 +212,10 @@ each category, the available configuration keys are alphabetized.
it is perfectly okay to create this directory as part of the provisioning it is perfectly okay to create this directory as part of the provisioning
process. process.
* `windows_password_timeout` (string) - The timeout for waiting for
a Windows password for Windows instances. Defaults to 20 minutes.
Example value: "10m"
## Basic Example ## Basic Example
Here is a basic example. It is completely valid except for the access keys: Here is a basic example. It is completely valid except for the access keys:
......
...@@ -27,7 +27,7 @@ the settings here. ...@@ -27,7 +27,7 @@ the settings here.
```javascript ```javascript
{ {
"type": "parallels-pvm", "type": "parallels-pvm",
"parallels_tools_flavor": "lin" "parallels_tools_flavor": "lin",
"source_path": "source.pvm", "source_path": "source.pvm",
"ssh_username": "packer", "ssh_username": "packer",
"ssh_password": "packer", "ssh_password": "packer",
......
...@@ -146,6 +146,33 @@ on reboot or in your shell script. For example, on Gentoo: ...@@ -146,6 +146,33 @@ on reboot or in your shell script. For example, on Gentoo:
/etc/init.d/net.eth0 stop /etc/init.d/net.eth0 stop
``` ```
## SSH Agent Forwarding
Some provisioning requires connecting to remote SSH servers from within the
packer instance. The below example is for pulling code from a private git
repository utilizing openssh on the client. Make sure you are running
`ssh-agent` and add your git repo ssh keys into it using `ssh-add /path/to/key`.
When the packer instance needs access to the ssh keys the agent will forward
the request back to your `ssh-agent`.
Note: when provisioning via git you should add the git server keys into
the `~/.ssh/known_hosts` file otherwise the git command could hang awaiting
input. This can be done by copying the file in via the
[file provisioner](/docs/provisioners/file.html) (more secure)
or using `ssh-keyscan` to populate the file (less secure). An example of the
latter accessing github would be:
```
{
"type": "shell",
"inline": [
"sudo apt-get install -y git",
"ssh-keyscan github.com >> ~/.ssh/known_hosts",
"git clone git@github.com:exampleorg/myprivaterepo.git"
]
}
```
## Troubleshooting ## Troubleshooting
*My shell script doesn't work correctly on Ubuntu* *My shell script doesn't work correctly on Ubuntu*
......
...@@ -55,6 +55,8 @@ While some configuration settings have local variables specific to only that ...@@ -55,6 +55,8 @@ While some configuration settings have local variables specific to only that
configuration, a set of functions are available globally for use in _any string_ configuration, a set of functions are available globally for use in _any string_
in Packer templates. These are listed below for reference. in Packer templates. These are listed below for reference.
* `build_name` - The name of the build being run.
* `build_type` - The type of the builder being used currently.
* `isotime [FORMAT]` - UTC time, which can be [formatted](http://golang.org/pkg/time/#example_Time_Format). * `isotime [FORMAT]` - UTC time, which can be [formatted](http://golang.org/pkg/time/#example_Time_Format).
See more examples below. See more examples below.
* `lower` - Lowercases the string. * `lower` - Lowercases the string.
......
...@@ -58,6 +58,22 @@ Along with each key, it is noted whether it is required or not. ...@@ -58,6 +58,22 @@ Along with each key, it is noted whether it is required or not.
For more information on how to define and use user variables, read the For more information on how to define and use user variables, read the
sub-section on [user variables in templates](/docs/templates/user-variables.html). sub-section on [user variables in templates](/docs/templates/user-variables.html).
## Comments
JSON doesn't support comments and Packer reports unknown keys as validation
errors. If you'd like to comment your template, you can prefix a _root level_
key with an underscore. Example:
```javascript
{
"_comment": "This is a comment",
"builders": [{}]
}
```
**Important:** Only _root level_ keys can be underscore prefixed. Keys within
builders, provisioners, etc. will still result in validation errors.
## Example Template ## Example Template
Below is an example of a basic template that is nearly fully functional. It is just Below is an example of a basic template that is nearly fully functional. It is just
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment