Merge pull request #2235 from mitchellh/b-refactor-ssh

Communicator refactor, shared code for communicator connect
This commit is contained in:
Mitchell Hashimoto 2015-06-13 19:38:21 -04:00
commit a832e1264b
59 changed files with 671 additions and 742 deletions

View File

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
@ -21,11 +21,6 @@ type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"`
SpotPrice string `mapstructure:"spot_price"`
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"`
SecurityGroupIds []string `mapstructure:"security_group_ids"`
SubnetId string `mapstructure:"subnet_id"`
@ -34,27 +29,19 @@ type RunConfig struct {
UserDataFile string `mapstructure:"user_data_file"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
// Communicator settings
Comm communicator.Config `mapstructure:",squash"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
}
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 == "" {
c.TemporaryKeyPairName = fmt.Sprintf(
"packer %s", uuid.TimeOrderedUUID())
}
// Validation
var errs []error
errs := c.Comm.Prepare(ctx)
if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
@ -70,10 +57,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 != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {
@ -91,15 +74,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
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}

View File

@ -4,6 +4,8 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func init() {
@ -19,7 +21,10 @@ func testConfig() *RunConfig {
return &RunConfig{
SourceAmi: "abcd",
InstanceType: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) {
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)
if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
c.SSHPort = 44
c.Comm.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
if c.Comm.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}

View File

@ -10,9 +10,9 @@ import (
"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.
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) {
for j := 0; j < 2; j++ {
var host string
@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st
}
if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil
return host, nil
}
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{

View File

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -89,11 +90,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
},
&awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
SSHPort: b.config.RunConfig.Comm.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
@ -112,11 +113,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
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{},
&stepStopInstance{SpotPrice: b.config.SpotPrice},

View File

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -175,11 +176,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
},
&awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
SSHPort: b.config.RunConfig.Comm.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
@ -197,11 +198,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
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{},
&StepUploadX509Cert{},

View File

@ -6,11 +6,11 @@ package digitalocean
import (
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"golang.org/x/oauth2"
)
@ -53,10 +53,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
new(stepCreateDroplet),
new(stepDropletInfo),
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
},
new(common.StepProvision),
new(stepShutdown),

View File

