packer-cn/builder/vmware/iso/step_create_vmx.go
Megan Marsh d2dd158fe2 Implement cd_files option. The Addfile code is modified from the floppy_files
tooling. Tested on OSX, `Windows, and Linux. Documented command line dependencies.
Added acceptance tests (behind testacc flag, since there are dependencies).
This option is only implemented for the vmware and hyperv iso builders. It will be
implemented for others in later PRs in order to make review easier.
2020-09-04 12:20:19 -07:00

616 lines
18 KiB
Go

package iso
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/packer/tmp"
"github.com/hashicorp/packer/template/interpolate"
)
type vmxTemplateData struct {
Name string
GuestOS string
ISOPath string
Version string
CpuCount string
MemorySize string
SCSI_Present string
SCSI_diskAdapterType string
SATA_Present string
NVME_Present string
DiskName string
DiskType string
CDROMType string
CDROMType_PrimarySecondary string
CDROM_PATH string
Network_Type string
Network_Device string
Network_Adapter string
Network_Name string
Sound_Present string
Usb_Present string
Serial_Present string
Serial_Type string
Serial_Endpoint string
Serial_Host string
Serial_Yield string
Serial_Filename string
Serial_Auto string
Parallel_Present string
Parallel_Bidirectional string
Parallel_Filename string
Parallel_Auto string
}
type additionalDiskTemplateData struct {
DiskNumber int
DiskName string
}
// This step creates the VMX file for the VM.
//
// Uses:
// config *config
// iso_path string
// ui packer.Ui
//
// Produces:
// vmx_path string - The path to the VMX file.
type stepCreateVMX struct {
tempDir string
}
/* regular steps */
func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packer.Ui)
// Convert the iso_path into a path relative to the .vmx file if possible
if relativeIsoPath, err := filepath.Rel(config.VMXTemplatePath, filepath.FromSlash(isoPath)); err == nil {
isoPath = relativeIsoPath
}
ui.Say("Building and writing VMX file")
vmxTemplate := DefaultVMXTemplate
if config.VMXTemplatePath != "" {
f, err := os.Open(config.VMXTemplatePath)
if err != nil {
err := fmt.Errorf("Error reading VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer f.Close()
rawBytes, err := ioutil.ReadAll(f)
if err != nil {
err := fmt.Errorf("Error reading VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxTemplate = string(rawBytes)
}
ictx := config.ctx
// Mount extra vmdks we created earlier.
if len(config.AdditionalDiskSize) > 0 {
for i := range config.AdditionalDiskSize {
ictx.Data = &additionalDiskTemplateData{
DiskNumber: i + 1,
DiskName: config.DiskName,
}
diskTemplate := DefaultAdditionalDiskTemplate
if config.VMXDiskTemplatePath != "" {
f, err := os.Open(config.VMXDiskTemplatePath)
if err != nil {
err := fmt.Errorf("Error reading VMX disk template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer f.Close()
rawBytes, err := ioutil.ReadAll(f)
if err != nil {
err := fmt.Errorf("Error reading VMX disk template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
diskTemplate = string(rawBytes)
}
diskContents, err := interpolate.Render(diskTemplate, &ictx)
if err != nil {
err := fmt.Errorf("Error preparing VMX template for additional disk: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxTemplate += diskContents
}
}
templateData := vmxTemplateData{
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
Version: config.Version,
ISOPath: isoPath,
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "ide",
CDROMType_PrimarySecondary: "0",
Network_Adapter: "e1000",
Sound_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.HWConfig.Sound)],
Usb_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.HWConfig.USB)],
Serial_Present: "FALSE",
Parallel_Present: "FALSE",
}
/// Use the disk adapter type that the user specified to tweak the .vmx
// Also sync the cdrom adapter type according to what's common for that disk type.
// XXX: If the cdrom type is modified, make sure to update common/step_clean_vmx.go
// so that it will regex the correct cdrom device for removal.
diskAdapterType := strings.ToLower(config.DiskAdapterType)
switch diskAdapterType {
case "ide":
templateData.DiskType = "ide"
templateData.CDROMType = "ide"
templateData.CDROMType_PrimarySecondary = "1"
case "sata":
templateData.SATA_Present = "TRUE"
templateData.DiskType = "sata"
templateData.CDROMType = "sata"
templateData.CDROMType_PrimarySecondary = "1"
case "nvme":
templateData.NVME_Present = "TRUE"
templateData.DiskType = "nvme"
templateData.SATA_Present = "TRUE"
templateData.CDROMType = "sata"
templateData.CDROMType_PrimarySecondary = "0"
case "scsi":
diskAdapterType = "lsilogic"
fallthrough
default:
templateData.SCSI_Present = "TRUE"
templateData.SCSI_diskAdapterType = diskAdapterType
templateData.DiskType = "scsi"
templateData.CDROMType = "ide"
templateData.CDROMType_PrimarySecondary = "0"
}
/// Handle the cdrom adapter type. If the disk adapter type and the
// cdrom adapter type are the same, then ensure that the cdrom is the
// secondary device on whatever bus the disk adapter is on.
cdromAdapterType := strings.ToLower(config.CdromAdapterType)
if cdromAdapterType == "" {
cdromAdapterType = templateData.CDROMType
} else if cdromAdapterType == diskAdapterType {
templateData.CDROMType_PrimarySecondary = "1"
} else {
templateData.CDROMType_PrimarySecondary = "0"
}
switch cdromAdapterType {
case "ide":
templateData.CDROMType = "ide"
case "sata":
templateData.SATA_Present = "TRUE"
templateData.CDROMType = "sata"
case "scsi":
templateData.SCSI_Present = "TRUE"
templateData.CDROMType = "scsi"
default:
err := fmt.Errorf("Error processing VMX template: %s", cdromAdapterType)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Add our custom CD, if it exists
cd_path, ok := state.Get("cd_path").(string)
if ok {
if cd_path != "" {
vmxTemplate += ExtraCDRomTemplate
templateData.CDROM_PATH = cd_path
}
}
/// Now that we figured out the CDROM device to add, store it
/// to the list of temporary build devices in our statebag
tmpBuildDevices := state.Get("temporaryDevices").([]string)
tmpCdromDevice := fmt.Sprintf("%s0:%s", templateData.CDROMType, templateData.CDROMType_PrimarySecondary)
tmpBuildDevices = append(tmpBuildDevices, tmpCdromDevice)
state.Put("temporaryDevices", tmpBuildDevices)
/// Assign the network adapter type into the template if one was specified.
network_adapter := strings.ToLower(config.HWConfig.NetworkAdapterType)
if network_adapter != "" {
templateData.Network_Adapter = network_adapter
}
/// Check the network type that the user specified
network := config.HWConfig.Network
if config.HWConfig.NetworkName != "" {
templateData.Network_Name = config.HWConfig.NetworkName
}
driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver()
// check to see if the driver implements a network mapper for mapping
// the network-type to its device-name.
if driver.NetworkMapper != nil {
// read network map configuration into a NetworkNameMapper.
netmap, err := driver.NetworkMapper()
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// try and convert the specified network to a device.
devices, err := netmap.NameIntoDevices(network)
if err == nil && len(devices) > 0 {
// If multiple devices exist, for example for network "nat", VMware chooses
// the actual device. Only type "custom" allows the exact choice of a
// specific virtual network (see below). We allow VMware to choose the device
// and for device-specific operations like GuestIP, try to go over all
// devices that match a name (e.g. "nat").
// https://pubs.vmware.com/workstation-9/index.jsp?topic=%2Fcom.vmware.ws.using.doc%2FGUID-3B504F2F-7A0B-415F-AE01-62363A95D052.html
templateData.Network_Type = network
templateData.Network_Device = ""
} else {
// otherwise, we were unable to find the type, so assume it's a custom device
templateData.Network_Type = "custom"
templateData.Network_Device = network
}
// if NetworkMapper is nil, then we're using something like ESX, so fall
// back to the previous logic of using "nat" despite it not mattering to ESX.
} else {
templateData.Network_Type = "nat"
templateData.Network_Device = network
network = "nat"
}
// store the network so that we can later figure out what ip address to bind to
state.Put("vmnetwork", network)
/// check if serial port has been configured
if !config.HWConfig.HasSerial() {
templateData.Serial_Present = "FALSE"
} else {
// FIXME
serial, err := config.HWConfig.ReadSerial()
if err != nil {
err := fmt.Errorf("Error processing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
templateData.Serial_Present = "TRUE"
templateData.Serial_Filename = ""
templateData.Serial_Yield = ""
templateData.Serial_Endpoint = ""
templateData.Serial_Host = ""
templateData.Serial_Auto = "FALSE"
// Set the number of cpus if it was specified
if config.HWConfig.CpuCount > 0 {
templateData.CpuCount = strconv.Itoa(config.HWConfig.CpuCount)
}
// Apply the memory size that was specified
if config.HWConfig.MemorySize > 0 {
templateData.MemorySize = strconv.Itoa(config.HWConfig.MemorySize)
} else {
templateData.MemorySize = "512"
}
switch serial.Union.(type) {
case *vmwcommon.SerialConfigPipe:
templateData.Serial_Type = "pipe"
templateData.Serial_Endpoint = serial.Pipe.Endpoint
templateData.Serial_Host = serial.Pipe.Host
templateData.Serial_Yield = serial.Pipe.Yield
templateData.Serial_Filename = filepath.FromSlash(serial.Pipe.Filename)
case *vmwcommon.SerialConfigFile:
templateData.Serial_Type = "file"
templateData.Serial_Filename = filepath.FromSlash(serial.File.Filename)
case *vmwcommon.SerialConfigDevice:
templateData.Serial_Type = "device"
templateData.Serial_Filename = filepath.FromSlash(serial.Device.Devicename)
case *vmwcommon.SerialConfigAuto:
templateData.Serial_Type = "device"
templateData.Serial_Filename = filepath.FromSlash(serial.Auto.Devicename)
templateData.Serial_Yield = serial.Auto.Yield
templateData.Serial_Auto = "TRUE"
case nil:
templateData.Serial_Present = "FALSE"
break
default:
err := fmt.Errorf("Error processing VMX template: %v", serial)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
/// check if parallel port has been configured
if !config.HWConfig.HasParallel() {
templateData.Parallel_Present = "FALSE"
} else {
// FIXME
parallel, err := config.HWConfig.ReadParallel()
if err != nil {
err := fmt.Errorf("Error processing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
templateData.Parallel_Auto = "FALSE"
switch parallel.Union.(type) {
case *vmwcommon.ParallelPortFile:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Filename = filepath.FromSlash(parallel.File.Filename)
case *vmwcommon.ParallelPortDevice:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Bidirectional = parallel.Device.Bidirectional
templateData.Parallel_Filename = filepath.FromSlash(parallel.Device.Devicename)
case *vmwcommon.ParallelPortAuto:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Auto = "TRUE"
templateData.Parallel_Bidirectional = parallel.Auto.Bidirectional
case nil:
templateData.Parallel_Present = "FALSE"
break
default:
err := fmt.Errorf("Error processing VMX template: %v", parallel)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
ictx.Data = &templateData
/// render the .vmx template
vmxContents, err := interpolate.Render(vmxTemplate, &ictx)
if err != nil {
err := fmt.Errorf("Error processing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxDir := config.OutputDir
if config.RemoteType != "" {
// For remote builds, we just put the VMX in a temporary
// directory since it just gets uploaded anyways.
vmxDir, err = tmp.Dir("vmw-iso")
if err != nil {
err := fmt.Errorf("Error preparing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the tempDir so we clean it up
s.tempDir = vmxDir
}
/// Now to handle options that will modify the template without using "vmxTemplateData"
vmxData := vmwcommon.ParseVMX(vmxContents)
// If no cpus were specified, then remove the entry to use the default
if vmxData["numvcpus"] == "" {
delete(vmxData, "numvcpus")
}
// If some number of cores were specified, then update "cpuid.coresPerSocket" with the requested value
if config.HWConfig.CoreCount > 0 {
vmxData["cpuid.corespersocket"] = strconv.Itoa(config.HWConfig.CoreCount)
}
/// Write the vmxData to the vmxPath
vmxPath := filepath.Join(vmxDir, config.VMName+".vmx")
if err := vmwcommon.WriteVMX(vmxPath, vmxData); err != nil {
err := fmt.Errorf("Error creating VMX file: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("vmx_path", vmxPath)
return multistep.ActionContinue
}
func (s *stepCreateVMX) Cleanup(multistep.StateBag) {
if s.tempDir != "" {
os.RemoveAll(s.tempDir)
}
}
// This is the default VMX template used if no other template is given.
// This is hardcoded here. If you wish to use a custom template please
// do so by specifying in the builder configuration.
const DefaultVMXTemplate = `
.encoding = "UTF-8"
displayName = "{{ .Name }}"
// Hardware
numvcpus = "{{ .CpuCount }}"
memsize = "{{ .MemorySize }}"
config.version = "8"
virtualHW.productCompatibility = "hosted"
virtualHW.version = "{{ .Version }}"
// Bootup
nvram = "{{ .Name }}.nvram"
floppy0.present = "FALSE"
bios.bootOrder = "hdd,cdrom"
// Configuration
extendedConfigFile = "{{ .Name }}.vmxf"
gui.fullScreenAtPowerOn = "FALSE"
gui.viewModeAtPowerOn = "windowed"
hgfs.linkRootShare = "TRUE"
hgfs.mapRootShare = "TRUE"
isolation.tools.hgfs.disable = "FALSE"
proxyApps.publishToHost = "FALSE"
replay.filename = ""
replay.supported = "FALSE"
checkpoint.vmState = ""
vmotion.checkpointFBSize = "65536000"
// Power control
cleanShutdown = "TRUE"
powerType.powerOff = "soft"
powerType.powerOn = "soft"
powerType.reset = "soft"
powerType.suspend = "soft"
// Tools
guestOS = "{{ .GuestOS }}"
tools.syncTime = "TRUE"
tools.upgrade.policy = "upgradeAtPowerCycle"
// Bus
pciBridge0.pciSlotNumber = "17"
pciBridge0.present = "TRUE"
pciBridge4.functions = "8"
pciBridge4.pciSlotNumber = "21"
pciBridge4.present = "TRUE"
pciBridge4.virtualDev = "pcieRootPort"
pciBridge5.functions = "8"
pciBridge5.pciSlotNumber = "22"
pciBridge5.present = "TRUE"
pciBridge5.virtualDev = "pcieRootPort"
pciBridge6.functions = "8"
pciBridge6.pciSlotNumber = "23"
pciBridge6.present = "TRUE"
pciBridge6.virtualDev = "pcieRootPort"
pciBridge7.functions = "8"
pciBridge7.pciSlotNumber = "24"
pciBridge7.present = "TRUE"
pciBridge7.virtualDev = "pcieRootPort"
ehci.present = "TRUE"
ehci.pciSlotNumber = "34"
vmci0.present = "TRUE"
vmci0.id = "1861462627"
vmci0.pciSlotNumber = "35"
// Network Adapter
ethernet0.addressType = "generated"
ethernet0.bsdName = "en0"
ethernet0.connectionType = "{{ .Network_Type }}"
ethernet0.vnet = "{{ .Network_Device }}"
ethernet0.displayName = "Ethernet"
ethernet0.linkStatePropagation.enable = "FALSE"
ethernet0.pciSlotNumber = "33"
ethernet0.present = "TRUE"
ethernet0.virtualDev = "{{ .Network_Adapter }}"
ethernet0.wakeOnPcktRcv = "FALSE"
{{if .Network_Name }}ethernet0.networkName = "{{ .Network_Name }}"{{end}}
// Hard disks
scsi0.present = "{{ .SCSI_Present }}"
scsi0.virtualDev = "{{ .SCSI_diskAdapterType }}"
scsi0.pciSlotNumber = "16"
scsi0:0.redo = ""
sata0.present = "{{ .SATA_Present }}"
nvme0.present = "{{ .NVME_Present }}"
{{ .DiskType }}0:0.present = "TRUE"
{{ .DiskType }}0:0.fileName = "{{ .DiskName }}.vmdk"
{{ .CDROMType }}0:{{ .CDROMType_PrimarySecondary }}.present = "TRUE"
{{ .CDROMType }}0:{{ .CDROMType_PrimarySecondary }}.fileName = "{{ .ISOPath }}"
{{ .CDROMType }}0:{{ .CDROMType_PrimarySecondary }}.deviceType = "cdrom-image"
// Sound
sound.startConnected = "{{ .Sound_Present }}"
sound.present = "{{ .Sound_Present }}"
sound.fileName = "-1"
sound.autodetect = "TRUE"
// USB
usb.pciSlotNumber = "32"
usb.present = "{{ .Usb_Present }}"
// Serial
serial0.present = "{{ .Serial_Present }}"
serial0.startConnected = "{{ .Serial_Present }}"
serial0.fileName = "{{ .Serial_Filename }}"
serial0.autodetect = "{{ .Serial_Auto }}"
serial0.fileType = "{{ .Serial_Type }}"
serial0.yieldOnMsrRead = "{{ .Serial_Yield }}"
serial0.pipe.endPoint = "{{ .Serial_Endpoint }}"
serial0.tryNoRxLoss = "{{ .Serial_Host }}"
// Parallel
parallel0.present = "{{ .Parallel_Present }}"
parallel0.startConnected = "{{ .Parallel_Present }}"
parallel0.fileName = "{{ .Parallel_Filename }}"
parallel0.autodetect = "{{ .Parallel_Auto }}"
parallel0.bidirectional = "{{ .Parallel_Bidirectional }}"
`
const DefaultAdditionalDiskTemplate = `
scsi0:{{ .DiskNumber }}.fileName = "{{ .DiskName}}-{{ .DiskNumber }}.vmdk"
scsi0:{{ .DiskNumber }}.present = "TRUE"
scsi0:{{ .DiskNumber }}.redo = ""
`
const ExtraCDRomTemplate = `
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.present = "TRUE"
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.fileName = "{{ .CDROM_PATH }}"
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.deviceType = "cdrom-image"
`