diff --git a/builder/vmware/common/driver_config.go b/builder/vmware/common/driver_config.go index 6367b1734..544aefef5 100644 --- a/builder/vmware/common/driver_config.go +++ b/builder/vmware/common/driver_config.go @@ -1,22 +1,30 @@ package common import ( + "bytes" + "context" + "fmt" + "net/url" "os" + "os/exec" + "strings" + "time" "github.com/hashicorp/packer/template/interpolate" ) type DriverConfig struct { - FusionAppPath string `mapstructure:"fusion_app_path"` - RemoteType string `mapstructure:"remote_type"` - RemoteDatastore string `mapstructure:"remote_datastore"` - RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"` - RemoteCacheDirectory string `mapstructure:"remote_cache_directory"` - RemoteHost string `mapstructure:"remote_host"` - RemotePort uint `mapstructure:"remote_port"` - RemoteUser string `mapstructure:"remote_username"` - RemotePassword string `mapstructure:"remote_password"` - RemotePrivateKey string `mapstructure:"remote_private_key_file"` + FusionAppPath string `mapstructure:"fusion_app_path"` + RemoteType string `mapstructure:"remote_type"` + RemoteDatastore string `mapstructure:"remote_datastore"` + RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"` + RemoteCacheDirectory string `mapstructure:"remote_cache_directory"` + RemoteHost string `mapstructure:"remote_host"` + RemotePort uint `mapstructure:"remote_port"` + RemoteUser string `mapstructure:"remote_username"` + RemotePassword string `mapstructure:"remote_password"` + RemotePrivateKey string `mapstructure:"remote_private_key_file"` + SkipValidateCredentials bool `mapstructure:"skip_validate_credentials"` } func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error { @@ -44,3 +52,50 @@ func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error { return nil } + +func (c *DriverConfig) Validate(SkipExport bool) error { + if c.RemoteType == "esx5" && SkipExport != true { + if c.RemotePassword == "" { + return fmt.Errorf("exporting the vm (with ovftool) requires that " + + "you set a value for remote_password") + } else if !c.SkipValidateCredentials { + // check that password is valid by sending a dummy ovftool command + // now, so that we don't fail for a simple mistake after a long + // build + ovftool := GetOVFTool() + ovfToolArgs := []string{"--verifyOnly", fmt.Sprintf("vi://" + + url.QueryEscape(c.RemoteUser) + ":" + + url.QueryEscape(c.RemotePassword) + "@" + + c.RemoteHost)} + + var out bytes.Buffer + cmdCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + cmd := exec.CommandContext(cmdCtx, ovftool, ovfToolArgs...) + cmd.Stdout = &out + + // Need to manually close stdin or else the ofvtool call will hang + // forever in a situation where the user has provided an invalid + // password or username + stdin, _ := cmd.StdinPipe() + defer stdin.Close() + + if err := cmd.Run(); err != nil { + outString := out.String() + // The command *should* fail with this error, if it + // authenticates properly. + if !strings.Contains(outString, "Found wrong kind of object") { + err := fmt.Errorf("ovftool validation error: %s; %s", + err, outString) + if strings.Contains(outString, + "Enter login information for source") { + err = fmt.Errorf("The username or password you " + + "provided to ovftool is invalid.") + } + return err + } + } + } + } + return nil +} diff --git a/builder/vmware/common/step_export.go b/builder/vmware/common/step_export.go index a475c7cd3..8de3140cd 100644 --- a/builder/vmware/common/step_export.go +++ b/builder/vmware/common/step_export.go @@ -26,6 +26,18 @@ type StepExport struct { OutputDir string } +func GetOVFTool() string { + ovftool := "ovftool" + if runtime.GOOS == "windows" { + ovftool = "ovftool.exe" + } + + if _, err := exec.LookPath(ovftool); err != nil { + return "" + } + return ovftool +} + func (s *StepExport) generateArgs(c *DriverConfig, displayName string, hidePassword bool) []string { password := url.QueryEscape(c.RemotePassword) if hidePassword { @@ -57,13 +69,9 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep. return multistep.ActionContinue } - ovftool := "ovftool" - if runtime.GOOS == "windows" { - ovftool = "ovftool.exe" - } - - if _, err := exec.LookPath(ovftool); err != nil { - err = fmt.Errorf("Error %s not found: %s", ovftool, err) + ovftool := GetOVFTool() + if ovftool == "" { + err := fmt.Errorf("Error %s not found: ", ovftool) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt diff --git a/builder/vmware/iso/builder.go b/builder/vmware/iso/builder.go index 4fa88c5b8..bbc09e561 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -3,7 +3,6 @@ package iso import ( "errors" "fmt" - "io/ioutil" "log" "os" "strings" @@ -11,12 +10,9 @@ import ( vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/common" - "github.com/hashicorp/packer/common/bootcommand" "github.com/hashicorp/packer/helper/communicator" - "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" - "github.com/hashicorp/packer/template/interpolate" ) type Builder struct { @@ -24,139 +20,14 @@ type Builder struct { runner multistep.Runner } -type Config struct { - common.PackerConfig `mapstructure:",squash"` - common.HTTPConfig `mapstructure:",squash"` - common.ISOConfig `mapstructure:",squash"` - common.FloppyConfig `mapstructure:",squash"` - bootcommand.VNCConfig `mapstructure:",squash"` - vmwcommon.DriverConfig `mapstructure:",squash"` - vmwcommon.OutputConfig `mapstructure:",squash"` - vmwcommon.RunConfig `mapstructure:",squash"` - vmwcommon.ShutdownConfig `mapstructure:",squash"` - vmwcommon.SSHConfig `mapstructure:",squash"` - vmwcommon.ToolsConfig `mapstructure:",squash"` - vmwcommon.VMXConfig `mapstructure:",squash"` - vmwcommon.ExportConfig `mapstructure:",squash"` - - // disk drives - AdditionalDiskSize []uint `mapstructure:"disk_additional_size"` - DiskAdapterType string `mapstructure:"disk_adapter_type"` - DiskName string `mapstructure:"vmdk_name"` - DiskSize uint `mapstructure:"disk_size"` - DiskTypeId string `mapstructure:"disk_type_id"` - Format string `mapstructure:"format"` - - // cdrom drive - CdromAdapterType string `mapstructure:"cdrom_adapter_type"` - - // platform information - GuestOSType string `mapstructure:"guest_os_type"` - Version string `mapstructure:"version"` - VMName string `mapstructure:"vm_name"` - - // Network adapter and type - NetworkAdapterType string `mapstructure:"network_adapter_type"` - Network string `mapstructure:"network"` - - // device presence - Sound bool `mapstructure:"sound"` - USB bool `mapstructure:"usb"` - - // communication ports - Serial string `mapstructure:"serial"` - Parallel string `mapstructure:"parallel"` - - VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"` - VMXTemplatePath string `mapstructure:"vmx_template_path"` - - ctx interpolate.Context -} - +// Prepare processes the build configuration parameters. func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { - err := config.Decode(&b.config, &config.DecodeOpts{ - Interpolate: true, - InterpolateContext: &b.config.ctx, - InterpolateFilter: &interpolate.RenderFilter{ - Exclude: []string{ - "boot_command", - "tools_upload_path", - }, - }, - }, raws...) - if err != nil { - return nil, err - } - - // Accumulate any errors and warnings - var errs *packer.MultiError - warnings := make([]string, 0) - - isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx) - warnings = append(warnings, isoWarnings...) - errs = packer.MultiErrorAppend(errs, isoErrs...) - errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.DriverConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, - b.config.OutputConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)...) - errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.ToolsConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.VMXConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.ExportConfig.Prepare(&b.config.ctx)...) - - if b.config.DiskName == "" { - b.config.DiskName = "disk" - } - - if b.config.DiskSize == 0 { - b.config.DiskSize = 40000 - } - - if b.config.DiskAdapterType == "" { - // Default is lsilogic - b.config.DiskAdapterType = "lsilogic" - } - - if !b.config.SkipCompaction { - if b.config.RemoteType == "esx5" { - if b.config.DiskTypeId == "" { - b.config.SkipCompaction = true - } - } - } - - if b.config.DiskTypeId == "" { - // Default is growable virtual disk split in 2GB files. - b.config.DiskTypeId = "1" - - if b.config.RemoteType == "esx5" { - b.config.DiskTypeId = "zeroedthick" - } - } - - if b.config.RemoteType == "esx5" { - if b.config.DiskTypeId != "thin" && !b.config.SkipCompaction { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("skip_compaction must be 'true' for disk_type_id: %s", b.config.DiskTypeId)) - } - } - - if b.config.GuestOSType == "" { - b.config.GuestOSType = "other" - } - - if b.config.VMName == "" { - b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) - } - - if b.config.Version == "" { - b.config.Version = "9" + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs } + b.config = *c if b.config.VMXTemplatePath != "" { if err := b.validateVMXTemplatePath(); err != nil { errs = packer.MultiErrorAppend( @@ -170,62 +41,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } } - if b.config.Network == "" { - b.config.Network = "nat" - } - - if !b.config.Sound { - b.config.Sound = false - } - - if !b.config.USB { - b.config.USB = false - } - - // Remote configuration validation - if b.config.RemoteType != "" { - if b.config.RemoteHost == "" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("remote_host must be specified")) - } - - if b.config.RemoteType != "esx5" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("Only 'esx5' value is accepted for remote_type")) - } - } - - if b.config.Format == "" { - b.config.Format = "ovf" - } - - if !(b.config.Format == "ova" || b.config.Format == "ovf" || b.config.Format == "vmx") { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("format must be one of ova, ovf, or vmx")) - } - - if b.config.RemoteType == "esx5" && b.config.SkipExport != true && b.config.RemotePassword == "" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("exporting the vm (with ovftool) requires that you set a value for remote_password")) - } - - // Warnings - if b.config.ShutdownCommand == "" { - warnings = append(warnings, - "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ - "will forcibly halt the virtual machine, which may result in data loss.") - } - - if b.config.Headless && b.config.DisableVNC { - warnings = append(warnings, - "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ - "you won't be able to see any output.") - } - - if errs != nil && len(errs.Errors) > 0 { - return warnings, errs - } - return warnings, nil } diff --git a/builder/vmware/iso/builder_test.go b/builder/vmware/iso/builder_test.go index cd96488d0..1607f1cbb 100644 --- a/builder/vmware/iso/builder_test.go +++ b/builder/vmware/iso/builder_test.go @@ -146,6 +146,7 @@ func TestBuilderPrepare_RemoteType(t *testing.T) { config["format"] = "ovf" config["remote_host"] = "foobar.example.com" config["remote_password"] = "supersecret" + config["skip_validate_credentials"] = true // Bad config["remote_type"] = "foobar" warns, err := b.Prepare(config) @@ -202,6 +203,7 @@ func TestBuilderPrepare_RemoteExport(t *testing.T) { config["remote_type"] = "esx5" config["remote_host"] = "foobar.example.com" + config["skip_validate_credentials"] = true // Bad config["remote_password"] = "" warns, err := b.Prepare(config) diff --git a/builder/vmware/iso/config.go b/builder/vmware/iso/config.go new file mode 100644 index 000000000..e55a31dc2 --- /dev/null +++ b/builder/vmware/iso/config.go @@ -0,0 +1,271 @@ +package iso + +import ( + "fmt" + "io/ioutil" + "os" + + vmwcommon "github.com/hashicorp/packer/builder/vmware/common" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + common.HTTPConfig `mapstructure:",squash"` + common.ISOConfig `mapstructure:",squash"` + common.FloppyConfig `mapstructure:",squash"` + bootcommand.VNCConfig `mapstructure:",squash"` + vmwcommon.DriverConfig `mapstructure:",squash"` + vmwcommon.OutputConfig `mapstructure:",squash"` + vmwcommon.RunConfig `mapstructure:",squash"` + vmwcommon.ShutdownConfig `mapstructure:",squash"` + vmwcommon.SSHConfig `mapstructure:",squash"` + vmwcommon.ToolsConfig `mapstructure:",squash"` + vmwcommon.VMXConfig `mapstructure:",squash"` + vmwcommon.ExportConfig `mapstructure:",squash"` + + // disk drives + AdditionalDiskSize []uint `mapstructure:"disk_additional_size"` + DiskAdapterType string `mapstructure:"disk_adapter_type"` + DiskName string `mapstructure:"vmdk_name"` + DiskSize uint `mapstructure:"disk_size"` + DiskTypeId string `mapstructure:"disk_type_id"` + Format string `mapstructure:"format"` + + // cdrom drive + CdromAdapterType string `mapstructure:"cdrom_adapter_type"` + + // platform information + GuestOSType string `mapstructure:"guest_os_type"` + Version string `mapstructure:"version"` + VMName string `mapstructure:"vm_name"` + + // Network adapter and type + NetworkAdapterType string `mapstructure:"network_adapter_type"` + Network string `mapstructure:"network"` + + // device presence + Sound bool `mapstructure:"sound"` + USB bool `mapstructure:"usb"` + + // communication ports + Serial string `mapstructure:"serial"` + Parallel string `mapstructure:"parallel"` + + VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"` + VMXTemplatePath string `mapstructure:"vmx_template_path"` + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + err := config.Decode(c, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "boot_command", + "tools_upload_path", + }, + }, + }, raws...) + if err != nil { + return nil, nil, err + } + + // Accumulate any errors and warnings + var errs *packer.MultiError + warnings := make([]string, 0) + + isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx) + warnings = append(warnings, isoWarnings...) + errs = packer.MultiErrorAppend(errs, isoErrs...) + errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, + c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...) + errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...) + + if c.DiskName == "" { + c.DiskName = "disk" + } + + if c.DiskSize == 0 { + c.DiskSize = 40000 + } + + if c.DiskAdapterType == "" { + // Default is lsilogic + c.DiskAdapterType = "lsilogic" + } + + if !c.SkipCompaction { + if c.RemoteType == "esx5" { + if c.DiskTypeId == "" { + c.SkipCompaction = true + } + } + } + + if c.DiskTypeId == "" { + // Default is growable virtual disk split in 2GB files. + c.DiskTypeId = "1" + + if c.RemoteType == "esx5" { + c.DiskTypeId = "zeroedthick" + } + } + + if c.RemoteType == "esx5" { + if c.DiskTypeId != "thin" && !c.SkipCompaction { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("skip_compaction must be 'true' for disk_type_id: %s", c.DiskTypeId)) + } + } + + if c.GuestOSType == "" { + c.GuestOSType = "other" + } + + if c.VMName == "" { + c.VMName = fmt.Sprintf("packer-%s", c.PackerBuildName) + } + + if c.Version == "" { + c.Version = "9" + } + + if c.VMXTemplatePath != "" { + if err := c.validateVMXTemplatePath(); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("vmx_template_path is invalid: %s", err)) + } + } else { + warn := c.checkForVMXTemplateAndVMXDataCollisions() + if warn != "" { + warnings = append(warnings, warn) + } + } + + if c.Network == "" { + c.Network = "nat" + } + + if !c.Sound { + c.Sound = false + } + + if !c.USB { + c.USB = false + } + + // Remote configuration validation + if c.RemoteType != "" { + if c.RemoteHost == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("remote_host must be specified")) + } + + if c.RemoteType != "esx5" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Only 'esx5' value is accepted for remote_type")) + } + } + + if c.Format == "" { + c.Format = "ovf" + } + + if !(c.Format == "ova" || c.Format == "ovf" || c.Format == "vmx") { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("format must be one of ova, ovf, or vmx")) + } + + err = c.DriverConfig.Validate(c.SkipExport) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + // Warnings + if c.ShutdownCommand == "" { + warnings = append(warnings, + "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ + "will forcibly halt the virtual machine, which may result in data loss.") + } + + if c.Headless && c.DisableVNC { + warnings = append(warnings, + "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ + "you won't be able to see any output.") + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, warnings, errs + } + + return c, warnings, nil +} + +// Validate the vmx_data option against the default vmx template to warn +// user if anything is being overridden. +func (c *Config) checkForVMXTemplateAndVMXDataCollisions() string { + if c.VMXTemplatePath != "" { + return "" + } + + var overridden []string + tplLines := strings.Split(DefaultVMXTemplate, "\n") + tplLines = append(tplLines, + fmt.Sprintf("%s0:0.present", strings.ToLower(c.DiskAdapterType)), + fmt.Sprintf("%s0:0.fileName", strings.ToLower(c.DiskAdapterType)), + fmt.Sprintf("%s0:0.deviceType", strings.ToLower(c.DiskAdapterType)), + fmt.Sprintf("%s0:1.present", strings.ToLower(c.DiskAdapterType)), + fmt.Sprintf("%s0:1.fileName", strings.ToLower(c.DiskAdapterType)), + fmt.Sprintf("%s0:1.deviceType", strings.ToLower(c.DiskAdapterType)), + ) + + for _, line := range tplLines { + if strings.Contains(line, `{{`) { + key := line[:strings.Index(line, " =")] + if _, ok := c.VMXData[key]; ok { + overridden = append(overridden, key) + } + } + } + + if len(overridden) > 0 { + warnings := fmt.Sprintf("Your vmx data contains the following "+ + "variable(s), which Packer normally sets when it generates its "+ + "own default vmx template. This may cause your build to fail or "+ + "behave unpredictably: %s", strings.Join(overridden, ", ")) + return warnings + } + return "" +} + +func (c *Config) validateVMXTemplatePath() error { + f, err := os.Open(c.VMXTemplatePath) + if err != nil { + return err + } + defer f.Close() + + data, err := ioutil.ReadAll(f) + if err != nil { + return err + } + + return interpolate.Validate(string(data), &c.ctx) +} diff --git a/builder/vmware/vmx/config.go b/builder/vmware/vmx/config.go index 38a252787..94235e27e 100644 --- a/builder/vmware/vmx/config.go +++ b/builder/vmware/vmx/config.go @@ -93,6 +93,11 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } } + err = c.DriverConfig.Validate(c.SkipExport) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + if c.Format == "" { c.Format = "ovf" }