Merge pull request #6448 from DanHam/fix-esxi-displayName

ESXi: Fix failure to discover IP or export VM when 'displayName' differs from 'vm_name'
This commit is contained in:
M. Marsh 2018-07-10 16:57:16 -07:00 committed by GitHub
commit 8e852af1eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 12 deletions

View File

@ -16,6 +16,9 @@ import (
// //
// Uses: // Uses:
// vmx_path string // vmx_path string
//
// Produces:
// display_name string - Value of the displayName key set in the VMX file
type StepConfigureVMX struct { type StepConfigureVMX struct {
CustomData map[string]string CustomData map[string]string
SkipFloppy bool SkipFloppy bool
@ -73,6 +76,19 @@ func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionHalt return multistep.ActionHalt
} }
// If the build is taking place on a remote ESX server, the displayName
// will be needed for discovery of the VM's IP address and for export
// of the VM. The displayName key should always be set in the VMX file,
// so error if we don't find it
if displayName, ok := vmxData["displayname"]; !ok { // Packer converts key names to lowercase!
err := fmt.Errorf("Error: Could not get value of displayName from VMX data")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else {
state.Put("display_name", displayName)
}
return multistep.ActionContinue return multistep.ActionContinue
} }

View File

@ -14,6 +14,9 @@ func testVMXFile(t *testing.T) string {
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
// displayName must always be set
err = WriteVMX(tf.Name(), map[string]string{"displayName": "PackerBuild"})
tf.Close() tf.Close()
return tf.Name() return tf.Name()
@ -132,12 +135,29 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
vmxPath := testVMXFile(t) vmxPath := testVMXFile(t)
defer os.Remove(vmxPath) defer os.Remove(vmxPath)
err := WriteVMX(vmxPath, map[string]string{ additionalTestVmxData := []struct {
"foo": "bar", Key string
"ethernet0.generatedAddress": "foo", Value string
"ethernet1.generatedAddress": "foo", }{
"ethernet1.generatedAddressOffset": "foo", {"foo", "bar"},
}) {"ethernet0.generatedaddress", "foo"},
{"ethernet1.generatedaddress", "foo"},
{"ethernet1.generatedaddressoffset", "foo"},
}
// Get any existing VMX data from the VMX file
vmxData, err := ReadVMX(vmxPath)
if err != nil {
t.Fatalf("err %s", err)
}
// Add the additional key/value pairs we need for this test to the existing VMX data
for _, data := range additionalTestVmxData {
vmxData[data.Key] = data.Value
}
// Recreate the VMX file so it includes all the data needed for this test
err = WriteVMX(vmxPath, vmxData)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -157,7 +177,7 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
vmxData := ParseVMX(string(vmxContents)) vmxData = ParseVMX(string(vmxContents))
cases := []struct { cases := []struct {
Key string Key string
@ -180,5 +200,71 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
} }
} }
} }
}
// Should fail if the displayName key is not found in the VMX
func TestStepConfigureVMX_displayNameMissing(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
// testVMXFile adds displayName key/value pair to the VMX
vmxPath := testVMXFile(t)
defer os.Remove(vmxPath)
// Bad: Delete displayName from the VMX/Create an empty VMX file
err := WriteVMX(vmxPath, map[string]string{})
if err != nil {
t.Fatalf("err: %s", err)
}
state.Put("vmx_path", vmxPath)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v. Should halt when displayName key is missing from VMX", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should store error in state when displayName key is missing from VMX")
}
}
// Should store the value of displayName in the statebag
func TestStepConfigureVMX_displayNameStore(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
// testVMXFile adds displayName key/value pair to the VMX
vmxPath := testVMXFile(t)
defer os.Remove(vmxPath)
state.Put("vmx_path", vmxPath)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// The value of displayName must be stored in the statebag
if _, ok := state.GetOk("display_name"); !ok {
t.Fatalf("displayName should be stored in the statebag as 'display_name'")
}
}
func TestStepConfigureVMX_vmxPathBad(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
state.Put("vmx_path", "some_bad_path")
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v. Should halt when vmxPath is bad", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should store error in state when vmxPath is bad")
}
} }

View File

@ -389,7 +389,13 @@ func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
return "", err return "", err
} }
record, err := r.find("Name", config.VMName) // The value in the Name field returned by 'esxcli network vm list'
// corresponds directly to the value of displayName set in the VMX file
var displayName string
if v, ok := state.GetOk("display_name"); ok {
displayName = v.(string)
}
record, err := r.find("Name", displayName)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -14,13 +14,17 @@ import (
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
// This step exports a VM built on ESXi using ovftool
//
// Uses:
// display_name string
type StepExport struct { type StepExport struct {
Format string Format string
SkipExport bool SkipExport bool
OutputDir string OutputDir string
} }
func (s *StepExport) generateArgs(c *Config, hidePassword bool) []string { func (s *StepExport) generateArgs(c *Config, displayName string, hidePassword bool) []string {
password := url.QueryEscape(c.RemotePassword) password := url.QueryEscape(c.RemotePassword)
if hidePassword { if hidePassword {
password = "****" password = "****"
@ -29,7 +33,7 @@ func (s *StepExport) generateArgs(c *Config, hidePassword bool) []string {
"--noSSLVerify=true", "--noSSLVerify=true",
"--skipManifestCheck", "--skipManifestCheck",
"-tt=" + s.Format, "-tt=" + s.Format,
"vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + c.VMName, "vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + displayName,
s.OutputDir, s.OutputDir,
} }
return append(c.OVFToolOptions, args...) return append(c.OVFToolOptions, args...)
@ -72,9 +76,13 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.
} }
ui.Say("Exporting virtual machine...") ui.Say("Exporting virtual machine...")
ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, true), " "))) var displayName string
if v, ok := state.GetOk("display_name"); ok {
displayName = v.(string)
}
ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, displayName, true), " ")))
var out bytes.Buffer var out bytes.Buffer
cmd := exec.Command(ovftool, s.generateArgs(c, false)...) cmd := exec.Command(ovftool, s.generateArgs(c, displayName, false)...)
cmd.Stdout = &out cmd.Stdout = &out
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
err := fmt.Errorf("Error exporting virtual machine: %s\n%s\n", err, out.String()) err := fmt.Errorf("Error exporting virtual machine: %s\n%s\n", err, out.String())