@ -3,6 +3,7 @@ package digitalocean
import (
"strconv"
"testing"
"time"
"github.com/mitchellh/packer/packer"
)
@ -163,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.SSHUsername)
if b.config.Comm.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
}
// Test set
@ -178,52 +179,11 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername)
if b.config.Comm.SSHUsername != "foo" {
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) {
var b Builder
config := testConfig()
@ -237,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawStateTimeout != "6m" {
t.Errorf("invalid: %s", b.config.RawStateTimeout)
if b.config.StateTimeout != 6*time.Minute {
t.Errorf("invalid: %s", b.config.StateTimeout)
}
// Test set

View File

@ -9,6 +9,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -16,6 +17,7 @@ import (
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
APIToken string `mapstructure:"api_token"`
@ -23,20 +25,11 @@ type Config struct {
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
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
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
StateTimeout time.Duration `mapstructure:"state_timeout"`
DropletName string `mapstructure:"droplet_name"`
UserData string `mapstructure:"user_data"`
ctx *interpolate.Context
}
@ -79,29 +72,22 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.SSHUsername == "" {
if c.Comm.SSHUsername == "" {
// Default to "root". You can override this if your
// 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 == "" {
// Default to 1 minute timeouts
c.RawSSHTimeout = "1m"
}
if c.RawStateTimeout == "" {
if c.StateTimeout == 0 {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
c.RawStateTimeout = "6m"
c.StateTimeout = 6 * time.Minute
}
var errs *packer.MultiError
if es := c.Comm.Prepare(c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.APIToken == "" {
// Required configurations that will display errors if not set
errs = packer.MultiErrorAppend(
@ -123,20 +109,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
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 {
return nil, nil, errs
}

View File

@ -2,14 +2,14 @@ package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
)
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(Config)
func commHost(state multistep.StateBag) (string, error) {
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) {
@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
}
return &ssh.ClientConfig{
User: config.SSHUsername,
User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},

View File

@ -18,7 +18,7 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
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 {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err)

View File

@ -42,7 +42,7 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
}
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 {
state.Put("error", err)
ui.Error(err.Error())

View File

@ -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
ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout)
err = waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state.Put("error", err)

View File

@ -8,6 +8,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepInstanceInfo{
Debug: b.config.PackerDebug,
},
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshTimeout,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
},
new(common.StepProvision),
new(StepTeardownInstance),

View File

@ -7,6 +7,7 @@ import (
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -17,6 +18,7 @@ import (
// state of the config object.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"`
@ -31,16 +33,12 @@ type Config struct {
Network string `mapstructure:"network"`
SourceImage string `mapstructure:"source_image"`
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"`
Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"`
account accountFile
privateKeyBytes []byte
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.MachineType = "n1-standard-1"
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.RawStateTimeout == "" {
c.RawStateTimeout = "5m"
}
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "root"
}
var errs *packer.MultiError
@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
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)
if err != nil {
errs = packer.MultiErrorAppend(

View File

@ -2,15 +2,14 @@ package googlecompute
import (
"fmt"
"github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh"
)
// sshAddress returns the ssh address.
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config)
func commHost(state multistep.StateBag) (string, error) {
ipAddress := state.Get("instance_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
return ipAddress, nil
}
// sshConfig returns the ssh configuration.
@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
}
return &ssh.ClientConfig{
User: config.SSHUsername,
User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},

View File

@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
// Merge any existing ssh keys with our public key
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 {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
}

View File

@ -1,11 +1,12 @@
package null
import (
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
const BuilderId = "fnoeding.null"
@ -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) {
steps := []multistep.Step{
&common.StepConnectSSH{
SSHAddress: SSHAddress(b.config.Host, b.config.Port),
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile),
SSHWaitTimeout: 1 * time.Minute,
&communicator.StepConnect{
Config: &b.config.CommConfig,
Host: CommHost(b.config.CommConfig.SSHHost),
SSHConfig: SSHConfig(
b.config.CommConfig.SSHUsername,
b.config.CommConfig.SSHPassword,
b.config.CommConfig.SSHPrivateKey),
},
&common.StepProvision{},
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -12,49 +13,40 @@ import (
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
CommConfig communicator.Config `mapstructure:",squash"`
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{},
}, raws...)
if err != nil {
return nil, nil, err
}
if c.Port == 0 {
c.Port = 22
}
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,
fmt.Errorf("host must be specified"))
}
if c.SSHUsername == "" {
if c.CommConfig.SSHUsername == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("ssh_username must be specified"))
}
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" {
if c.CommConfig.SSHPassword == "" && c.CommConfig.SSHPrivateKey == "" {
errs = packer.MultiErrorAppend(errs,
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,
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified"))
}

View File

@ -1,12 +1,15 @@
package null
import (
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"host": "foo",
"ssh_host": "foo",
"ssh_username": "bar",
"ssh_password": "baz",
}
@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) {
// default port should be 22
delete(raw, "port")
c, warns, errs := NewConfig(raw)
if c.Port != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.Port)
if c.CommConfig.SSHPort != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.CommConfig.SSHPort)
}
testConfigOk(t, warns, errs)
}
@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) {
raw := testConfig()
// No host
delete(raw, "host")
delete(raw, "ssh_host")
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// Good host
raw["host"] = "good"
raw["ssh_host"] = "good"
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
}
@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) {
testConfigOk(t, warns, errs)
// 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")
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)

View File

@ -8,11 +8,9 @@ import (
"io/ioutil"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
func CommHost(host string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
return fmt.Sprintf("%s:%d", host, port), nil
return host, nil
}
}

View File

