diff --git a/builder/cloudstack/builder.go b/builder/cloudstack/builder.go index 369fe0cc6..c4ad8a5b2 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,8 +63,17 @@ 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), + KeyPair: b.config.Keypair, + PrivateKeyFile: b.config.Comm.SSHPrivateKey, + SSHAgentAuth: b.config.Comm.SSHAgentAuth, + TemporaryKeyPairName: b.config.TemporaryKeypairName, + }, &stepCreateInstance{ - Ctx: b.config.ctx, + Ctx: b.config.ctx, + Debug: b.config.PackerDebug, }, &stepSetupNetworking{}, &communicator.StepConnect{ diff --git a/builder/cloudstack/config.go b/builder/cloudstack/config.go index 5d6e9da0a..513ad8eed 100644 --- a/builder/cloudstack/config.go +++ b/builder/cloudstack/config.go @@ -27,23 +27,24 @@ type Config struct { HTTPGetOnly bool `mapstructure:"http_get_only"` SSLNoVerify bool `mapstructure:"ssl_no_verify"` - CIDRList []string `mapstructure:"cidr_list"` - DiskOffering string `mapstructure:"disk_offering"` - DiskSize int64 `mapstructure:"disk_size"` - Expunge bool `mapstructure:"expunge"` - Hypervisor string `mapstructure:"hypervisor"` - InstanceName string `mapstructure:"instance_name"` - Keypair string `mapstructure:"keypair"` - Network string `mapstructure:"network"` - Project string `mapstructure:"project"` - PublicIPAddress string `mapstructure:"public_ip_address"` - ServiceOffering string `mapstructure:"service_offering"` - SourceTemplate string `mapstructure:"source_template"` - SourceISO string `mapstructure:"source_iso"` - UserData string `mapstructure:"user_data"` - UserDataFile string `mapstructure:"user_data_file"` - UseLocalIPAddress bool `mapstructure:"use_local_ip_address"` - Zone string `mapstructure:"zone"` + CIDRList []string `mapstructure:"cidr_list"` + DiskOffering string `mapstructure:"disk_offering"` + DiskSize int64 `mapstructure:"disk_size"` + Expunge bool `mapstructure:"expunge"` + Hypervisor string `mapstructure:"hypervisor"` + InstanceName string `mapstructure:"instance_name"` + Keypair string `mapstructure:"keypair"` + TemporaryKeypairName string `mapstructure:"temporary_keypair_name"` + Network string `mapstructure:"network"` + Project string `mapstructure:"project"` + PublicIPAddress string `mapstructure:"public_ip_address"` + ServiceOffering string `mapstructure:"service_offering"` + SourceTemplate string `mapstructure:"source_template"` + SourceISO string `mapstructure:"source_iso"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` + UseLocalIPAddress bool `mapstructure:"use_local_ip_address"` + Zone string `mapstructure:"zone"` TemplateName string `mapstructure:"template_name"` TemplateDisplayText string `mapstructure:"template_display_text"` @@ -120,6 +121,14 @@ func NewConfig(raws ...interface{}) (*Config, error) { c.TemplateDisplayText = c.TemplateName } + // If we are not given an explicit keypair, ssh_password or ssh_private_key_file, + // then create a temporary one, but only if the temporary_keypair_name has not + // been provided. + if c.Keypair == "" && c.TemporaryKeypairName == "" && + c.Comm.SSHPrivateKey == "" && c.Comm.SSHPassword == "" { + c.TemporaryKeypairName = 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..894d0394e 100644 --- a/builder/cloudstack/step_create_instance.go +++ b/builder/cloudstack/step_create_instance.go @@ -22,7 +22,8 @@ type userDataTemplateData struct { // stepCreateInstance represents a Packer build step that creates CloudStack instances. type stepCreateInstance struct { - Ctx interpolate.Context + Debug bool + Ctx interpolate.Context } // Run executes the Packer build step that creates a CloudStack instance. @@ -44,8 +45,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. @@ -115,6 +116,12 @@ func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction ui.Message("Instance has been created!") + // In debug-mode, we output the password + if s.Debug { + ui.Message(fmt.Sprintf( + "Password (since debug is enabled) \"%s\"", instance.Password)) + } + // Set the auto generated password if a password was not explicitly configured. switch config.Comm.Type { case "ssh": diff --git a/builder/cloudstack/step_keypair.go b/builder/cloudstack/step_keypair.go new file mode 100644 index 000000000..675994fc1 --- /dev/null +++ b/builder/cloudstack/step_keypair.go @@ -0,0 +1,133 @@ +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 + DebugKeyPath string + KeyPair string + PrivateKeyFile string + SSHAgentAuth bool + TemporaryKeyPairName string +} + +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.TemporaryKeyPairName == "" { + ui.Say("Not using a keypair") + state.Put("keypair", "") + return multistep.ActionContinue + } + + client := state.Get("client").(*cloudstack.CloudStackClient) + + ui.Say(fmt.Sprintf("Creating temporary keypair: %s ...", s.TemporaryKeyPairName)) + + p := client.SSH.NewCreateSSHKeyPairParams(s.TemporaryKeyPairName) + 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.TemporaryKeyPairName)) + + // 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 + } + } + } + + // Set some state data for use in future steps + state.Put("keypair", s.TemporaryKeyPairName) + state.Put("privateKey", keypair.Privatekey) + + return multistep.ActionContinue +} + +func (s *stepKeypair) Cleanup(state multistep.StateBag) { + if s.TemporaryKeyPairName == "" { + return + } + + ui := state.Get("ui").(packer.Ui) + client := state.Get("client").(*cloudstack.CloudStackClient) + + ui.Say(fmt.Sprintf("Deleting temporary keypair: %s ...", s.TemporaryKeyPairName)) + + _, err := client.SSH.DeleteSSHKeyPair(client.SSH.NewDeleteSSHKeyPairParams( + s.TemporaryKeyPairName, + )) + if err != nil { + ui.Error(err.Error()) + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.TemporaryKeyPairName)) + } +} 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.