Merge pull request #4655 from lbordowitz/ostk-sshca

OpenStack: Add ssh agent support
This commit is contained in:
Rickard von Essen 2017-03-14 21:07:03 +01:00 committed by GitHub
commit 8c2a8f5f81
5 changed files with 103 additions and 32 deletions

View File

@ -75,10 +75,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Flavor: b.config.Flavor, Flavor: b.config.Flavor,
}, },
&StepKeyPair{ &StepKeyPair{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.SSHKeyPairName, KeyPairName: b.config.SSHKeyPairName,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey, TemporaryKeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
SSHAgentAuth: b.config.RunConfig.Comm.SSHAgentAuth,
}, },
&StepRunSourceServer{ &StepRunSourceServer{
Name: b.config.ImageName, Name: b.config.ImageName,
@ -110,7 +112,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
computeClient, computeClient,
b.config.SSHInterface, b.config.SSHInterface,
b.config.SSHIPVersion), b.config.SSHIPVersion),
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername, SSHConfig: SSHConfig(
b.config.RunConfig.Comm.SSHAgentAuth,
b.config.RunConfig.Comm.SSHUsername,
b.config.RunConfig.Comm.SSHPassword), b.config.RunConfig.Comm.SSHPassword),
}, },
&common.StepProvision{}, &common.StepProvision{},

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
@ -11,10 +12,11 @@ import (
// 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"` Comm communicator.Config `mapstructure:",squash"`
SSHKeyPairName string `mapstructure:"ssh_keypair_name"` SSHKeyPairName string `mapstructure:"ssh_keypair_name"`
SSHInterface string `mapstructure:"ssh_interface"` TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
SSHIPVersion string `mapstructure:"ssh_ip_version"` SSHInterface string `mapstructure:"ssh_interface"`
SSHIPVersion string `mapstructure:"ssh_ip_version"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
SourceImageName string `mapstructure:"source_image_name"` SourceImageName string `mapstructure:"source_image_name"`
@ -38,6 +40,15 @@ type RunConfig struct {
} }
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// If we are not given an explicit ssh_keypair_name or
// ssh_private_key_file, then create a temporary one, but only if the
// temporary_key_pair_name has not been provided and we are not using
// ssh_password.
if c.SSHKeyPairName == "" && c.TemporaryKeyPairName == "" &&
c.Comm.SSHPrivateKey == "" && c.Comm.SSHPassword == "" {
c.TemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
if c.UseFloatingIp && c.FloatingIpPool == "" { if c.UseFloatingIp && c.FloatingIpPool == "" {
c.FloatingIpPool = "public" c.FloatingIpPool = "public"
@ -45,6 +56,15 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Validation // Validation
errs := c.Comm.Prepare(ctx) errs := c.Comm.Prepare(ctx)
if c.SSHKeyPairName != "" {
if c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" && c.Comm.SSHPrivateKey == "" {
errs = append(errs, errors.New("A private_key_file must be provided to retrieve the winrm password when using ssh_keypair_name."))
} else if c.Comm.SSHPrivateKey == "" && !c.Comm.SSHAgentAuth {
errs = append(errs, errors.New("A private_key_file must be provided or ssh_agent_auth enabled when ssh_keypair_name is specified."))
}
}
if c.SourceImage == "" && c.SourceImageName == "" { if c.SourceImage == "" && c.SourceImageName == "" {
errs = append(errs, errors.New("Either a source_image or a source_image_name must be specified")) errs = append(errs, errors.New("Either a source_image or a source_image_name must be specified"))
} else if len(c.SourceImage) > 0 && len(c.SourceImageName) > 0 { } else if len(c.SourceImage) > 0 && len(c.SourceImageName) > 0 {

View File

@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"os"
"time" "time"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
@ -12,6 +14,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
packerssh "github.com/mitchellh/packer/communicator/ssh" packerssh "github.com/mitchellh/packer/communicator/ssh"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
) )
// CommHost looks up the host for the communicator. // CommHost looks up the host for the communicator.
@ -63,8 +66,26 @@ func CommHost(
// SSHConfig returns a function that can be used for the SSH communicator // SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the instance created over SSH using a private key // config for connecting to the instance created over SSH using a private key
// or a password. // or a password.
func SSHConfig(username, password string) func(multistep.StateBag) (*ssh.ClientConfig, error) { func SSHConfig(useAgent bool, username, password string) func(multistep.StateBag) (*ssh.ClientConfig, error) {
return func(state multistep.StateBag) (*ssh.ClientConfig, error) { return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
if useAgent {
authSock := os.Getenv("SSH_AUTH_SOCK")
if authSock == "" {
return nil, fmt.Errorf("SSH_AUTH_SOCK is not set")
}
sshAgent, err := net.Dial("unix", authSock)
if err != nil {
return nil, fmt.Errorf("Cannot connect to SSH Agent socket %q: %s", authSock, err)
}
return &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers),
},
}, nil
}
privateKey, hasKey := state.GetOk("privateKey") privateKey, hasKey := state.GetOk("privateKey")

View File

@ -10,21 +10,24 @@ import (
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type StepKeyPair struct { type StepKeyPair struct {
Debug bool Debug bool
DebugKeyPath string SSHAgentAuth bool
KeyPairName string DebugKeyPath string
PrivateKeyFile string TemporaryKeyPairName string
KeyPairName string
PrivateKeyFile string
keyName string keyName string
} }
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.PrivateKeyFile != "" { if s.PrivateKeyFile != "" {
privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile) privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile)
if err != nil { if err != nil {
@ -39,14 +42,25 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
config := state.Get("config").(Config) if s.SSHAgentAuth && s.KeyPairName == "" {
ui := state.Get("ui").(packer.Ui) ui.Say("Using SSH Agent with key pair in Source image")
if config.Comm.Type == "ssh" && config.Comm.SSHPassword != "" {
ui.Say("Not creating temporary keypair when using password.")
return multistep.ActionContinue return multistep.ActionContinue
} }
if s.SSHAgentAuth && s.KeyPairName != "" {
ui.Say(fmt.Sprintf("Using SSH Agent for existing key pair %s", s.KeyPairName))
state.Put("keyPair", s.KeyPairName)
return multistep.ActionContinue
}
if s.TemporaryKeyPairName == "" {
ui.Say("Not using temporary keypair")
state.Put("keyPair", "")
return multistep.ActionContinue
}
config := state.Get("config").(Config)
// We need the v2 compute client // We need the v2 compute client
computeClient, err := config.computeV2Client() computeClient, err := config.computeV2Client()
if err != nil { if err != nil {
@ -55,10 +69,9 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID()) ui.Say(fmt.Sprintf("Creating temporary keypair: %s ...", s.TemporaryKeyPairName))
ui.Say(fmt.Sprintf("Creating temporary keypair: %s ...", keyName))
keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{ keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{
Name: keyName, Name: s.TemporaryKeyPairName,
}).Extract() }).Extract()
if err != nil { if err != nil {
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
@ -70,7 +83,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
ui.Say(fmt.Sprintf("Created temporary keypair: %s", keyName)) ui.Say(fmt.Sprintf("Created temporary keypair: %s", s.TemporaryKeyPairName))
keypair.PrivateKey = berToDer(keypair.PrivateKey, ui) keypair.PrivateKey = berToDer(keypair.PrivateKey, ui)
@ -101,10 +114,10 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
} }
// Set the keyname so we know to delete it later // Set the keyname so we know to delete it later
s.keyName = keyName s.keyName = s.TemporaryKeyPairName
// Set some state data for use in future steps // Set some state data for use in future steps
state.Put("keyPair", keyName) state.Put("keyPair", s.keyName)
state.Put("privateKey", keypair.PrivateKey) state.Put("privateKey", keypair.PrivateKey)
return multistep.ActionContinue return multistep.ActionContinue
@ -156,11 +169,11 @@ func berToDer(ber string, ui packer.Ui) string {
func (s *StepKeyPair) Cleanup(state multistep.StateBag) { func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
// If we used an SSH private key file, do not go about deleting // If we used an SSH private key file, do not go about deleting
// keypairs // keypairs
if s.PrivateKeyFile != "" { if s.PrivateKeyFile != "" || (s.KeyPairName == "" && s.keyName == "") {
return return
} }
// If no key name is set, then we never created it, so just return // If no key name is set, then we never created it, so just return
if s.keyName == "" { if s.TemporaryKeyPairName == "" {
return return
} }
@ -171,14 +184,14 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
computeClient, err := config.computeV2Client() computeClient, err := config.computeV2Client()
if err != nil { if err != nil {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) "Error cleaning up keypair. Please delete the key manually: %s", s.TemporaryKeyPairName))
return return
} }
ui.Say(fmt.Sprintf("Deleting temporary keypair: %s ...", s.keyName)) ui.Say(fmt.Sprintf("Deleting temporary keypair: %s ...", s.TemporaryKeyPairName))
err = keypairs.Delete(computeClient, s.keyName).ExtractErr() err = keypairs.Delete(computeClient, s.keyName).ExtractErr()
if err != nil { if err != nil {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) "Error cleaning up keypair. Please delete the key manually: %s", s.TemporaryKeyPairName))
} }
} }

View File

@ -144,8 +144,21 @@ builder.
- `ssh_keypair_name` (string) - If specified, this is the key that will be - `ssh_keypair_name` (string) - If specified, this is the key that will be
used for SSH with the machine. By default, this is blank, and Packer will used for SSH with the machine. By default, this is blank, and Packer will
generate a temporary keypair. generate a temporary keypair.
[`ssh_password`](/docs/templates/communicator.html#ssh_password) is used.
[`ssh_private_key_file`](/docs/templates/communicator.html#ssh_private_key_file) [`ssh_private_key_file`](/docs/templates/communicator.html#ssh_private_key_file)
must be specified with this. or `ssh_agent_auth` must be specified when `ssh_keypair_name` is utilized.
- `ssh_agent_auth` (boolean) - If true, the local SSH agent will be used to
authenticate connections to the source instance. No temporary keypair will
be created, and the values of `ssh_password` and `ssh_private_key_file` will
be ignored. To use this option with a key pair already configured in the source
image, leave the `ssh_keypair_name` blank. To associate an existing key pair
with the source instance, set the `ssh_keypair_name` field to the name
of the key pair.
- `temporary_key_pair_name` (string) - The name of the temporary key pair
to generate. By default, Packer generates a name that looks like
`packer_<UUID>`, where \<UUID\> is a 36 character unique identifier.
- `tenant_id` or `tenant_name` (string) - The tenant ID or name to boot the - `tenant_id` or `tenant_name` (string) - The tenant ID or name to boot the
instance into. Some OpenStack installations require this. If not specified, instance into. Some OpenStack installations require this. If not specified,