@ -5,10 +5,11 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"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/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -19,9 +20,10 @@ const BuilderId = "mitchellh.openstack"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"`
ctx interpolate.Context
}
@ -88,10 +90,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
FloatingIpPool: b.config.FloatingIpPool,
FloatingIp: b.config.FloatingIp,
},
&common.StepConnectSSH{
SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, b.config.SSHPort),
SSHConfig: SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
&communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: CommHost(
computeClient,
b.config.SSHInterface),
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername),
},
&common.StepProvision{},
&stepCreateImage{},

View File

@ -2,21 +2,19 @@ package openstack
import (
"errors"
"fmt"
"time"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
// RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image.
type RunConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHInterface string `mapstructure:"ssh_interface"`
SourceImage string `mapstructure:"source_image"`
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"`
RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIpPool string `mapstructure:"floating_ip_pool"`
@ -27,23 +25,12 @@ type RunConfig struct {
// Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
}
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "root"
}
if c.UseFloatingIp && c.FloatingIpPool == "" {
@ -51,8 +38,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
// Validation
var err error
errs := make([]error, 0)
errs := c.Comm.Prepare(ctx)
if c.SourceImage == "" {
errs = append(errs, errors.New("A source_image must be specified"))
}
@ -61,18 +47,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
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
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}

View File

@ -3,6 +3,8 @@ package openstack
import (
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func init() {
@ -17,7 +19,10 @@ func testRunConfig() *RunConfig {
return &RunConfig{
SourceImage: "abcd",
Flavor: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) {
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)
if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
c.SSHPort = 44
c.Comm.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testRunConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
if c.Comm.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testRunConfig()
c.SSHUsername = ""
c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}

View File

@ -13,22 +13,21 @@ import (
"golang.org/x/crypto/ssh"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the server AccessIPv4 setting..
func SSHAddress(
// CommHost looks up the host for the communicator.
func CommHost(
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) {
s := state.Get("server").(*servers.Server)
// If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingip.FloatingIP)
if ip != nil && ip.IP != "" {
return fmt.Sprintf("%s:%d", ip.IP, port), nil
return ip.IP, nil
}
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
@ -53,7 +52,7 @@ func SSHAddress(
}
}
if addr != "" {
return fmt.Sprintf("%s:%d", addr, port), nil
return addr, nil
}
}
}

View File

@ -1,15 +1,13 @@
package common
import (
"fmt"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
packerssh "github.com/mitchellh/packer/communicator/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)
driver := state.Get("driver").(Driver)
@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) {
return "", err
}
return fmt.Sprintf("%s:22", ip), nil
return ip, nil
}
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
auth := []ssh.AuthMethod{
ssh.Password(config.SSHPassword),
ssh.Password(config.Comm.SSHPassword),
ssh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.SSHPassword)),
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
}
return &ssh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -1,52 +1,29 @@
package common
import (
"errors"
"fmt"
"os"
"time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
type SSHConfig struct {
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"`
Comm communicator.Config `mapstructure:",squash"`
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 {
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
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))
}
c.Comm.SSHPrivateKey = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
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)
}

View File

@ -4,11 +4,15 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testSSHConfig() *SSHConfig {
return &SSHConfig{
SSHUser: "foo",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Fatalf("err: %#v", errs)
}
if c.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
}
}
@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error
c = testSSHConfig()
c.SSHUser = ""
c.Comm.SSHUsername = ""
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"
c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)

View File

