diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f776a04c..8402e7157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * provisioner/salt-masterless: Also use sudo to clean up if we used sudo to install. [GH-5240] * builder/profitbricks: added support for Cloud API v4. [GH-5233] * builder/vmware: Set artifact ID to `VMName`. [GH-5187] +* core: Fix issue where some builders wouldn't respect `-on-error` behavior. [GH-5297] +* builder/cloudstack: Add support for Security Groups. [GH-5175] +* provisioner/puppet: Add `guest_os_type` option to add support for Windows. [GH-5252] ### BACKWARDS INCOMPATIBILITIES: diff --git a/builder/cloudstack/builder.go b/builder/cloudstack/builder.go index 369fe0cc6..e46e56086 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,18 @@ 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, + }, + &stepCreateSecurityGroup{}, &stepCreateInstance{ - Ctx: b.config.ctx, + Ctx: b.config.ctx, + Debug: b.config.PackerDebug, }, &stepSetupNetworking{}, &communicator.StepConnect{ @@ -78,17 +90,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &stepCreateTemplate{}, } - // Configure the runner. - if b.config.PackerDebug { - b.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: common.MultistepDebugFn(ui), - } - } else { - b.runner = &multistep.BasicRunner{Steps: steps} - } - - // Run the steps. + // Configure the runner and run the steps. + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(state) // If there was an error, return that diff --git a/builder/cloudstack/config.go b/builder/cloudstack/config.go index 5d6e9da0a..35026f6e3 100644 --- a/builder/cloudstack/config.go +++ b/builder/cloudstack/config.go @@ -27,23 +27,26 @@ 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"` + CreateSecurityGroup bool `mapstructure:"create_security_group"` + 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"` + SecurityGroups []string `mapstructure:"security_groups"` + ServiceOffering string `mapstructure:"service_offering"` + SourceISO string `mapstructure:"source_iso"` + SourceTemplate string `mapstructure:"source_template"` + TemporaryKeypairName string `mapstructure:"temporary_keypair_name"` + UseLocalIPAddress bool `mapstructure:"use_local_ip_address"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` + Zone string `mapstructure:"zone"` TemplateName string `mapstructure:"template_name"` TemplateDisplayText string `mapstructure:"template_display_text"` @@ -98,7 +101,7 @@ func NewConfig(raws ...interface{}) (*Config, error) { c.AsyncTimeout = 30 * time.Minute } - if len(c.CIDRList) == 0 && !c.UseLocalIPAddress { + if len(c.CIDRList) == 0 { c.CIDRList = []string{"0.0.0.0/0"} } @@ -120,6 +123,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")) @@ -137,6 +148,10 @@ func NewConfig(raws ...interface{}) (*Config, error) { errs = packer.MultiErrorAppend(errs, errors.New("a network must be specified")) } + if c.CreateSecurityGroup && !c.Expunge { + errs = packer.MultiErrorAppend(errs, errors.New("auto creating a temporary security group requires expunge")) + } + if c.ServiceOffering == "" { errs = packer.MultiErrorAppend(errs, errors.New("a service_offering must be specified")) } diff --git a/builder/cloudstack/step_create_instance.go b/builder/cloudstack/step_create_instance.go index 3b624de3c..538a062b9 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,12 @@ 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 securitygroups, ok := state.GetOk("security_groups"); ok { + p.SetSecuritygroupids(securitygroups.([]string)) } // If we use an ISO, configure the disk offering. @@ -115,6 +120,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_create_security_group.go b/builder/cloudstack/step_create_security_group.go new file mode 100644 index 000000000..1bf23100b --- /dev/null +++ b/builder/cloudstack/step_create_security_group.go @@ -0,0 +1,94 @@ +package cloudstack + +import ( + "fmt" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +type stepCreateSecurityGroup struct { + tempSG string +} + +func (s *stepCreateSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*cloudstack.CloudStackClient) + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + if len(config.SecurityGroups) > 0 { + state.Put("security_groups", config.SecurityGroups) + return multistep.ActionContinue + } + + if !config.CreateSecurityGroup { + return multistep.ActionContinue + } + + ui.Say("Creating temporary Security Group...") + + p := client.SecurityGroup.NewCreateSecurityGroupParams( + fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()), + ) + p.SetDescription("Temporary SG created by Packer") + if config.Project != "" { + p.SetProjectid(config.Project) + } + + sg, err := client.SecurityGroup.CreateSecurityGroup(p) + if err != nil { + err := fmt.Errorf("Failed to create security group: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.tempSG = sg.Id + state.Put("security_groups", []string{sg.Id}) + + // Create Ingress rule + i := client.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() + i.SetCidrlist(config.CIDRList) + i.SetProtocol("TCP") + i.SetSecuritygroupid(sg.Id) + i.SetStartport(config.Comm.Port()) + i.SetEndport(config.Comm.Port()) + if config.Project != "" { + i.SetProjectid(config.Project) + } + + _, err = client.SecurityGroup.AuthorizeSecurityGroupIngress(i) + if err != nil { + err := fmt.Errorf("Failed to authorize security group ingress rule: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +// Cleanup any resources that may have been created during the Run phase. +func (s *stepCreateSecurityGroup) Cleanup(state multistep.StateBag) { + client := state.Get("client").(*cloudstack.CloudStackClient) + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + if s.tempSG == "" { + return + } + + ui.Say(fmt.Sprintf("Cleanup temporary security group: %s ...", s.tempSG)) + p := client.SecurityGroup.NewDeleteSecurityGroupParams() + p.SetId(s.tempSG) + if config.Project != "" { + p.SetProjectid(config.Project) + } + + if _, err := client.SecurityGroup.DeleteSecurityGroup(p); err != nil { + ui.Error(err.Error()) + ui.Error(fmt.Sprintf("Error deleting security group: %s. Please destroy it manually.\n", s.tempSG)) + } +} 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..6cfb5c478 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) @@ -92,6 +83,18 @@ func (s *stepPrepareConfig) Run(state multistep.StateBag) multistep.StepAction { } } + // Then try to get the SG's UUID's. + if len(config.SecurityGroups) > 0 { + for i := range config.SecurityGroups { + if !isUUID(config.SecurityGroups[i]) { + config.SecurityGroups[i], _, err = client.SecurityGroup.GetSecurityGroupID(config.SecurityGroups[i], cloudstack.WithProject(config.Project)) + if err != nil { + errs = packer.MultiErrorAppend(errs, &retrieveErr{"network", config.SecurityGroups[i], err}) + } + } + } + } + if !isUUID(config.ServiceOffering) { config.ServiceOffering, _, err = client.ServiceOffering.GetServiceOfferingID(config.ServiceOffering) if err != nil { diff --git a/builder/hyperv/iso/builder.go b/builder/hyperv/iso/builder.go index 657c07886..9b4065d8c 100644 --- a/builder/hyperv/iso/builder.go +++ b/builder/hyperv/iso/builder.go @@ -405,17 +405,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe } // Run the steps. - if b.config.PackerDebug { - pauseFn := common.MultistepDebugFn(ui) - state.Put("pauseFn", pauseFn) - b.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: pauseFn, - } - } else { - b.runner = &multistep.BasicRunner{Steps: steps} - } - + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(state) // Report any errors. diff --git a/builder/oneandone/builder.go b/builder/oneandone/builder.go index f4f8e1dc3..ad470c152 100644 --- a/builder/oneandone/builder.go +++ b/builder/oneandone/builder.go @@ -50,15 +50,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepTakeSnapshot), } - if b.config.PackerDebug { - b.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: common.MultistepDebugFn(ui), - } - } else { - b.runner = &multistep.BasicRunner{Steps: steps} - } - + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(state) if rawErr, ok := state.GetOk("error"); ok { diff --git a/builder/profitbricks/builder.go b/builder/profitbricks/builder.go index 95a2b1b2d..750b843c0 100644 --- a/builder/profitbricks/builder.go +++ b/builder/profitbricks/builder.go @@ -49,15 +49,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe config := state.Get("config").(*Config) - if b.config.PackerDebug { - b.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: common.MultistepDebugFn(ui), - } - } else { - b.runner = &multistep.BasicRunner{Steps: steps} - } - + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(state) if rawErr, ok := state.GetOk("error"); ok { diff --git a/builder/vmware/iso/builder.go b/builder/vmware/iso/builder.go index 3567e26a2..688f52d72 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -200,6 +200,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe default: dir = new(vmwcommon.LocalOutputDir) } + + exportOutputPath := b.config.OutputDir + if b.config.RemoteType != "" && b.config.Format != "" { b.config.OutputDir = b.config.VMName } @@ -307,6 +310,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepExport{ Format: b.config.Format, SkipExport: b.config.SkipExport, + OutputDir: exportOutputPath, }, } @@ -332,7 +336,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe var files []string if b.config.RemoteType != "" && b.config.Format != "" { dir = new(vmwcommon.LocalOutputDir) - dir.SetOutputDir(b.config.OutputDir) + dir.SetOutputDir(exportOutputPath) files, err = dir.ListFiles() } else { files, err = state.Get("dir").(OutputDir).ListFiles() diff --git a/builder/vmware/iso/step_export.go b/builder/vmware/iso/step_export.go index c0d39c76f..91a2ce486 100644 --- a/builder/vmware/iso/step_export.go +++ b/builder/vmware/iso/step_export.go @@ -6,7 +6,6 @@ import ( "net/url" "os" "os/exec" - "path/filepath" "runtime" "strings" @@ -17,9 +16,10 @@ import ( type StepExport struct { Format string SkipExport bool + OutputDir string } -func (s *StepExport) generateArgs(c *Config, outputPath string, hidePassword bool) []string { +func (s *StepExport) generateArgs(c *Config, hidePassword bool) []string { password := url.QueryEscape(c.RemotePassword) if hidePassword { password = "****" @@ -29,7 +29,7 @@ func (s *StepExport) generateArgs(c *Config, outputPath string, hidePassword boo "--skipManifestCheck", "-tt=" + s.Format, "vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + c.VMName, - outputPath, + s.OutputDir, } return append(c.OVFToolOptions, args...) } @@ -62,16 +62,18 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { } // Export the VM - outputPath := filepath.Join(c.VMName, c.VMName+"."+s.Format) + if s.OutputDir == "" { + s.OutputDir = c.VMName + "." + s.Format + } if s.Format == "ova" { - os.MkdirAll(outputPath, 0755) + os.MkdirAll(s.OutputDir, 0755) } ui.Say("Exporting virtual machine...") - ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, outputPath, true), " "))) + ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, true), " "))) var out bytes.Buffer - cmd := exec.Command(ovftool, s.generateArgs(c, outputPath, false)...) + cmd := exec.Command(ovftool, s.generateArgs(c, false)...) cmd.Stdout = &out if err := cmd.Run(); err != nil { err := fmt.Errorf("Error exporting virtual machine: %s\n%s\n", err, out.String()) @@ -82,8 +84,6 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { ui.Message(fmt.Sprintf("%s", out.String())) - state.Put("exportPath", outputPath) - return multistep.ActionContinue } diff --git a/post-processor/googlecompute-export/post-processor.go b/post-processor/googlecompute-export/post-processor.go index 9fd39aa46..8a5befbc8 100644 --- a/post-processor/googlecompute-export/post-processor.go +++ b/post-processor/googlecompute-export/post-processor.go @@ -120,14 +120,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac } // Run the steps. - if p.config.PackerDebug { - p.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: common.MultistepDebugFn(ui), - } - } else { - p.runner = &multistep.BasicRunner{Steps: steps} - } + p.runner = common.NewRunner(steps, p.config.PackerConfig, ui) p.runner.Run(state) } diff --git a/post-processor/vagrant-cloud/post-processor.go b/post-processor/vagrant-cloud/post-processor.go index 01575def7..b8eeb7105 100644 --- a/post-processor/vagrant-cloud/post-processor.go +++ b/post-processor/vagrant-cloud/post-processor.go @@ -164,15 +164,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac } // Run the steps - if p.config.PackerDebug { - p.runner = &multistep.DebugRunner{ - Steps: steps, - PauseFn: common.MultistepDebugFn(ui), - } - } else { - p.runner = &multistep.BasicRunner{Steps: steps} - } - + p.runner = common.NewRunner(steps, p.config.PackerConfig, ui) p.runner.Run(state) // If there was an error, return that diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go index 6f1b6eee4..1c4ad3bc1 100644 --- a/provisioner/puppet-masterless/provisioner.go +++ b/provisioner/puppet-masterless/provisioner.go @@ -1,4 +1,4 @@ -// This package implements a provisioner for Packer that executes +// Package puppetmasterless implements a provisioner for Packer that executes // Puppet on the remote machine, configured to apply a local manifest // versus connecting to a Puppet master. package puppetmasterless @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/provisioner" "github.com/hashicorp/packer/template/interpolate" ) @@ -61,10 +62,51 @@ type Config struct { // If true, packer will ignore all exit-codes from a puppet run IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"` + + // The Guest OS Type (unix or windows) + GuestOSType string `mapstructure:"guest_os_type"` +} + +type guestOSTypeConfig struct { + stagingDir string + executeCommand string + facterVarsFmt string + modulePathJoiner string +} + +var guestOSTypeConfigs = map[string]guestOSTypeConfig{ + provisioner.UnixOSType: { + stagingDir: "/tmp/packer-puppet-masterless", + executeCommand: "cd {{.WorkingDir}} && " + + "{{.FacterVars}} {{if .Sudo}} sudo -E {{end}}" + + "puppet apply --verbose --modulepath='{{.ModulePath}}' " + + "{{if ne .HieraConfigPath \"\"}}--hiera_config='{{.HieraConfigPath}}' {{end}}" + + "{{if ne .ManifestDir \"\"}}--manifestdir='{{.ManifestDir}}' {{end}}" + + "--detailed-exitcodes " + + "{{if ne .ExtraArguments \"\"}}{{.ExtraArguments}} {{end}}" + + "{{.ManifestFile}}", + facterVarsFmt: "FACTER_%s='%s'", + modulePathJoiner: ":", + }, + provisioner.WindowsOSType: { + stagingDir: "C:/Windows/Temp/packer-puppet-masterless", + executeCommand: "cd {{.WorkingDir}} && " + + "{{.FacterVars}} && " + + "puppet apply --verbose --modulepath='{{.ModulePath}}' " + + "{{if ne .HieraConfigPath \"\"}}--hiera_config='{{.HieraConfigPath}}' {{end}}" + + "{{if ne .ManifestDir \"\"}}--manifestdir='{{.ManifestDir}}' {{end}}" + + "--detailed-exitcodes " + + "{{if ne .ExtraArguments \"\"}}{{.ExtraArguments}} {{end}}" + + "{{.ManifestFile}}", + facterVarsFmt: "SET \"FACTER_%s=%s\" &", + modulePathJoiner: ";", + }, } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands } type ExecuteTemplate struct { @@ -94,20 +136,32 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } // Set some defaults + if p.config.GuestOSType == "" { + p.config.GuestOSType = provisioner.DefaultOSType + } + p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) + + var ok bool + p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] + if !ok { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + + p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) + if err != nil { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = "cd {{.WorkingDir}} && " + - "{{.FacterVars}} {{if .Sudo}} sudo -E {{end}}" + - "{{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}/{{end}}puppet apply " + - "--verbose --modulepath='{{.ModulePath}}' " + - "{{if ne .HieraConfigPath \"\"}}--hiera_config='{{.HieraConfigPath}}' {{end}}" + - "{{if ne .ManifestDir \"\"}}--manifestdir='{{.ManifestDir}}' {{end}}" + - "--detailed-exitcodes " + - "{{if ne .ExtraArguments \"\"}}{{.ExtraArguments}} {{end}}" + - "{{.ManifestFile}}" + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand + } + + if p.config.ExecuteCommand == "" { + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand } if p.config.StagingDir == "" { - p.config.StagingDir = "/tmp/packer-puppet-masterless" + p.config.StagingDir = p.guestOSTypeConfig.stagingDir } if p.config.WorkingDir == "" { @@ -223,7 +277,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { // Compile the facter variables facterVars := make([]string, 0, len(p.config.Facter)) for k, v := range p.config.Facter { - facterVars = append(facterVars, fmt.Sprintf("FACTER_%s='%s'", k, v)) + facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v)) } // Execute Puppet @@ -232,7 +286,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { HieraConfigPath: remoteHieraConfigPath, ManifestDir: remoteManifestDir, ManifestFile: remoteManifestFile, - ModulePath: strings.Join(modulePaths, ":"), + ModulePath: strings.Join(modulePaths, p.guestOSTypeConfig.modulePathJoiner), PuppetBinDir: p.config.PuppetBinDir, Sudo: !p.config.PreventSudo, WorkingDir: p.config.WorkingDir, @@ -249,7 +303,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Message(fmt.Sprintf("Running Puppet: %s", command)) if err := cmd.StartWithUi(comm, ui); err != nil { - return err + return fmt.Errorf("Got an error starting command: %s", err) } if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 && !p.config.IgnoreExitCodes { @@ -314,30 +368,29 @@ func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (s return "", fmt.Errorf("Error uploading manifest dir: %s", err) } return remoteManifestDir, nil - } else { - // Otherwise manifest_file is a file and we'll upload it - ui.Message(fmt.Sprintf( - "Uploading manifest file from: %s", p.config.ManifestFile)) - - f, err := os.Open(p.config.ManifestFile) - if err != nil { - return "", err - } - defer f.Close() - - manifestFilename := filepath.Base(p.config.ManifestFile) - remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename) - if err := comm.Upload(remoteManifestFile, f, nil); err != nil { - return "", err - } - return remoteManifestFile, nil } + // Otherwise manifest_file is a file and we'll upload it + ui.Message(fmt.Sprintf( + "Uploading manifest file from: %s", p.config.ManifestFile)) + + f, err := os.Open(p.config.ManifestFile) + if err != nil { + return "", err + } + defer f.Close() + + manifestFilename := filepath.Base(p.config.ManifestFile) + remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename) + if err := comm.Upload(remoteManifestFile, f, nil); err != nil { + return "", err + } + return remoteManifestFile, nil } func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("mkdir -p '%s'", dir), - } + ui.Message(fmt.Sprintf("Creating directory: %s", dir)) + + cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index 9e5e6a498..4da3ecbc6 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -1,4 +1,4 @@ -// This package implements a provisioner for Packer that executes +// Package puppetserver implements a provisioner for Packer that executes // Puppet on the remote machine connecting to a Puppet master. package puppetserver @@ -10,9 +10,45 @@ import ( "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/provisioner" "github.com/hashicorp/packer/template/interpolate" ) +type guestOSTypeConfig struct { + executeCommand string + facterVarsFmt string + stagingDir string +} + +var guestOSTypeConfigs = map[string]guestOSTypeConfig{ + provisioner.UnixOSType: { + executeCommand: "{{.FacterVars}} {{if .Sudo}}sudo -E {{end}}" + + "{{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}/{{end}}puppet agent " + + "--onetime --no-daemonize " + + "{{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}}" + + "{{if ne .Options \"\"}}{{.Options}} {{end}}" + + "{{if ne .PuppetNode \"\"}}--certname={{.PuppetNode}} {{end}}" + + "{{if ne .ClientCertPath \"\"}}--certdir='{{.ClientCertPath}}' {{end}}" + + "{{if ne .ClientPrivateKeyPath \"\"}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}" + + "--detailed-exitcodes", + facterVarsFmt: "FACTER_%s='%s'", + stagingDir: "/tmp/packer-puppet-server", + }, + provisioner.WindowsOSType: { + executeCommand: "{{.FacterVars}} " + + "{{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}/{{end}}puppet agent " + + "--onetime --no-daemonize " + + "{{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}}" + + "{{if ne .Options \"\"}}{{.Options}} {{end}}" + + "{{if ne .PuppetNode \"\"}}--certname={{.PuppetNode}} {{end}}" + + "{{if ne .ClientCertPath \"\"}}--certdir='{{.ClientCertPath}}' {{end}}" + + "{{if ne .ClientPrivateKeyPath \"\"}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}" + + "--detailed-exitcodes", + facterVarsFmt: "SET \"FACTER_%s=%s\" &", + stagingDir: "C:/Windows/Temp/packer-puppet-server", + }, +} + type Config struct { common.PackerConfig `mapstructure:",squash"` ctx interpolate.Context @@ -20,6 +56,9 @@ type Config struct { // The command used to execute Puppet. ExecuteCommand string `mapstructure:"execute_command"` + // The Guest OS Type (unix or windows) + GuestOSType string `mapstructure:"guest_os_type"` + // Additional facts to set when executing Puppet Facter map[string]string @@ -54,7 +93,9 @@ type Config struct { } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands } type ExecuteTemplate struct { @@ -82,12 +123,28 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return err } + if p.config.GuestOSType == "" { + p.config.GuestOSType = provisioner.DefaultOSType + } + p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) + + var ok bool + p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] + if !ok { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + + p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) + if err != nil { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = p.commandTemplate() + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand } if p.config.StagingDir == "" { - p.config.StagingDir = "/tmp/packer-puppet-server" + p.config.StagingDir = p.guestOSTypeConfig.stagingDir } if p.config.Facter == nil { @@ -160,7 +217,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { // Compile the facter variables facterVars := make([]string, 0, len(p.config.Facter)) for k, v := range p.config.Facter { - facterVars = append(facterVars, fmt.Sprintf("FACTER_%s='%s'", k, v)) + facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v)) } // Execute Puppet @@ -202,16 +259,23 @@ func (p *Provisioner) Cancel() { } func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("mkdir -p '%s'", dir), - } + ui.Message(fmt.Sprintf("Creating directory: %s", dir)) + cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } - if cmd.ExitStatus != 0 { - return fmt.Errorf("Non-zero exit status.") + return fmt.Errorf("Non-zero exit status. See output above for more info.") + } + + // Chmod the directory to 0777 just so that we can access it as our user + cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} + if err := cmd.StartWithUi(comm, ui); err != nil { + return err + } + if cmd.ExitStatus != 0 { + return fmt.Errorf("Non-zero exit status. See output above for more info.") } return nil @@ -230,15 +294,3 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } - -func (p *Provisioner) commandTemplate() string { - return "{{.FacterVars}} {{if .Sudo}} sudo -E {{end}}" + - "{{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}/{{end}}puppet agent " + - "--onetime --no-daemonize " + - "{{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}}" + - "{{if ne .Options \"\"}}{{.Options}} {{end}}" + - "{{if ne .PuppetNode \"\"}}--certname={{.PuppetNode}} {{end}}" + - "{{if ne .ClientCertPath \"\"}}--certdir='{{.ClientCertPath}}' {{end}}" + - "{{if ne .ClientPrivateKeyPath \"\"}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}" + - "--detailed-exitcodes" -} diff --git a/website/source/docs/builders/cloudstack.html.md b/website/source/docs/builders/cloudstack.html.md index 9b6bc2cab..7e59fe718 100644 --- a/website/source/docs/builders/cloudstack.html.md +++ b/website/source/docs/builders/cloudstack.html.md @@ -74,6 +74,11 @@ builder. connect to the instance. Defaults to `[ "0.0.0.0/0" ]`. Only required when `use_local_ip_address` is `false`. +- `create_security_group` (boolean) - If `true` a temporary security group + will be created which allows traffic towards the instance from the + `cidr_list`. This option will be ignored if `security_groups` is also + defined. Requires `expunge` set to `true`. Defaults to `false`. + - `disk_offering` (string) - The name or ID of the disk offering used for the instance. This option is only available (and also required) when using `source_iso`. @@ -118,6 +123,9 @@ builder. connecting any provisioners to. If not provided, a temporary public IP address will be associated and released during the Packer run. +- `security_groups` (array of strings) - A list of security group IDs or names + to associate the instance with. + - `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 @@ -149,6 +157,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. diff --git a/website/source/docs/provisioners/puppet-masterless.html.md b/website/source/docs/provisioners/puppet-masterless.html.md index c630f8412..b58d78251 100644 --- a/website/source/docs/provisioners/puppet-masterless.html.md +++ b/website/source/docs/provisioners/puppet-masterless.html.md @@ -59,6 +59,10 @@ Optional parameters: variables](/docs/templates/engine.html) available. See below for more information. +- `guest_os_type` (string) - The target guest OS type, either "unix" or + "windows". Setting this to "windows" will cause the provisioner to use + Windows friendly paths and commands. By default, this is "unix". + - `extra_arguments` (array of strings) - This is an array of additional options to pass to the puppet command when executing puppet. This allows for customization of the `execute_command` without having to completely replace @@ -99,12 +103,13 @@ multiple manifests you should use `manifest_file` instead. executed to run Puppet are executed with `sudo`. If this is true, then the sudo will be omitted. -- `staging_directory` (string) - This is the directory where all the - configuration of Puppet by Packer will be placed. By default this - is "/tmp/packer-puppet-masterless". This directory doesn't need to exist but - must have proper permissions so that the SSH user that Packer uses is able - to create directories and write into this folder. If the permissions are not - correct, use a shell provisioner prior to this to configure it properly. +- `staging_directory` (string) - This is the directory where all the configuration + of Puppet by Packer will be placed. By default this is "/tmp/packer-puppet-masterless" + when guest OS type is unix and "C:/Windows/Temp/packer-puppet-masterless" when windows. + This directory doesn't need to exist but must have proper permissions so that the SSH + user that Packer uses is able to create directories and write into this folder. + If the permissions are not correct, use a shell provisioner prior to this to configure + it properly. - `working_directory` (string) - This is the directory from which the puppet command will be run. When using hiera with a relative path, this option @@ -117,17 +122,28 @@ multiple manifests you should use `manifest_file` instead. By default, Packer uses the following command (broken across multiple lines for readability) to execute Puppet: -``` liquid -cd {{.WorkingDir}} && \ -{{.FacterVars}}{{if .Sudo}} sudo -E {{end}} \ -{{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}{{end}}puppet apply \ - --verbose \ - --modulepath='{{.ModulePath}}' \ - {{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}} \ - {{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}} \ - --detailed-exitcodes \ - {{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}} \ - {{.ManifestFile}} +``` +cd {{.WorkingDir}} && +{{.FacterVars}} {{if .Sudo}} sudo -E {{end}} +puppet apply --verbose --modulepath='{{.ModulePath}}' +{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}} +{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}} +--detailed-exitcodes +{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}} +{{.ManifestFile}} +``` + +The following command is used if guest OS type is windows: + +``` +cd {{.WorkingDir}} && +{{.FacterVars}} && +puppet apply --verbose --modulepath='{{.ModulePath}}' +{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}} +{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}} +--detailed-exitcodes +{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}} +{{.ManifestFile}} ``` This command can be customized using the `execute_command` configuration. As you diff --git a/website/source/docs/provisioners/puppet-server.html.md b/website/source/docs/provisioners/puppet-server.html.md index 61a06d228..6adfd6b0b 100644 --- a/website/source/docs/provisioners/puppet-server.html.md +++ b/website/source/docs/provisioners/puppet-server.html.md @@ -81,20 +81,38 @@ listed below: or `%PATH%` environment variable, but some builders (notably, the Docker one) do not run profile-setup scripts, therefore the path is usually empty. -- `execute_command` (string) - This is optional. The command used to execute Puppet. This has - various [configuration template - variables](/docs/templates/engine.html) available. See - below for more information. By default, Packer uses the following command: +- `guest_os_type` (string) - The target guest OS type, either "unix" or + "windows". Setting this to "windows" will cause the provisioner to use + Windows friendly paths and commands. By default, this is "unix". -``` liquid -{{.FacterVars}} {{if .Sudo}} sudo -E {{end}} \ - {{if ne .PuppetBinDir \"\"}}{{.PuppetBinDir}}/{{end}}puppet agent --onetime --no-daemonize \ - {{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}} \ - {{if ne .Options \"\"}}{{.Options}} {{end}} \ - {{if ne .PuppetNode \"\"}}--certname={{.PuppetNode}} {{end}} \ - {{if ne .ClientCertPath \"\"}}--certdir='{{.ClientCertPath}}' {{end}} \ - {{if ne .ClientPrivateKeyPath \"\"}}--privatekeydir='{{.ClientPrivateKeyPath}}' \ - {{end}} --detailed-exitcodes +- `execute_command` (string) - This is optional. The command used to execute Puppet. This has + various [configuration template variables](/docs/templates/engine.html) available. By default, + Packer uses the following command (broken across multiple lines for readability) to execute Puppet: + +``` +{{.FacterVars}} {{if .Sudo}}sudo -E {{end}} +{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}puppet agent +--onetime --no-daemonize +{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}} +{{if ne .Options ""}}{{.Options}} {{end}} +{{if ne .PuppetNode ""}}--certname={{.PuppetNode}} {{end}} +{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}} +{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}} +--detailed-exitcodes +``` + +The following command is used if guest OS type is windows: + +``` +{{.FacterVars}} +{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}puppet agent +--onetime --no-daemonize +{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}} +{{if ne .Options ""}}{{.Options}} {{end}} +{{if ne .PuppetNode ""}}--certname={{.PuppetNode}} {{end}} +{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}} +{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}} +--detailed-exitcodes ``` ## Default Facts