diff --git a/builder/cloudstack/builder.go b/builder/cloudstack/builder.go index 369fe0cc6..34dd67dbb 100644 --- a/builder/cloudstack/builder.go +++ b/builder/cloudstack/builder.go @@ -1,6 +1,8 @@ package cloudstack import ( + "fmt" + "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/packer" @@ -61,6 +63,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe HTTPPortMin: b.config.HTTPPortMin, HTTPPortMax: b.config.HTTPPortMax, }, + &stepKeypair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("cs_%s.pem", b.config.PackerBuildName), + SSHAgentAuth: b.config.Comm.SSHAgentAuth, + TemporaryKeyPair: b.config.TemporaryKeypair, + KeyPair: b.config.Keypair, + PrivateKeyFile: b.config.Comm.SSHPrivateKey, + }, &stepCreateInstance{ Ctx: b.config.ctx, }, diff --git a/builder/cloudstack/config.go b/builder/cloudstack/config.go index 5d6e9da0a..8571a0e03 100644 --- a/builder/cloudstack/config.go +++ b/builder/cloudstack/config.go @@ -34,6 +34,7 @@ type Config struct { Hypervisor string `mapstructure:"hypervisor"` InstanceName string `mapstructure:"instance_name"` Keypair string `mapstructure:"keypair"` + TemporaryKeypair string `mapstructure:"temporary_keypair"` Network string `mapstructure:"network"` Project string `mapstructure:"project"` PublicIPAddress string `mapstructure:"public_ip_address"` @@ -120,6 +121,15 @@ func NewConfig(raws ...interface{}) (*Config, error) { c.TemplateDisplayText = c.TemplateName } + // If we are not given an explicit keypair or ssh_private_key_file, then create + // a temporary one, but only if the temporary_keypair has not been provided and + // we are not using ssh_password. + if c.Keypair == "" && c.TemporaryKeypair == "" && + c.Comm.SSHPrivateKey == "" && c.Comm.SSHPassword == "" { + + c.TemporaryKeypair = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()) + } + // Process required parameters. if c.APIURL == "" { errs = packer.MultiErrorAppend(errs, errors.New("a api_url must be specified")) diff --git a/builder/cloudstack/step_create_instance.go b/builder/cloudstack/step_create_instance.go index 3b624de3c..1e3dbae99 100644 --- a/builder/cloudstack/step_create_instance.go +++ b/builder/cloudstack/step_create_instance.go @@ -44,8 +44,8 @@ func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction p.SetName(config.InstanceName) p.SetDisplayname("Created by Packer") - if config.Keypair != "" { - p.SetKeypair(config.Keypair) + if keypair, ok := state.GetOk("keypair"); ok { + p.SetKeypair(keypair.(string)) } // If we use an ISO, configure the disk offering. diff --git a/builder/cloudstack/step_keypair.go b/builder/cloudstack/step_keypair.go new file mode 100644 index 000000000..41e180fc0 --- /dev/null +++ b/builder/cloudstack/step_keypair.go @@ -0,0 +1,141 @@ +package cloudstack + +import ( + "fmt" + "io/ioutil" + "os" + "runtime" + + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +type stepKeypair struct { + Debug bool + SSHAgentAuth bool + DebugKeyPath string + TemporaryKeyPair string + KeyPair string + PrivateKeyFile string + + doCleanup bool +} + +func (s *stepKeypair) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if s.PrivateKeyFile != "" { + privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile) + if err != nil { + state.Put("error", fmt.Errorf( + "Error loading configured private key file: %s", err)) + return multistep.ActionHalt + } + + state.Put("keypair", s.KeyPair) + state.Put("privateKey", string(privateKeyBytes)) + + return multistep.ActionContinue + } + + if s.SSHAgentAuth && s.KeyPair == "" { + ui.Say("Using SSH Agent with keypair in Source image") + return multistep.ActionContinue + } + + if s.SSHAgentAuth && s.KeyPair != "" { + ui.Say(fmt.Sprintf("Using SSH Agent for existing keypair %s", s.KeyPair)) + state.Put("keypair", s.KeyPair) + return multistep.ActionContinue + } + + if s.TemporaryKeyPair == "" { + ui.Say("Not using temporary keypair") + state.Put("keypair", "") + return multistep.ActionContinue + } + + client := state.Get("client").(*cloudstack.CloudStackClient) + + ui.Say(fmt.Sprintf("Creating temporary keypair: %s ...", s.TemporaryKeyPair)) + p := client.SSH.NewCreateSSHKeyPairParams( + s.TemporaryKeyPair, + ) + + keypair, err := client.SSH.CreateSSHKeyPair(p) + if err != nil { + err := fmt.Errorf("Error creating temporary keypair: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if keypair.Privatekey == "" { + err := fmt.Errorf("The temporary keypair returned was blank") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Created temporary keypair: %s", s.TemporaryKeyPair)) + + // If we're in debug mode, output the private key to the working + // directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.Create(s.DebugKeyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write([]byte(keypair.Privatekey)); err != nil { + err := fmt.Errorf("Error saving debug key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Chmod it so that it is SSH ready + if runtime.GOOS != "windows" { + if err := f.Chmod(0600); err != nil { + err := fmt.Errorf("Error setting permissions of debug key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + + // we created a temporary key, so remember to clean it up + s.doCleanup = true + + // Set some state data for use in future steps + state.Put("keypair", s.TemporaryKeyPair) + state.Put("privateKey", keypair.Privatekey) + + return multistep.ActionContinue +} + +func (s *stepKeypair) Cleanup(state multistep.StateBag) { + if !s.doCleanup { + return + } + + ui := state.Get("ui").(packer.Ui) + client := state.Get("client").(*cloudstack.CloudStackClient) + + ui.Say(fmt.Sprintf("Deleting temporary keypair: %s ...", s.TemporaryKeyPair)) + + _, err := client.SSH.DeleteSSHKeyPair(client.SSH.NewDeleteSSHKeyPairParams( + s.TemporaryKeyPair, + )) + if err != nil { + ui.Error(err.Error()) + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.TemporaryKeyPair)) + } +} diff --git a/builder/cloudstack/step_prepare_config.go b/builder/cloudstack/step_prepare_config.go index de397308e..af28a76a8 100644 --- a/builder/cloudstack/step_prepare_config.go +++ b/builder/cloudstack/step_prepare_config.go @@ -22,15 +22,6 @@ func (s *stepPrepareConfig) Run(state multistep.StateBag) multistep.StepAction { var err error var errs *packer.MultiError - if config.Comm.SSHPrivateKey != "" { - privateKey, err := ioutil.ReadFile(config.Comm.SSHPrivateKey) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Error loading configured private key file: %s", err)) - } - - state.Put("privateKey", privateKey) - } - // First get the project and zone UUID's so we can use them in other calls when needed. if config.Project != "" && !isUUID(config.Project) { config.Project, _, err = client.Project.GetProjectID(config.Project) diff --git a/website/source/docs/builders/cloudstack.html.md b/website/source/docs/builders/cloudstack.html.md index 9b6bc2cab..a3f091315 100644 --- a/website/source/docs/builders/cloudstack.html.md +++ b/website/source/docs/builders/cloudstack.html.md @@ -149,6 +149,10 @@ builder. - `template_scalable` (boolean) - Set to `true` to indicate that the template contains tools to support dynamic scaling of VM cpu/memory. Defaults to `false`. +- `temporary_keypair_name` (string) - The name of the temporary SSH key pair + to generate. By default, Packer generates a name that looks like + `packer_`, where <UUID> is a 36 character unique identifier. + - `user_data` (string) - User data to launch with the instance. This is a [template engine](/docs/templates/engine.html) see _User Data_ bellow for more details.