@ -9,6 +9,7 @@ import (
"github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: parallelscommon.SSHAddress,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,

View File

@ -3,11 +3,13 @@ package pvm
import (
"errors"
"fmt"
"log"
"github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"log"
)
// 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
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: parallelscommon.SSHAddress,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,

View File

@ -12,7 +12,7 @@ import (
"github.com/mitchellh/multistep"
"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/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -78,6 +78,7 @@ type Builder struct {
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Accelerator string `mapstructure:"accelerator"`
BootCommand []string `mapstructure:"boot_command"`
@ -103,25 +104,24 @@ type Config struct {
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
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"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
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
RunOnce bool `mapstructure:"run_once"`
RawBootWait string `mapstructure:"boot_wait"`
RawSingleISOUrl string `mapstructure:"iso_url"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
ctx interpolate.Context
}
@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
return nil, err
}
var errs *packer.MultiError
warnings := make([]string, 0)
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
}
@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.SSHHostPortMax = 4444
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.VNCPortMin == 0 {
b.config.VNCPortMin = 5900
}
@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
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") {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.RawShutdownTimeout = "5m"
}
if b.config.RawSSHWaitTimeout == "" {
b.config.RawSSHWaitTimeout = "20m"
}
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
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 {
errs = packer.MultiErrorAppend(
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 {
errs = packer.MultiErrorAppend(
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
steprun,
&stepBootWait{},
&stepTypeBootCommand{},
&common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
SSHPort: commPort,
},
new(common.StepProvision),
new(stepShutdown),

View File

@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
}
if b.config.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.SSHPort)
if b.config.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.Comm.SSHPort)
}
if b.config.VMName != "packer-foo" {
@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
t.Fatalf("err: %s", err)
}
if b.config.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
}
// Test with a bad value
config["ssh_wait_timeout"] = "this is not good"
b = Builder{}

View File

@ -1,30 +1,32 @@
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/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)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
return int(sshHostPort), nil
}
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(*Config)
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
if config.Comm.SSHPrivateKey != "" {
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
}
return &gossh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -1,29 +1,31 @@
package common
import (
"fmt"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/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)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
return int(sshHostPort), nil
}
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
}
return &gossh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -2,25 +2,23 @@ package common
import (
"errors"
"fmt"
"os"
"time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
type SSHConfig struct {
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
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"`
Comm communicator.Config `mapstructure:",squash"`
SSHWaitTimeout time.Duration
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
// 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 {
@ -32,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
c.SSHHostPortMax = 4444
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
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))
}
c.Comm.SSHPrivateKey = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
errs := c.Comm.Prepare(ctx)
if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs,
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
}

View File

@ -4,11 +4,15 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testSSHConfig() *SSHConfig {
return &SSHConfig{
SSHUser: "foo",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
}
if c.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
}
}
@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error
c = testSSHConfig()
c.SSHUser = ""
c.Comm.SSHUsername = ""
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"
c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)

View File

@ -11,6 +11,7 @@ import (
"github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -253,7 +254,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort,
GuestPort: uint(b.config.SSHConfig.Comm.SSHPort),
HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
@ -271,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHPort: vboxcommon.SSHPort,
},
&vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile,

View File

@ -10,6 +10,7 @@ import (
"github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
@ -82,7 +83,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort,
GuestPort: uint(b.config.SSHConfig.Comm.SSHPort),
HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
@ -100,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHPort: vboxcommon.SSHPort,
},
&vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile,

View File

@ -29,9 +29,9 @@ type Driver interface {
// Checks if the VMX file at the given path is running.
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.
SSHAddress(multistep.StateBag) (string, error)
CommHost(multistep.StateBag) (string, error)
// Start starts a VM specified by the path to the VMX given.
Start(string, bool) error

View File

@ -69,8 +69,8 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil
}
func (d *Fusion5Driver) SSHAddress(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state)
func (d *Fusion5Driver) CommHost(state multistep.StateBag) (string, error) {
return CommHost(d.SSHConfig)(state)
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {

View File

@ -29,10 +29,10 @@ type DriverMock struct {
IsRunningResult bool
IsRunningErr error
SSHAddressCalled bool
SSHAddressState multistep.StateBag
SSHAddressResult string
SSHAddressErr error
CommHostCalled bool
CommHostState multistep.StateBag
CommHostResult string
CommHostErr error
StartCalled bool
StartPath string
@ -92,10 +92,10 @@ func (d *DriverMock) IsRunning(path string) (bool, error) {
return d.IsRunningResult, d.IsRunningErr
}
func (d *DriverMock) SSHAddress(state multistep.StateBag) (string, error) {
d.SSHAddressCalled = true
d.SSHAddressState = state
return d.SSHAddressResult, d.SSHAddressErr
func (d *DriverMock) CommHost(state multistep.StateBag) (string, error) {
d.CommHostCalled = true
d.CommHostState = state
return d.CommHostResult, d.CommHostErr
}
func (d *DriverMock) Start(path string, headless bool) error {

View File

@ -97,8 +97,8 @@ func (d *Player5Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil
}
func (d *Player5Driver) SSHAddress(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state)
func (d *Player5Driver) CommHost(state multistep.StateBag) (string, error) {
return CommHost(d.SSHConfig)(state)
}
func (d *Player5Driver) Start(vmxPath string, headless bool) error {

View File

@ -70,8 +70,8 @@ func (d *Workstation9Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil
}
func (d *Workstation9Driver) SSHAddress(state multistep.StateBag) (string, error) {
return SSHAddressFunc(d.SSHConfig)(state)
func (d *Workstation9Driver) CommHost(state multistep.StateBag) (string, error) {
return CommHost(d.SSHConfig)(state)
}
func (d *Workstation9Driver) Start(vmxPath string, headless bool) error {

View File

@ -13,13 +13,13 @@ import (
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) {
driver := state.Get("driver").(Driver)
vmxPath := state.Get("vmx_path").(string)
if config.SSHHost != "" {
return fmt.Sprintf("%s:%d", config.SSHHost, config.SSHPort), nil
if config.Comm.SSHHost != "" {
return config.Comm.SSHHost, nil
}
log.Println("Lookup up IP information...")
@ -62,20 +62,20 @@ func SSHAddressFunc(config *SSHConfig) func(multistep.StateBag) (string, error)
}
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) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
if config.Comm.SSHPrivateKey != "" {
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -84,7 +84,7 @@ func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientCon
}
return &gossh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -1,63 +1,33 @@
package common
import (
"errors"
"fmt"
"net"
"os"
"time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
type SSHConfig struct {
SSHUser string `mapstructure:"ssh_username"`
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"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
Comm communicator.Config `mapstructure:",squash"`
SSHWaitTimeout time.Duration
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHSkipRequestPty bool `mapstructure:"ssh_skip_request_pty"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
}
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
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))
}
c.Comm.SSHPrivateKey = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
if c.SSHSkipRequestPty {
c.Comm.SSHPty = false
}
if c.SSHHost != "" {
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)
}

View File

@ -4,11 +4,15 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testSSHConfig() *SSHConfig {
return &SSHConfig{
SSHUser: "foo",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Fatalf("err: %#v", errs)
}
if c.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
}
}
@ -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 = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu

View File

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep"
vmwcommon "github.com/mitchellh/packer/builder/vmware/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -298,11 +299,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: driver.SSHAddress,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
Pty: !b.config.SSHSkipRequestPty,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
},
&vmwcommon.StepUploadTools{
RemoteType: b.config.RemoteType,

View File

@ -1,12 +1,12 @@
package iso
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"reflect"
"testing"
"time"
"github.com/mitchellh/packer/packer"
)
func testConfig() map[string]interface{} {
@ -138,10 +138,6 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
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" {
t.Errorf("bad vm name: %s", b.config.VMName)
}

View File

@ -218,7 +218,7 @@ func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) {
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)
if address, ok := state.GetOk("vm_address"); ok {
@ -253,7 +253,7 @@ func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) {
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)
return address, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/mitchellh/multistep"
vmwcommon "github.com/mitchellh/packer/builder/vmware/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
@ -90,11 +91,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: driver.SSHAddress,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
Pty: !b.config.SSHSkipRequestPty,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: driver.CommHost,
SSHConfig: vmwcommon.SSHConfigFunc(&b.config.SSHConfig),
},
&vmwcommon.StepUploadTools{
RemoteType: b.config.RemoteType,

View File

@ -1,14 +0,0 @@
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")
}
}

View File

@ -0,0 +1,57 @@
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"`
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"`
}
func (c *Config) Prepare(ctx *interpolate.Context) []error {
if c.Type == "" {
c.Type = "ssh"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.SSHTimeout == 0 {
c.SSHTimeout = 5 * time.Minute
}
// Validation
var errs []error
if c.Type == "ssh" {
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
}

View File

@ -0,0 +1,35 @@
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
}

View File

@ -0,0 +1,44 @@
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
}

View File

@ -0,0 +1,62 @@
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)
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,
},
}
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)
}
}

View File

@ -1,4 +1,4 @@
package common
package communicator
import (
"errors"
@ -13,32 +13,15 @@ import (
gossh "golang.org/x/crypto/ssh"
)
// StepConnectSSH is a multistep Step implementation that waits for SSH
// to become available. It gets the connection information from a single
// configuration when creating the step.
// StepConnectSSH is a step that only connects to SSH.
//
// Uses:
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
// In general, you should use StepConnect.
type StepConnectSSH struct {
// SSHAddress is a function that returns the TCP address to connect to
// for SSH. This is a function so that you can query information
// if necessary for this address.
SSHAddress func(multistep.StateBag) (string, error)
// SSHConfig is a function that returns the proper client configuration
// for SSH access.
// All the fields below are documented on StepConnect
Config *Config
Host func(multistep.StateBag) (string, error)
SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error)
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
// Pty, if true, will request a Pty from the remote end.
Pty bool
comm packer.Communicator
SSHPort func(multistep.StateBag) (int, error)
}
func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction {
@ -55,8 +38,8 @@ func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction {
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout)
timeout := time.After(s.SSHWaitTimeout)
log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout)
timeout := time.After(s.Config.SSHTimeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
@ -70,7 +53,6 @@ WaitLoop:
}
ui.Say("Connected to SSH!")
s.comm = comm
state.Put("communicator", comm)
break WaitLoop
case <-timeout:
@ -84,7 +66,7 @@ WaitLoop:
// The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process.
close(cancel)
log.Println("Interrupt detected, quitting waiting for SSH.")
log.Println("[WARN] Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
@ -106,7 +88,7 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
if !first {
select {
case <-cancel:
log.Println("SSH wait cancelled. Exiting loop.")
log.Println("[DEBUG] SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
case <-time.After(5 * time.Second):
}
@ -114,24 +96,34 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
first = false
// First we request the TCP connection information
address, err := s.SSHAddress(state)
host, err := s.Host(state)
if err != nil {
log.Printf("Error getting SSH address: %s", err)
log.Printf("[DEBUG] Error getting SSH address: %s", err)
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
sshConfig, err := s.SSHConfig(state)
if err != nil {
log.Printf("Error getting SSH config: %s", err)
log.Printf("[DEBUG] Error getting SSH config: %s", err)
continue
}
address := fmt.Sprintf("%s:%d", host, port)
// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address)
nc, err := connFunc()
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
}
nc.Close()
@ -140,19 +132,20 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
config := &ssh.Config{
Connection: connFunc,
SSHConfig: sshConfig,
Pty: s.Pty,
Pty: s.Config.SSHPty,
}
log.Println("Attempting SSH connection...")
log.Println("[INFO] Attempting SSH connection...")
comm, err = ssh.New(address, config)
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
// to authenticate. Note this is very brittle since it depends
// on the string of the error... but I don't see any other way.
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
}

View File

@ -0,0 +1,39 @@
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
}

View File

@ -0,0 +1,47 @@
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-----
`

View File

@ -66,6 +66,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
DecodeHook: mapstructure.ComposeDecodeHookFunc(
uint8ToStringHook,
mapstructure.StringToSliceHookFunc(","),
mapstructure.StringToTimeDurationHookFunc(),
),
})
if err != nil {

View File

@ -3,6 +3,7 @@ package config
import (
"reflect"
"testing"
"time"
"github.com/mitchellh/packer/template/interpolate"
)
@ -11,6 +12,7 @@ func TestDecode(t *testing.T) {
type Target struct {
Name string
Address string
Time time.Duration
}
cases := map[string]struct {
@ -22,10 +24,12 @@ func TestDecode(t *testing.T) {
[]interface{}{
map[string]interface{}{
"name": "bar",
"time": "5s",
},
},
&Target{
Name: "bar",
Time: 5 * time.Second,
},
nil,
},