diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index d5f8b44f4..8fa185efd 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -2,13 +2,18 @@ package common import ( "bytes" + "errors" "fmt" + "io/ioutil" "log" + "net" + "os" "os/exec" "regexp" "runtime" "strconv" "strings" + "time" "github.com/hashicorp/packer/helper/multistep" ) @@ -24,15 +29,11 @@ type Driver interface { CompactDisk(string) error // CreateDisk creates a virtual disk with the given size. - CreateDisk(string, string, string) error + CreateDisk(string, string, string, string) error // Checks if the VMX file at the given path is running. IsRunning(string) (bool, error) - // CommHost returns the host address for the VM that is being - // managed by this driver. - CommHost(multistep.StateBag) (string, error) - // Start starts a VM specified by the path to the VMX given. Start(string, bool) error @@ -49,14 +50,33 @@ type Driver interface { // Attach the VMware tools ISO ToolsInstall() error - // Get the path to the DHCP leases file for the given device. - DhcpLeasesPath(string) string - // Verify checks to make sure that this driver should function // properly. This should check that all the files it will use // appear to exist and so on. If everything is okay, this doesn't - // return an error. Otherwise, this returns an error. + // return an error. Otherwise, this returns an error. Each vmware + // driver should assign the VmwareMachine callback functions for locating + // paths within this function. Verify() error + + /// This is to establish a connection to the guest + CommHost(multistep.StateBag) (string, error) + + /// These methods are generally implemented by the VmwareDriver + /// structure within this file. A driver implementation can + /// reimplement these, though, if it wants. + GetVmwareDriver() VmwareDriver + + // Get the guest hw address for the vm + GuestAddress(multistep.StateBag) (string, error) + + // Get the guest ip address for the vm + GuestIP(multistep.StateBag) (string, error) + + // Get the host hw address for the vm + HostAddress(multistep.StateBag) (string, error) + + // Get the host ip address for the vm + HostIP(multistep.StateBag) (string, error) } // NewDriver returns a new driver implementation for this operating @@ -192,3 +212,305 @@ func compareVersions(versionFound string, versionWanted string, product string) return nil } + +/// helper functions that read configuration information from a file +// read the network<->device configuration out of the specified path +func ReadNetmapConfig(path string) (NetworkMap, error) { + fd, err := os.Open(path) + if err != nil { + return nil, err + } + defer fd.Close() + return ReadNetworkMap(fd) +} + +// read the dhcp configuration out of the specified path +func ReadDhcpConfig(path string) (DhcpConfiguration, error) { + fd, err := os.Open(path) + if err != nil { + return nil, err + } + defer fd.Close() + return ReadDhcpConfiguration(fd) +} + +// read the VMX configuration from the specified path +func readVMXConfig(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return map[string]string{}, err + } + defer f.Close() + + vmxBytes, err := ioutil.ReadAll(f) + if err != nil { + return map[string]string{}, err + } + return ParseVMX(string(vmxBytes)), nil +} + +// read the connection type out of a vmx configuration +func readCustomDeviceName(vmxData map[string]string) (string, error) { + + connectionType, ok := vmxData["ethernet0.connectiontype"] + if !ok || connectionType != "custom" { + return "", fmt.Errorf("Unable to determine the device name for the connection type : %s", connectionType) + } + + device, ok := vmxData["ethernet0.vnet"] + if !ok || device == "" { + return "", fmt.Errorf("Unable to determine the device name for the connection type \"%s\" : %s", connectionType, device) + } + return device, nil +} + +// This VmwareDriver is a base class that contains default methods +// that a Driver can use or implement themselves. +type VmwareDriver struct { + /// These methods define paths that are utilized by the driver + /// A driver must overload these in order to point to the correct + /// files so that the address detection (ip and ethernet) machinery + /// works. + DhcpLeasesPath func(string) string + DhcpConfPath func(string) string + VmnetnatConfPath func(string) string + + /// This method returns an object with the NetworkNameMapper interface + /// that maps network to device and vice-versa. + NetworkMapper func() (NetworkNameMapper, error) +} + +func (d *VmwareDriver) GuestAddress(state multistep.StateBag) (string, error) { + vmxPath := state.Get("vmx_path").(string) + + log.Println("Lookup up IP information...") + vmxData, err := readVMXConfig(vmxPath) + if err != nil { + return "", err + } + + var ok bool + macAddress := "" + if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" { + if macAddress, ok = vmxData["ethernet0.generatedaddress"]; !ok || macAddress == "" { + return "", errors.New("couldn't find MAC address in VMX") + } + } + + res, err := net.ParseMAC(macAddress) + if err != nil { + return "", err + } + + return res.String(), nil +} + +func (d *VmwareDriver) GuestIP(state multistep.StateBag) (string, error) { + + // grab network mapper + netmap, err := d.NetworkMapper() + if err != nil { + return "", err + } + + // convert the stashed network to a device + network := state.Get("vmnetwork").(string) + device, err := netmap.NameIntoDevice(network) + + // we were unable to find the device, maybe it's a custom one... + // so, check to see if it's in the .vmx configuration + if err != nil || network == "custom" { + vmxPath := state.Get("vmx_path").(string) + vmxData, err := readVMXConfig(vmxPath) + if err != nil { + return "", err + } + + device, err = readCustomDeviceName(vmxData) + if err != nil { + return "", err + } + } + + // figure out our MAC address for looking up the guest address + MACAddress, err := d.GuestAddress(state) + if err != nil { + return "", err + } + + // figure out the correct dhcp leases + dhcpLeasesPath := d.DhcpLeasesPath(device) + log.Printf("DHCP leases path: %s", dhcpLeasesPath) + if dhcpLeasesPath == "" { + return "", errors.New("no DHCP leases path found.") + } + + // open up the lease and read its contents + fh, err := os.Open(dhcpLeasesPath) + if err != nil { + return "", err + } + defer fh.Close() + + dhcpBytes, err := ioutil.ReadAll(fh) + if err != nil { + return "", err + } + + // start grepping through the file looking for fields that we care about + var lastIp string + var lastLeaseEnd time.Time + + var curIp string + var curLeaseEnd time.Time + + ipLineRe := regexp.MustCompile(`^lease (.+?) {$`) + endTimeLineRe := regexp.MustCompile(`^\s*ends \d (.+?);$`) + macLineRe := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`) + + for _, line := range strings.Split(string(dhcpBytes), "\n") { + // Need to trim off CR character when running in windows + line = strings.TrimRight(line, "\r") + + matches := ipLineRe.FindStringSubmatch(line) + if matches != nil { + lastIp = matches[1] + continue + } + + matches = endTimeLineRe.FindStringSubmatch(line) + if matches != nil { + lastLeaseEnd, _ = time.Parse("2006/01/02 15:04:05", matches[1]) + continue + } + + // If the mac address matches and this lease ends farther in the + // future than the last match we might have, then choose it. + matches = macLineRe.FindStringSubmatch(line) + if matches != nil && strings.EqualFold(matches[1], MACAddress) && curLeaseEnd.Before(lastLeaseEnd) { + curIp = lastIp + curLeaseEnd = lastLeaseEnd + } + } + if curIp == "" { + return "", fmt.Errorf("IP not found for MAC %s in DHCP leases at %s", MACAddress, dhcpLeasesPath) + } + return curIp, nil +} + +func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) { + + // grab mapper for converting network<->device + netmap, err := d.NetworkMapper() + if err != nil { + return "", err + } + + // convert network to name + network := state.Get("vmnetwork").(string) + device, err := netmap.NameIntoDevice(network) + + // we were unable to find the device, maybe it's a custom one... + // so, check to see if it's in the .vmx configuration + if err != nil || network == "custom" { + vmxPath := state.Get("vmx_path").(string) + vmxData, err := readVMXConfig(vmxPath) + if err != nil { + return "", err + } + + device, err = readCustomDeviceName(vmxData) + if err != nil { + return "", err + } + } + + // parse dhcpd configuration + pathDhcpConfig := d.DhcpConfPath(device) + if _, err := os.Stat(pathDhcpConfig); err != nil { + return "", fmt.Errorf("Could not find vmnetdhcp conf file: %s", pathDhcpConfig) + } + + config, err := ReadDhcpConfig(pathDhcpConfig) + if err != nil { + return "", err + } + + // find the entry configured in the dhcpd + interfaceConfig, err := config.HostByName(device) + if err != nil { + return "", err + } + + // finally grab the hardware address + address, err := interfaceConfig.Hardware() + if err == nil { + return address.String(), nil + } + + // we didn't find it, so search through our interfaces for the device name + interfaceList, err := net.Interfaces() + if err == nil { + return "", err + } + + names := make([]string, 0) + for _, intf := range interfaceList { + if strings.HasSuffix(strings.ToLower(intf.Name), device) { + return intf.HardwareAddr.String(), nil + } + names = append(names, intf.Name) + } + return "", fmt.Errorf("Unable to find device %s : %v", device, names) +} + +func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) { + + // grab mapper for converting network<->device + netmap, err := d.NetworkMapper() + if err != nil { + return "", err + } + + // convert network to name + network := state.Get("vmnetwork").(string) + device, err := netmap.NameIntoDevice(network) + + // we were unable to find the device, maybe it's a custom one... + // so, check to see if it's in the .vmx configuration + if err != nil || network == "custom" { + vmxPath := state.Get("vmx_path").(string) + vmxData, err := readVMXConfig(vmxPath) + if err != nil { + return "", err + } + + device, err = readCustomDeviceName(vmxData) + if err != nil { + return "", err + } + } + + // parse dhcpd configuration + pathDhcpConfig := d.DhcpConfPath(device) + if _, err := os.Stat(pathDhcpConfig); err != nil { + return "", fmt.Errorf("Could not find vmnetdhcp conf file: %s", pathDhcpConfig) + } + config, err := ReadDhcpConfig(pathDhcpConfig) + if err != nil { + return "", err + } + + // find the entry configured in the dhcpd + interfaceConfig, err := config.HostByName(device) + if err != nil { + return "", err + } + + address, err := interfaceConfig.IP4() + if err != nil { + return "", err + } + + return address.String(), nil +} diff --git a/builder/vmware/common/driver_fusion5.go b/builder/vmware/common/driver_fusion5.go index 9545d3e81..9c81efd4a 100644 --- a/builder/vmware/common/driver_fusion5.go +++ b/builder/vmware/common/driver_fusion5.go @@ -14,6 +14,8 @@ import ( // Fusion5Driver is a driver that can run VMware Fusion 5. type Fusion5Driver struct { + VmwareDriver + // This is the path to the "VMware Fusion.app" AppPath string @@ -39,8 +41,8 @@ func (d *Fusion5Driver) CompactDisk(diskPath string) error { return nil } -func (d *Fusion5Driver) CreateDisk(output string, size string, type_id string) error { - cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output) +func (d *Fusion5Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error { + cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", adapter_type, "-t", type_id, output) if _, _, err := runAndLog(cmd); err != nil { return err } @@ -139,6 +141,32 @@ func (d *Fusion5Driver) Verify() error { return err } + libpath := filepath.Join("/", "Library", "Preferences", "VMware Fusion") + + d.VmwareDriver.DhcpLeasesPath = func(device string) string { + return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases" + } + d.VmwareDriver.DhcpConfPath = func(device string) string { + return filepath.Join(libpath, device, "dhcpd.conf") + } + d.VmwareDriver.VmnetnatConfPath = func(device string) string { + return filepath.Join(libpath, device, "nat.conf") + } + d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) { + pathNetworking := filepath.Join(libpath, "networking") + if _, err := os.Stat(pathNetworking); err != nil { + return nil, fmt.Errorf("Could not find networking conf file: %s", pathNetworking) + } + + fd, err := os.Open(pathNetworking) + if err != nil { + return nil, err + } + defer fd.Close() + + return ReadNetworkingConfig(fd) + } + return nil } @@ -158,10 +186,6 @@ func (d *Fusion5Driver) ToolsInstall() error { return nil } -func (d *Fusion5Driver) DhcpLeasesPath(device string) string { - return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases" -} - const fusionSuppressPlist = ` @@ -170,3 +194,7 @@ const fusionSuppressPlist = ` ` + +func (d *Fusion5Driver) GetVmwareDriver() VmwareDriver { + return d.VmwareDriver +} diff --git a/builder/vmware/common/driver_fusion6.go b/builder/vmware/common/driver_fusion6.go index af42c7ef9..1f959d35d 100644 --- a/builder/vmware/common/driver_fusion6.go +++ b/builder/vmware/common/driver_fusion6.go @@ -66,5 +66,36 @@ func (d *Fusion6Driver) Verify() error { } log.Printf("Detected VMware version: %s", matches[1]) + libpath := filepath.Join("/", "Library", "Preferences", "VMware Fusion") + + d.VmwareDriver.DhcpLeasesPath = func(device string) string { + return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases" + } + d.VmwareDriver.DhcpConfPath = func(device string) string { + return filepath.Join(libpath, device, "dhcpd.conf") + } + + d.VmwareDriver.VmnetnatConfPath = func(device string) string { + return filepath.Join(libpath, device, "nat.conf") + } + d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) { + pathNetworking := filepath.Join(libpath, "networking") + if _, err := os.Stat(pathNetworking); err != nil { + return nil, fmt.Errorf("Could not find networking conf file: %s", pathNetworking) + } + + fd, err := os.Open(pathNetworking) + if err != nil { + return nil, err + } + defer fd.Close() + + return ReadNetworkingConfig(fd) + } + return compareVersions(matches[1], VMWARE_FUSION_VERSION, "Fusion Professional") } + +func (d *Fusion6Driver) GetVmwareDriver() VmwareDriver { + return d.Fusion5Driver.VmwareDriver +} diff --git a/builder/vmware/common/driver_mock.go b/builder/vmware/common/driver_mock.go index 9291be976..a0955db88 100644 --- a/builder/vmware/common/driver_mock.go +++ b/builder/vmware/common/driver_mock.go @@ -1,6 +1,7 @@ package common import ( + "net" "sync" "github.com/hashicorp/packer/helper/multistep" @@ -18,11 +19,12 @@ type DriverMock struct { CompactDiskPath string CompactDiskErr error - CreateDiskCalled bool - CreateDiskOutput string - CreateDiskSize string - CreateDiskTypeId string - CreateDiskErr error + CreateDiskCalled bool + CreateDiskOutput string + CreateDiskSize string + CreateDiskAdapterType string + CreateDiskTypeId string + CreateDiskErr error IsRunningCalled bool IsRunningPath string @@ -34,6 +36,26 @@ type DriverMock struct { CommHostResult string CommHostErr error + HostAddressCalled bool + HostAddressState multistep.StateBag + HostAddressResult string + HostAddressErr error + + HostIPCalled bool + HostIPState multistep.StateBag + HostIPResult string + HostIPErr error + + GuestAddressCalled bool + GuestAddressState multistep.StateBag + GuestAddressResult string + GuestAddressErr error + + GuestIPCalled bool + GuestIPState multistep.StateBag + GuestIPResult string + GuestIPErr error + StartCalled bool StartPath string StartHeadless bool @@ -58,10 +80,33 @@ type DriverMock struct { DhcpLeasesPathDevice string DhcpLeasesPathResult string + DhcpConfPathCalled bool + DhcpConfPathResult string + + VmnetnatConfPathCalled bool + VmnetnatConfPathResult string + + NetmapConfPathCalled bool + NetmapConfPathResult string + VerifyCalled bool VerifyErr error } +type NetworkMapperMock struct { + NameIntoDeviceCalled int + DeviceIntoNameCalled int +} + +func (m NetworkMapperMock) NameIntoDevice(name string) (string, error) { + m.NameIntoDeviceCalled += 1 + return "", nil +} +func (m NetworkMapperMock) DeviceIntoName(device string) (string, error) { + m.DeviceIntoNameCalled += 1 + return "", nil +} + func (d *DriverMock) Clone(dst string, src string) error { d.CloneCalled = true d.CloneDst = dst @@ -75,10 +120,11 @@ func (d *DriverMock) CompactDisk(path string) error { return d.CompactDiskErr } -func (d *DriverMock) CreateDisk(output string, size string, typeId string) error { +func (d *DriverMock) CreateDisk(output string, size string, adapterType string, typeId string) error { d.CreateDiskCalled = true d.CreateDiskOutput = output d.CreateDiskSize = size + d.CreateDiskAdapterType = adapterType d.CreateDiskTypeId = typeId return d.CreateDiskErr } @@ -98,6 +144,58 @@ func (d *DriverMock) CommHost(state multistep.StateBag) (string, error) { return d.CommHostResult, d.CommHostErr } +func MockInterface() net.Interface { + interfaces, err := net.Interfaces() + + // Build a dummy interface due to being unable to enumerate interfaces + if err != nil || len(interfaces) == 0 { + return net.Interface{ + Index: 0, + MTU: -1, + Name: "dummy", + HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, + Flags: net.FlagLoopback, + } + } + + // Find the first loopback interface + for _, intf := range interfaces { + if intf.Flags&net.FlagLoopback == net.FlagLoopback { + return intf + } + } + + // Fall-back to just the first one + return interfaces[0] +} + +func (d *DriverMock) HostAddress(state multistep.StateBag) (string, error) { + intf := MockInterface() + d.HostAddressResult = intf.HardwareAddr.String() + d.HostAddressCalled = true + d.HostAddressState = state + return d.HostAddressResult, d.HostAddressErr +} + +func (d *DriverMock) HostIP(state multistep.StateBag) (string, error) { + d.HostIPResult = "127.0.0.1" + d.HostIPCalled = true + d.HostIPState = state + return d.HostIPResult, d.HostIPErr +} + +func (d *DriverMock) GuestAddress(state multistep.StateBag) (string, error) { + d.GuestAddressCalled = true + d.GuestAddressState = state + return d.GuestAddressResult, d.GuestAddressErr +} + +func (d *DriverMock) GuestIP(state multistep.StateBag) (string, error) { + d.GuestIPCalled = true + d.GuestIPState = state + return d.GuestIPResult, d.GuestIPErr +} + func (d *DriverMock) Start(path string, headless bool) error { d.StartCalled = true d.StartPath = path @@ -134,7 +232,39 @@ func (d *DriverMock) DhcpLeasesPath(device string) string { return d.DhcpLeasesPathResult } +func (d *DriverMock) DhcpConfPath(device string) string { + d.DhcpConfPathCalled = true + return d.DhcpConfPathResult +} + +func (d *DriverMock) VmnetnatConfPath(device string) string { + d.VmnetnatConfPathCalled = true + return d.VmnetnatConfPathResult +} + +func (d *DriverMock) NetmapConfPath() string { + d.NetmapConfPathCalled = true + return d.NetmapConfPathResult +} + func (d *DriverMock) Verify() error { d.VerifyCalled = true return d.VerifyErr } + +func (d *DriverMock) GetVmwareDriver() VmwareDriver { + var state VmwareDriver + state.DhcpLeasesPath = func(string) string { + return "/path/to/dhcp.leases" + } + state.DhcpConfPath = func(string) string { + return "/path/to/dhcp.conf" + } + state.VmnetnatConfPath = func(string) string { + return "/path/to/vmnetnat.conf" + } + state.NetworkMapper = func() (NetworkNameMapper, error) { + return NetworkMapperMock{}, nil + } + return state +} diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go new file mode 100644 index 000000000..24ecaf0df --- /dev/null +++ b/builder/vmware/common/driver_parser.go @@ -0,0 +1,1964 @@ +package common + +import ( + "fmt" + "io" + "log" + "math" + "net" + "os" + "reflect" + "sort" + "strconv" + "strings" +) + +type sentinelSignaller chan struct{} + +/** low-level parsing */ +// strip the comments and extraneous newlines from a byte channel +func uncomment(eof sentinelSignaller, in <-chan byte) (chan byte, sentinelSignaller) { + out := make(chan byte) + eoc := make(sentinelSignaller) + + go func(in <-chan byte, out chan byte) { + var endofline bool + for stillReading := true; stillReading; { + select { + case <-eof: + stillReading = false + case ch := <-in: + switch ch { + case '#': + endofline = true + case '\n': + if endofline { + endofline = false + } + } + if !endofline { + out <- ch + } + } + } + close(eoc) + }(in, out) + return out, eoc +} + +// convert a byte channel into a channel of pseudo-tokens +func tokenizeDhcpConfig(eof sentinelSignaller, in chan byte) (chan string, sentinelSignaller) { + var ch byte + var state string + var quote bool + + eot := make(sentinelSignaller) + + out := make(chan string) + go func(out chan string) { + for stillReading := true; stillReading; { + select { + case <-eof: + stillReading = false + + case ch = <-in: + if quote { + if ch == '"' { + out <- state + string(ch) + state, quote = "", false + continue + } + state += string(ch) + continue + } + + switch ch { + case '"': + quote = true + state += string(ch) + continue + + case '\r': + fallthrough + case '\n': + fallthrough + case '\t': + fallthrough + case ' ': + if len(state) == 0 { + continue + } + out <- state + state = "" + + case '{': + fallthrough + case '}': + fallthrough + case ';': + if len(state) > 0 { + out <- state + } + out <- string(ch) + state = "" + + default: + state += string(ch) + } + } + } + if len(state) > 0 { + out <- state + } + close(eot) + }(out) + return out, eot +} + +/** mid-level parsing */ +type tkParameter struct { + name string + operand []string +} + +func (e *tkParameter) String() string { + var values []string + for _, val := range e.operand { + values = append(values, val) + } + return fmt.Sprintf("%s [%s]", e.name, strings.Join(values, ",")) +} + +type tkGroup struct { + parent *tkGroup + id tkParameter + + groups []*tkGroup + params []tkParameter +} + +func (e *tkGroup) String() string { + var id []string + + id = append(id, e.id.name) + for _, val := range e.id.operand { + id = append(id, val) + } + + var config []string + for _, val := range e.params { + config = append(config, val.String()) + } + return fmt.Sprintf("%s {\n%s\n}", strings.Join(id, " "), strings.Join(config, "\n")) +} + +// convert a channel of pseudo-tokens into an tkParameter struct +func parseTokenParameter(in chan string) tkParameter { + var result tkParameter + for { + token := <-in + if result.name == "" { + result.name = token + continue + } + switch token { + case "{": + fallthrough + case "}": + fallthrough + case ";": + goto leave + default: + result.operand = append(result.operand, token) + } + } +leave: + return result +} + +// convert a channel of pseudo-tokens into an tkGroup tree */ +func parseDhcpConfig(eof sentinelSignaller, in chan string) (tkGroup, error) { + var tokens []string + var result tkGroup + + toParameter := func(tokens []string) tkParameter { + out := make(chan string) + go func(out chan string) { + for _, v := range tokens { + out <- v + } + out <- ";" + }(out) + return parseTokenParameter(out) + } + + for stillReading, currentgroup := true, &result; stillReading; { + select { + case <-eof: + stillReading = false + + case tk := <-in: + switch tk { + case "{": + grp := &tkGroup{parent: currentgroup} + grp.id = toParameter(tokens) + currentgroup.groups = append(currentgroup.groups, grp) + currentgroup = grp + case "}": + if currentgroup.parent == nil { + return tkGroup{}, fmt.Errorf("Unable to close the global declaration") + } + if len(tokens) > 0 { + return tkGroup{}, fmt.Errorf("List of tokens was left unterminated : %v", tokens) + } + currentgroup = currentgroup.parent + case ";": + arg := toParameter(tokens) + currentgroup.params = append(currentgroup.params, arg) + default: + tokens = append(tokens, tk) + continue + } + tokens = []string{} + } + } + return result, nil +} + +func tokenizeNetworkMapConfig(eof sentinelSignaller, in chan byte) (chan string, sentinelSignaller) { + var ch byte + var state string + var quote bool + var lastnewline bool + + eot := make(sentinelSignaller) + + out := make(chan string) + go func(out chan string) { + for stillReading := true; stillReading; { + select { + case <-eof: + stillReading = false + + case ch = <-in: + if quote { + if ch == '"' { + out <- state + string(ch) + state, quote = "", false + continue + } + state += string(ch) + continue + } + + switch ch { + case '"': + quote = true + state += string(ch) + continue + + case '\r': + fallthrough + case '\t': + fallthrough + case ' ': + if len(state) == 0 { + continue + } + out <- state + state = "" + + case '\n': + if lastnewline { + continue + } + if len(state) > 0 { + out <- state + } + out <- string(ch) + state = "" + lastnewline = true + continue + + case '.': + fallthrough + case '=': + if len(state) > 0 { + out <- state + } + out <- string(ch) + state = "" + + default: + state += string(ch) + } + lastnewline = false + } + } + if len(state) > 0 { + out <- state + } + close(eot) + }(out) + return out, eot +} + +func parseNetworkMapConfig(eof sentinelSignaller, in chan string) (NetworkMap, error) { + var unsorted map[string]map[string]string + var state []string + + addResult := func(network string, attribute string, value string) error { + _, ok := unsorted[network] + if !ok { + unsorted[network] = make(map[string]string) + } + + val, err := strconv.Unquote(value) + if err != nil { + return err + } + + current := unsorted[network] + current[attribute] = val + return nil + } + + stillReading := true + for unsorted = make(map[string]map[string]string); stillReading; { + select { + case <-eof: + if len(state) == 3 { + err := addResult(state[0], state[1], state[2]) + if err != nil { + return nil, err + } + } + stillReading = false + case tk := <-in: + switch tk { + case ".": + if len(state) != 1 { + return nil, fmt.Errorf("Missing network index") + } + case "=": + if len(state) != 2 { + return nil, fmt.Errorf("Assignment to empty attribute") + } + case "\n": + if len(state) == 0 { + continue + } + if len(state) != 3 { + return nil, fmt.Errorf("Invalid attribute assignment : %v", state) + } + err := addResult(state[0], state[1], state[2]) + if err != nil { + return nil, err + } + state = make([]string, 0) + default: + state = append(state, tk) + } + } + } + result := make([]map[string]string, 0) + var keys []string + for k := range unsorted { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + result = append(result, unsorted[k]) + } + return result, nil +} + +/** higher-level parsing */ +/// parameters +type pParameter interface { + repr() string +} + +type pParameterInclude struct { + filename string +} + +func (e pParameterInclude) repr() string { return fmt.Sprintf("include-file:filename=%s", e.filename) } + +type pParameterOption struct { + name string + value string +} + +func (e pParameterOption) repr() string { return fmt.Sprintf("option:%s=%s", e.name, e.value) } + +// allow some-kind-of-something +type pParameterGrant struct { + verb string // allow,deny,ignore + attribute string +} + +func (e pParameterGrant) repr() string { return fmt.Sprintf("grant:%s,%s", e.verb, e.attribute) } + +type pParameterAddress4 []string + +func (e pParameterAddress4) repr() string { + return fmt.Sprintf("fixed-address4:%s", strings.Join(e, ",")) +} + +type pParameterAddress6 []string + +func (e pParameterAddress6) repr() string { + return fmt.Sprintf("fixed-address6:%s", strings.Join(e, ",")) +} + +// hardware address 00:00:00:00:00:00 +type pParameterHardware struct { + class string + address []byte +} + +func (e pParameterHardware) repr() string { + res := make([]string, 0) + for _, v := range e.address { + res = append(res, fmt.Sprintf("%02x", v)) + } + return fmt.Sprintf("hardware-address:%s[%s]", e.class, strings.Join(res, ":")) +} + +type pParameterBoolean struct { + parameter string + truancy bool +} + +func (e pParameterBoolean) repr() string { return fmt.Sprintf("boolean:%s=%v", e.parameter, e.truancy) } + +type pParameterClientMatch struct { + name string + data string +} + +func (e pParameterClientMatch) repr() string { return fmt.Sprintf("match-client:%s=%s", e.name, e.data) } + +// range 127.0.0.1 127.0.0.255 +type pParameterRange4 struct { + min net.IP + max net.IP +} + +func (e pParameterRange4) repr() string { + return fmt.Sprintf("range4:%s-%s", e.min.String(), e.max.String()) +} + +type pParameterRange6 struct { + min net.IP + max net.IP +} + +func (e pParameterRange6) repr() string { + return fmt.Sprintf("range6:%s-%s", e.min.String(), e.max.String()) +} + +type pParameterPrefix6 struct { + min net.IP + max net.IP + bits int +} + +func (e pParameterPrefix6) repr() string { + return fmt.Sprintf("prefix6:/%d:%s-%s", e.bits, e.min.String(), e.max.String()) +} + +// some-kind-of-parameter 1024 +type pParameterOther struct { + parameter string + value string +} + +func (e pParameterOther) repr() string { return fmt.Sprintf("parameter:%s=%s", e.parameter, e.value) } + +type pParameterExpression struct { + parameter string + expression string +} + +func (e pParameterExpression) repr() string { + return fmt.Sprintf("parameter-expression:%s=\"%s\"", e.parameter, e.expression) +} + +type pDeclarationIdentifier interface { + repr() string +} + +type pDeclaration struct { + id pDeclarationIdentifier + parent *pDeclaration + parameters []pParameter + declarations []pDeclaration +} + +func (e *pDeclaration) short() string { + return e.id.repr() +} + +func (e *pDeclaration) repr() string { + res := e.short() + + var parameters []string + for _, v := range e.parameters { + parameters = append(parameters, v.repr()) + } + + var groups []string + for _, v := range e.declarations { + groups = append(groups, fmt.Sprintf("-> %s", v.short())) + } + + if e.parent != nil { + res = fmt.Sprintf("%s parent:%s", res, e.parent.short()) + } + return fmt.Sprintf("%s\n%s\n%s\n", res, strings.Join(parameters, "\n"), strings.Join(groups, "\n")) +} + +type pDeclarationGlobal struct{} + +func (e pDeclarationGlobal) repr() string { return fmt.Sprintf("{global}") } + +type pDeclarationShared struct{ name string } + +func (e pDeclarationShared) repr() string { return fmt.Sprintf("{shared-network %s}", e.name) } + +type pDeclarationSubnet4 struct{ net.IPNet } + +func (e pDeclarationSubnet4) repr() string { return fmt.Sprintf("{subnet4 %s}", e.String()) } + +type pDeclarationSubnet6 struct{ net.IPNet } + +func (e pDeclarationSubnet6) repr() string { return fmt.Sprintf("{subnet6 %s}", e.String()) } + +type pDeclarationHost struct{ name string } + +func (e pDeclarationHost) repr() string { return fmt.Sprintf("{host name:%s}", e.name) } + +type pDeclarationPool struct{} + +func (e pDeclarationPool) repr() string { return fmt.Sprintf("{pool}") } + +type pDeclarationGroup struct{} + +func (e pDeclarationGroup) repr() string { return fmt.Sprintf("{group}") } + +type pDeclarationClass struct{ name string } + +func (e pDeclarationClass) repr() string { return fmt.Sprintf("{class}") } + +/** parsers */ +func parseParameter(val tkParameter) (pParameter, error) { + switch val.name { + case "include": + if len(val.operand) != 2 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterInclude : %v", val.operand) + } + name := val.operand[0] + return pParameterInclude{filename: name}, nil + + case "option": + if len(val.operand) != 2 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterOption : %v", val.operand) + } + name, value := val.operand[0], val.operand[1] + return pParameterOption{name: name, value: value}, nil + + case "allow": + fallthrough + case "deny": + fallthrough + case "ignore": + if len(val.operand) < 1 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterGrant : %v", val.operand) + } + attribute := strings.Join(val.operand, " ") + return pParameterGrant{verb: strings.ToLower(val.name), attribute: attribute}, nil + + case "range": + if len(val.operand) < 1 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterRange4 : %v", val.operand) + } + idxAddress := map[bool]int{true: 1, false: 0}[strings.ToLower(val.operand[0]) == "bootp"] + if len(val.operand) > 2+idxAddress { + return nil, fmt.Errorf("Invalid number of parameters for pParameterRange : %v", val.operand) + } + if idxAddress+1 > len(val.operand) { + res := net.ParseIP(val.operand[idxAddress]) + return pParameterRange4{min: res, max: res}, nil + } + addr1 := net.ParseIP(val.operand[idxAddress]) + addr2 := net.ParseIP(val.operand[idxAddress+1]) + return pParameterRange4{min: addr1, max: addr2}, nil + + case "range6": + if len(val.operand) == 1 { + address := val.operand[0] + if strings.Contains(address, "/") { + cidr := strings.SplitN(address, "/", 2) + if len(cidr) != 2 { + return nil, fmt.Errorf("Unknown ipv6 format : %v", address) + } + address := net.ParseIP(cidr[0]) + bits, err := strconv.Atoi(cidr[1]) + if err != nil { + return nil, err + } + mask := net.CIDRMask(bits, net.IPv6len*8) + + // figure out the network address + network := address.Mask(mask) + + // make a broadcast address + broadcast := network + networkSize, totalSize := mask.Size() + hostSize := totalSize - networkSize + for i := networkSize / 8; i < totalSize/8; i++ { + broadcast[i] = byte(0xff) + } + octetIndex := network[networkSize/8] + bitsLeft := (uint32)(hostSize % 8) + broadcast[octetIndex] = network[octetIndex] | ((1 << bitsLeft) - 1) + + // FIXME: check that the broadcast address was made correctly + return pParameterRange6{min: network, max: broadcast}, nil + } + res := net.ParseIP(address) + return pParameterRange6{min: res, max: res}, nil + } + if len(val.operand) == 2 { + addr := net.ParseIP(val.operand[0]) + if strings.ToLower(val.operand[1]) == "temporary" { + return pParameterRange6{min: addr, max: addr}, nil + } + other := net.ParseIP(val.operand[1]) + return pParameterRange6{min: addr, max: other}, nil + } + return nil, fmt.Errorf("Invalid number of parameters for pParameterRange6 : %v", val.operand) + + case "prefix6": + if len(val.operand) != 3 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterRange6 : %v", val.operand) + } + bits, err := strconv.Atoi(val.operand[2]) + if err != nil { + return nil, fmt.Errorf("Invalid bits for pParameterPrefix6 : %v", val.operand[2]) + } + minaddr := net.ParseIP(val.operand[0]) + maxaddr := net.ParseIP(val.operand[1]) + return pParameterPrefix6{min: minaddr, max: maxaddr, bits: bits}, nil + + case "hardware": + if len(val.operand) != 2 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterHardware : %v", val.operand) + } + class := val.operand[0] + octets := strings.Split(val.operand[1], ":") + address := make([]byte, 0) + for _, v := range octets { + b, err := strconv.ParseInt(v, 16, 0) + if err != nil { + return nil, err + } + address = append(address, byte(b)) + } + return pParameterHardware{class: class, address: address}, nil + + case "fixed-address": + ip4addrs := make(pParameterAddress4, len(val.operand)) + copy(ip4addrs, val.operand) + return ip4addrs, nil + + case "fixed-address6": + ip6addrs := make(pParameterAddress6, len(val.operand)) + copy(ip6addrs, val.operand) + return ip6addrs, nil + + case "host-identifier": + if len(val.operand) != 3 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterClientMatch : %v", val.operand) + } + if val.operand[0] != "option" { + return nil, fmt.Errorf("Invalid match parameter : %v", val.operand[0]) + } + optionName := val.operand[1] + optionData := val.operand[2] + return pParameterClientMatch{name: optionName, data: optionData}, nil + + default: + length := len(val.operand) + if length < 1 { + return pParameterBoolean{parameter: val.name, truancy: true}, nil + } else if length > 1 { + if val.operand[0] == "=" { + return pParameterExpression{parameter: val.name, expression: strings.Join(val.operand[1:], "")}, nil + } + } + if length != 1 { + return nil, fmt.Errorf("Invalid number of parameters for pParameterOther : %v", val.operand) + } + if strings.ToLower(val.name) == "not" { + return pParameterBoolean{parameter: val.operand[0], truancy: false}, nil + } + return pParameterOther{parameter: val.name, value: val.operand[0]}, nil + } +} + +func parseTokenGroup(val tkGroup) (*pDeclaration, error) { + params := val.id.operand + switch val.id.name { + case "group": + return &pDeclaration{id: pDeclarationGroup{}}, nil + + case "pool": + return &pDeclaration{id: pDeclarationPool{}}, nil + + case "host": + if len(params) == 1 { + return &pDeclaration{id: pDeclarationHost{name: params[0]}}, nil + } + + case "subnet": + if len(params) == 3 && strings.ToLower(params[1]) == "netmask" { + addr := make([]byte, 4) + for i, v := range strings.SplitN(params[2], ".", 4) { + res, err := strconv.ParseInt(v, 10, 0) + if err != nil { + return nil, err + } + addr[i] = byte(res) + } + oc1, oc2, oc3, oc4 := addr[0], addr[1], addr[2], addr[3] + if subnet, mask := net.ParseIP(params[0]), net.IPv4Mask(oc1, oc2, oc3, oc4); subnet != nil && mask != nil { + return &pDeclaration{id: pDeclarationSubnet4{net.IPNet{IP: subnet, Mask: mask}}}, nil + } + } + case "subnet6": + if len(params) == 1 { + ip6 := strings.SplitN(params[0], "/", 2) + if len(ip6) == 2 && strings.Contains(ip6[0], ":") { + address := net.ParseIP(ip6[0]) + prefix, err := strconv.Atoi(ip6[1]) + if err != nil { + return nil, err + } + return &pDeclaration{id: pDeclarationSubnet6{net.IPNet{IP: address, Mask: net.CIDRMask(prefix, net.IPv6len*8)}}}, nil + } + } + case "shared-network": + if len(params) == 1 { + return &pDeclaration{id: pDeclarationShared{name: params[0]}}, nil + } + case "": + return &pDeclaration{id: pDeclarationGlobal{}}, nil + } + return nil, fmt.Errorf("Invalid pDeclaration : %v : %v", val.id.name, params) +} + +func flattenDhcpConfig(root tkGroup) (*pDeclaration, error) { + var result *pDeclaration + result, err := parseTokenGroup(root) + if err != nil { + return nil, err + } + + for _, p := range root.params { + param, err := parseParameter(p) + if err != nil { + return nil, err + } + result.parameters = append(result.parameters, param) + } + for _, p := range root.groups { + group, err := flattenDhcpConfig(*p) + if err != nil { + return nil, err + } + group.parent = result + result.declarations = append(result.declarations, *group) + } + return result, nil +} + +/** reduce the tree into the things that we care about */ +type grant uint + +const ( + ALLOW grant = iota + IGNORE grant = iota + DENY grant = iota +) + +type configDeclaration struct { + id []pDeclarationIdentifier + composites []pDeclaration + + address []pParameter + + options map[string]string + grants map[string]grant + attributes map[string]bool + parameters map[string]string + expressions map[string]string + + hostid []pParameterClientMatch +} + +func createDeclaration(node pDeclaration) configDeclaration { + var hierarchy []pDeclaration + + for n := &node; n != nil; n = n.parent { + hierarchy = append(hierarchy, *n) + } + + var result configDeclaration + result.address = make([]pParameter, 0) + + result.options = make(map[string]string) + result.grants = make(map[string]grant) + result.attributes = make(map[string]bool) + result.parameters = make(map[string]string) + result.expressions = make(map[string]string) + + result.hostid = make([]pParameterClientMatch, 0) + + // walk from globals to pDeclaration collecting all parameters + for i := len(hierarchy) - 1; i >= 0; i-- { + result.composites = append(result.composites, hierarchy[(len(hierarchy)-1)-i]) + result.id = append(result.id, hierarchy[(len(hierarchy)-1)-i].id) + + // update configDeclaration parameters + for _, p := range hierarchy[i].parameters { + switch p.(type) { + case pParameterOption: + result.options[p.(pParameterOption).name] = p.(pParameterOption).value + case pParameterGrant: + Grant := map[string]grant{"ignore": IGNORE, "allow": ALLOW, "deny": DENY} + result.grants[p.(pParameterGrant).attribute] = Grant[p.(pParameterGrant).verb] + case pParameterBoolean: + result.attributes[p.(pParameterBoolean).parameter] = p.(pParameterBoolean).truancy + case pParameterClientMatch: + result.hostid = append(result.hostid, p.(pParameterClientMatch)) + case pParameterExpression: + result.expressions[p.(pParameterExpression).parameter] = p.(pParameterExpression).expression + case pParameterOther: + result.parameters[p.(pParameterOther).parameter] = p.(pParameterOther).value + default: + result.address = append(result.address, p) + } + } + } + return result +} + +func (e *configDeclaration) repr() string { + var result []string + + var res []string + + res = make([]string, 0) + for _, v := range e.id { + res = append(res, v.repr()) + } + result = append(result, strings.Join(res, ",")) + + if len(e.address) > 0 { + res = make([]string, 0) + for _, v := range e.address { + res = append(res, v.repr()) + } + result = append(result, fmt.Sprintf("address : %v", strings.Join(res, ","))) + } + + if len(e.options) > 0 { + result = append(result, fmt.Sprintf("options : %v", e.options)) + } + if len(e.grants) > 0 { + result = append(result, fmt.Sprintf("grants : %v", e.grants)) + } + if len(e.attributes) > 0 { + result = append(result, fmt.Sprintf("attributes : %v", e.attributes)) + } + if len(e.parameters) > 0 { + result = append(result, fmt.Sprintf("parameters : %v", e.parameters)) + } + if len(e.expressions) > 0 { + result = append(result, fmt.Sprintf("parameter-expressions : %v", e.expressions)) + } + + if len(e.hostid) > 0 { + res = make([]string, 0) + for _, v := range e.hostid { + res = append(res, v.repr()) + } + result = append(result, fmt.Sprintf("hostid : %v", strings.Join(res, " "))) + } + return strings.Join(result, "\n") + "\n" +} + +func (e *configDeclaration) IP4() (net.IP, error) { + var result []string + for _, entry := range e.address { + switch entry.(type) { + case pParameterAddress4: + for _, s := range entry.(pParameterAddress4) { + result = append(result, s) + } + } + } + if len(result) > 1 { + return nil, fmt.Errorf("More than one address4 returned : %v", result) + } else if len(result) == 0 { + return nil, fmt.Errorf("No IP4 addresses found") + } + + if res := net.ParseIP(result[0]); res != nil { + return res, nil + } + res, err := net.ResolveIPAddr("ip4", result[0]) + if err != nil { + return nil, err + } + return res.IP, nil +} +func (e *configDeclaration) IP6() (net.IP, error) { + var result []string + for _, entry := range e.address { + switch entry.(type) { + case pParameterAddress6: + for _, s := range entry.(pParameterAddress6) { + result = append(result, s) + } + } + } + if len(result) > 1 { + return nil, fmt.Errorf("More than one address6 returned : %v", result) + } else if len(result) == 0 { + return nil, fmt.Errorf("No IP6 addresses found") + } + + if res := net.ParseIP(result[0]); res != nil { + return res, nil + } + res, err := net.ResolveIPAddr("ip6", result[0]) + if err != nil { + return nil, err + } + return res.IP, nil +} +func (e *configDeclaration) Hardware() (net.HardwareAddr, error) { + var result []pParameterHardware + for _, addr := range e.address { + switch addr.(type) { + case pParameterHardware: + result = append(result, addr.(pParameterHardware)) + } + } + if len(result) > 0 { + return nil, fmt.Errorf("More than one hardware address returned : %v", result) + } + res := make(net.HardwareAddr, 0) + for _, by := range result[0].address { + res = append(res, by) + } + return res, nil +} + +/*** Dhcp Configuration */ +type DhcpConfiguration []configDeclaration + +func ReadDhcpConfiguration(fd *os.File) (DhcpConfiguration, error) { + fromfile, eof := consumeFile(fd) + uncommented, eoc := uncomment(eof, fromfile) + tokenized, eot := tokenizeDhcpConfig(eoc, uncommented) + parsetree, err := parseDhcpConfig(eot, tokenized) + if err != nil { + return nil, err + } + + global, err := flattenDhcpConfig(parsetree) + if err != nil { + return nil, err + } + + var walkDeclarations func(root pDeclaration, out chan *configDeclaration) + walkDeclarations = func(root pDeclaration, out chan *configDeclaration) { + res := createDeclaration(root) + out <- &res + for _, p := range root.declarations { + walkDeclarations(p, out) + } + } + + each := make(chan *configDeclaration) + go func(out chan *configDeclaration) { + walkDeclarations(*global, out) + out <- nil + }(each) + + var result DhcpConfiguration + for decl := <-each; decl != nil; decl = <-each { + result = append(result, *decl) + } + return result, nil +} + +func (e *DhcpConfiguration) Global() configDeclaration { + result := (*e)[0] + if len(result.id) != 1 { + panic(fmt.Errorf("Something that can't happen happened")) + } + return result +} + +func (e *DhcpConfiguration) SubnetByAddress(address net.IP) (configDeclaration, error) { + var result []configDeclaration + for _, entry := range *e { + switch entry.id[0].(type) { + case pDeclarationSubnet4: + id := entry.id[0].(pDeclarationSubnet4) + if id.Contains(address) { + result = append(result, entry) + } + case pDeclarationSubnet6: + id := entry.id[0].(pDeclarationSubnet6) + if id.Contains(address) { + result = append(result, entry) + } + } + } + if len(result) == 0 { + return configDeclaration{}, fmt.Errorf("No network declarations containing %s found", address.String()) + } + if len(result) > 1 { + return configDeclaration{}, fmt.Errorf("More than 1 network declaration found : %v", result) + } + return result[0], nil +} + +func (e *DhcpConfiguration) HostByName(host string) (configDeclaration, error) { + var result []configDeclaration + for _, entry := range *e { + switch entry.id[0].(type) { + case pDeclarationHost: + id := entry.id[0].(pDeclarationHost) + if strings.ToLower(id.name) == strings.ToLower(host) { + result = append(result, entry) + } + } + } + if len(result) == 0 { + return configDeclaration{}, fmt.Errorf("No host declarations containing %s found", host) + } + if len(result) > 1 { + return configDeclaration{}, fmt.Errorf("More than 1 host declaration found : %v", result) + } + return result[0], nil +} + +/*** Network Map */ +type NetworkMap []map[string]string + +type NetworkNameMapper interface { + NameIntoDevice(string) (string, error) + DeviceIntoName(string) (string, error) +} + +func ReadNetworkMap(fd *os.File) (NetworkMap, error) { + + fromfile, eof := consumeFile(fd) + uncommented, eoc := uncomment(eof, fromfile) + tokenized, eot := tokenizeNetworkMapConfig(eoc, uncommented) + + result, err := parseNetworkMapConfig(eot, tokenized) + if err != nil { + return nil, err + } + return result, nil +} + +func (e NetworkMap) NameIntoDevice(name string) (string, error) { + for _, val := range e { + if strings.ToLower(val["name"]) == strings.ToLower(name) { + return val["device"], nil + } + } + return "", fmt.Errorf("Network name not found : %v", name) +} +func (e NetworkMap) DeviceIntoName(device string) (string, error) { + for _, val := range e { + if strings.ToLower(val["device"]) == strings.ToLower(device) { + return val["name"], nil + } + } + return "", fmt.Errorf("Device name not found : %v", device) +} +func (e *NetworkMap) repr() string { + var result []string + for idx, val := range *e { + result = append(result, fmt.Sprintf("network%d.name = \"%s\"", idx, val["name"])) + result = append(result, fmt.Sprintf("network%d.device = \"%s\"", idx, val["device"])) + } + return strings.Join(result, "\n") +} + +/*** parser for VMware Fusion's networking file */ +func tokenizeNetworkingConfig(eof sentinelSignaller, in chan byte) (chan string, sentinelSignaller) { + var ch byte + var state string + var repeat_newline bool + + eot := make(sentinelSignaller) + + out := make(chan string) + go func(out chan string) { + for reading := true; reading; { + select { + case <-eof: + reading = false + + case ch = <-in: + switch ch { + case '\r': + fallthrough + case '\t': + fallthrough + case ' ': + if len(state) == 0 { + continue + } + out <- state + state = "" + case '\n': + if repeat_newline { + continue + } + if len(state) > 0 { + out <- state + } + out <- string(ch) + state = "" + repeat_newline = true + continue + default: + state += string(ch) + } + repeat_newline = false + } + } + if len(state) > 0 { + out <- state + } + close(eot) + }(out) + return out, eot +} + +func splitNetworkingConfig(eof sentinelSignaller, in chan string) (chan []string, sentinelSignaller) { + var out chan []string + + eos := make(sentinelSignaller) + + out = make(chan []string) + go func(out chan []string) { + row := make([]string, 0) + for reading := true; reading; { + select { + case <-eof: + reading = false + + case tk := <-in: + switch tk { + case "\n": + if len(row) > 0 { + out <- row + } + row = make([]string, 0) + default: + row = append(row, tk) + } + } + } + if len(row) > 0 { + out <- row + } + close(eos) + }(out) + return out, eos +} + +/// All token types in networking file. +// VERSION token +type networkingVERSION struct { + value string +} + +func networkingReadVersion(row []string) (*networkingVERSION, error) { + if len(row) != 1 { + return nil, fmt.Errorf("Unexpected format for VERSION entry : %v", row) + } + res := &networkingVERSION{value: row[0]} + if !res.Valid() { + return nil, fmt.Errorf("Unexpected format for VERSION entry : %v", row) + } + return res, nil +} + +func (s networkingVERSION) Repr() string { + if !s.Valid() { + return fmt.Sprintf("VERSION{INVALID=\"%v\"}", s.value) + } + return fmt.Sprintf("VERSION{%f}", s.Number()) +} + +func (s networkingVERSION) Valid() bool { + tokens := strings.SplitN(s.value, "=", 2) + if len(tokens) != 2 || tokens[0] != "VERSION" { + return false + } + + tokens = strings.Split(tokens[1], ",") + if len(tokens) != 2 { + return false + } + + for _, t := range tokens { + _, err := strconv.ParseUint(t, 10, 64) + if err != nil { + return false + } + } + return true +} + +func (s networkingVERSION) Number() float64 { + var result float64 + tokens := strings.SplitN(s.value, "=", 2) + tokens = strings.Split(tokens[1], ",") + + integer, err := strconv.ParseUint(tokens[0], 10, 64) + if err != nil { + integer = 0 + } + result = float64(integer) + + mantissa, err := strconv.ParseUint(tokens[1], 10, 64) + if err != nil { + return result + } + denomination := math.Pow(10.0, float64(len(tokens[1]))) + return result + (float64(mantissa) / denomination) +} + +// VNET_X token +type networkingVNET struct { + value string +} + +func (s networkingVNET) Valid() bool { + if strings.ToUpper(s.value) != s.value { + return false + } + tokens := strings.SplitN(s.value, "_", 3) + if len(tokens) != 3 || tokens[0] != "VNET" { + return false + } + _, err := strconv.ParseUint(tokens[1], 10, 64) + if err != nil { + return false + } + return true +} + +func (s networkingVNET) Number() int { + tokens := strings.SplitN(s.value, "_", 3) + res, err := strconv.Atoi(tokens[1]) + if err != nil { + return ^int(0) + } + return res +} + +func (s networkingVNET) Option() string { + tokens := strings.SplitN(s.value, "_", 3) + if len(tokens) == 3 { + return tokens[2] + } + return "" +} + +func (s networkingVNET) Repr() string { + if !s.Valid() { + tokens := strings.SplitN(s.value, "_", 3) + return fmt.Sprintf("VNET{INVALID=%v}", tokens) + } + return fmt.Sprintf("VNET{%d} %s", s.Number(), s.Option()) +} + +// Interface name +type networkingInterface struct { + name string +} + +func (s networkingInterface) Interface() (*net.Interface, error) { + return net.InterfaceByName(s.name) +} + +// networking command entry types +type networkingCommandEntry_answer struct { + vnet networkingVNET + value string +} +type networkingCommandEntry_remove_answer struct { + vnet networkingVNET +} +type networkingCommandEntry_add_nat_portfwd struct { + vnet int + protocol string + port int + target_host net.IP + target_port int +} +type networkingCommandEntry_remove_nat_portfwd struct { + vnet int + protocol string + port int +} +type networkingCommandEntry_add_dhcp_mac_to_ip struct { + vnet int + mac net.HardwareAddr + ip net.IP +} +type networkingCommandEntry_remove_dhcp_mac_to_ip struct { + vnet int + mac net.HardwareAddr +} +type networkingCommandEntry_add_bridge_mapping struct { + intf networkingInterface + vnet int +} +type networkingCommandEntry_remove_bridge_mapping struct { + intf networkingInterface +} +type networkingCommandEntry_add_nat_prefix struct { + vnet int + prefix int +} +type networkingCommandEntry_remove_nat_prefix struct { + vnet int + prefix int +} + +type networkingCommandEntry struct { + entry interface{} + answer *networkingCommandEntry_answer + remove_answer *networkingCommandEntry_remove_answer + add_nat_portfwd *networkingCommandEntry_add_nat_portfwd + remove_nat_portfwd *networkingCommandEntry_remove_nat_portfwd + add_dhcp_mac_to_ip *networkingCommandEntry_add_dhcp_mac_to_ip + remove_dhcp_mac_to_ip *networkingCommandEntry_remove_dhcp_mac_to_ip + add_bridge_mapping *networkingCommandEntry_add_bridge_mapping + remove_bridge_mapping *networkingCommandEntry_remove_bridge_mapping + add_nat_prefix *networkingCommandEntry_add_nat_prefix + remove_nat_prefix *networkingCommandEntry_remove_nat_prefix +} + +func (e networkingCommandEntry) Name() string { + switch e.entry.(type) { + case networkingCommandEntry_answer: + return "answer" + case networkingCommandEntry_remove_answer: + return "remove_answer" + case networkingCommandEntry_add_nat_portfwd: + return "add_nat_portfwd" + case networkingCommandEntry_remove_nat_portfwd: + return "remove_nat_portfwd" + case networkingCommandEntry_add_dhcp_mac_to_ip: + return "add_dhcp_mac_to_ip" + case networkingCommandEntry_remove_dhcp_mac_to_ip: + return "remove_dhcp_mac_to_ip" + case networkingCommandEntry_add_bridge_mapping: + return "add_bridge_mapping" + case networkingCommandEntry_remove_bridge_mapping: + return "remove_bridge_mapping" + case networkingCommandEntry_add_nat_prefix: + return "add_nat_prefix" + case networkingCommandEntry_remove_nat_prefix: + return "remove_nat_prefix" + } + return "" +} + +func (e networkingCommandEntry) Entry() reflect.Value { + this := reflect.ValueOf(e) + switch e.entry.(type) { + case networkingCommandEntry_answer: + return reflect.Indirect(this.FieldByName("answer")) + case networkingCommandEntry_remove_answer: + return reflect.Indirect(this.FieldByName("remove_answer")) + case networkingCommandEntry_add_nat_portfwd: + return reflect.Indirect(this.FieldByName("add_nat_portfwd")) + case networkingCommandEntry_remove_nat_portfwd: + return reflect.Indirect(this.FieldByName("remove_nat_portfwd")) + case networkingCommandEntry_add_dhcp_mac_to_ip: + return reflect.Indirect(this.FieldByName("add_dhcp_mac_to_ip")) + case networkingCommandEntry_remove_dhcp_mac_to_ip: + return reflect.Indirect(this.FieldByName("remove_dhcp_mac_to_ip")) + case networkingCommandEntry_add_bridge_mapping: + return reflect.Indirect(this.FieldByName("add_bridge_mapping")) + case networkingCommandEntry_remove_bridge_mapping: + return reflect.Indirect(this.FieldByName("remove_bridge_mapping")) + case networkingCommandEntry_add_nat_prefix: + return reflect.Indirect(this.FieldByName("add_nat_prefix")) + case networkingCommandEntry_remove_nat_prefix: + return reflect.Indirect(this.FieldByName("remove_nat_prefix")) + } + return reflect.Value{} +} + +func (e networkingCommandEntry) Repr() string { + var result map[string]interface{} + result = make(map[string]interface{}) + + entryN, entry := e.Name(), e.Entry() + entryT := entry.Type() + for i := 0; i < entry.NumField(); i++ { + fld, fldT := entry.Field(i), entryT.Field(i) + result[fldT.Name] = fld + } + return fmt.Sprintf("%s -> %v", entryN, result) +} + +// networking command entry parsers +func parseNetworkingCommand_answer(row []string) (*networkingCommandEntry, error) { + if len(row) != 2 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 2, len(row)) + } + vnet := networkingVNET{value: row[0]} + if !vnet.Valid() { + return nil, fmt.Errorf("Invalid format for VNET.") + } + value := row[1] + + result := networkingCommandEntry_answer{vnet: vnet, value: value} + return &networkingCommandEntry{entry: result, answer: &result}, nil +} +func parseNetworkingCommand_remove_answer(row []string) (*networkingCommandEntry, error) { + if len(row) != 1 { + return nil, fmt.Errorf("Expected %d argument. Received %d.", 1, len(row)) + } + vnet := networkingVNET{value: row[0]} + if !vnet.Valid() { + return nil, fmt.Errorf("Invalid format for VNET.") + } + + result := networkingCommandEntry_remove_answer{vnet: vnet} + return &networkingCommandEntry{entry: result, remove_answer: &result}, nil +} +func parseNetworkingCommand_add_nat_portfwd(row []string) (*networkingCommandEntry, error) { + if len(row) != 5 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 5, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + protocol := strings.ToLower(row[1]) + if !(protocol == "tcp" || protocol == "udp") { + return nil, fmt.Errorf("Expected \"tcp\" or \"udp\" for second argument. : %v", row[1]) + } + + sport, err := strconv.Atoi(row[2]) + if err != nil { + return nil, fmt.Errorf("Unable to parse third argument as an integer. : %v", row[2]) + } + + dest := net.ParseIP(row[3]) + if dest == nil { + return nil, fmt.Errorf("Unable to parse fourth argument as an IPv4 address. : %v", row[2]) + } + + dport, err := strconv.Atoi(row[4]) + if err != nil { + return nil, fmt.Errorf("Unable to parse fifth argument as an integer. : %v", row[4]) + } + + result := networkingCommandEntry_add_nat_portfwd{vnet: vnet - 1, protocol: protocol, port: sport, target_host: dest, target_port: dport} + return &networkingCommandEntry{entry: result, add_nat_portfwd: &result}, nil +} +func parseNetworkingCommand_remove_nat_portfwd(row []string) (*networkingCommandEntry, error) { + if len(row) != 3 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 3, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + protocol := strings.ToLower(row[1]) + if !(protocol == "tcp" || protocol == "udp") { + return nil, fmt.Errorf("Expected \"tcp\" or \"udp\" for second argument. : %v", row[1]) + } + + sport, err := strconv.Atoi(row[2]) + if err != nil { + return nil, fmt.Errorf("Unable to parse third argument as an integer. : %v", row[2]) + } + + result := networkingCommandEntry_remove_nat_portfwd{vnet: vnet - 1, protocol: protocol, port: sport} + return &networkingCommandEntry{entry: result, remove_nat_portfwd: &result}, nil +} +func parseNetworkingCommand_add_dhcp_mac_to_ip(row []string) (*networkingCommandEntry, error) { + if len(row) != 3 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 3, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + mac, err := net.ParseMAC(row[1]) + if err != nil { + return nil, fmt.Errorf("Unable to parse second argument as hwaddr. : %v", row[1]) + } + + ip := net.ParseIP(row[2]) + if ip != nil { + return nil, fmt.Errorf("Unable to parse third argument as ipv4. : %v", row[2]) + } + + result := networkingCommandEntry_add_dhcp_mac_to_ip{vnet: vnet - 1, mac: mac, ip: ip} + return &networkingCommandEntry{entry: result, add_dhcp_mac_to_ip: &result}, nil +} +func parseNetworkingCommand_remove_dhcp_mac_to_ip(row []string) (*networkingCommandEntry, error) { + if len(row) != 2 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 2, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + mac, err := net.ParseMAC(row[1]) + if err != nil { + return nil, fmt.Errorf("Unable to parse second argument as hwaddr. : %v", row[1]) + } + + result := networkingCommandEntry_remove_dhcp_mac_to_ip{vnet: vnet - 1, mac: mac} + return &networkingCommandEntry{entry: result, remove_dhcp_mac_to_ip: &result}, nil +} +func parseNetworkingCommand_add_bridge_mapping(row []string) (*networkingCommandEntry, error) { + if len(row) != 2 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 2, len(row)) + } + intf := networkingInterface{name: row[0]} + + vnet, err := strconv.Atoi(row[1]) + if err != nil { + return nil, fmt.Errorf("Unable to parse second argument as an integer. : %v", row[2]) + } + + result := networkingCommandEntry_add_bridge_mapping{intf: intf, vnet: vnet - 1} + return &networkingCommandEntry{entry: result, add_bridge_mapping: &result}, nil +} +func parseNetworkingCommand_remove_bridge_mapping(row []string) (*networkingCommandEntry, error) { + if len(row) != 1 { + return nil, fmt.Errorf("Expected %d argument. Received %d.", 1, len(row)) + } + intf := networkingInterface{name: row[0]} + /* + number, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + */ + result := networkingCommandEntry_remove_bridge_mapping{intf: intf} + return &networkingCommandEntry{entry: result, remove_bridge_mapping: &result}, nil +} +func parseNetworkingCommand_add_nat_prefix(row []string) (*networkingCommandEntry, error) { + if len(row) != 2 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 2, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + if !strings.HasPrefix(row[1], "/") { + return nil, fmt.Errorf("Expected second argument to begin with \"/\". : %v", row[1]) + } + + prefix, err := strconv.Atoi(row[1][1:]) + if err != nil { + return nil, fmt.Errorf("Unable to parse prefix out of second argument. : %v", row[1]) + } + + result := networkingCommandEntry_add_nat_prefix{vnet: vnet - 1, prefix: prefix} + return &networkingCommandEntry{entry: result, add_nat_prefix: &result}, nil +} +func parseNetworkingCommand_remove_nat_prefix(row []string) (*networkingCommandEntry, error) { + if len(row) != 2 { + return nil, fmt.Errorf("Expected %d arguments. Received only %d.", 2, len(row)) + } + + vnet, err := strconv.Atoi(row[0]) + if err != nil { + return nil, fmt.Errorf("Unable to parse first argument as an integer. : %v", row[0]) + } + + if !strings.HasPrefix(row[1], "/") { + return nil, fmt.Errorf("Expected second argument to begin with \"/\". : %v", row[1]) + } + prefix, err := strconv.Atoi(row[1][1:]) + if err != nil { + return nil, fmt.Errorf("Unable to parse prefix out of second argument. : %v", row[1]) + } + + result := networkingCommandEntry_remove_nat_prefix{vnet: vnet - 1, prefix: prefix} + return &networkingCommandEntry{entry: result, remove_nat_prefix: &result}, nil +} + +type networkingCommandParser struct { + command string + callback func([]string) (*networkingCommandEntry, error) +} + +var NetworkingCommandParsers = []networkingCommandParser{ + /* DictRecordParseFunct */ {command: "answer", callback: parseNetworkingCommand_answer}, + /* DictRecordParseFunct */ {command: "remove_answer", callback: parseNetworkingCommand_remove_answer}, + /* NatFwdRecordParseFunct */ {command: "add_nat_portfwd", callback: parseNetworkingCommand_add_nat_portfwd}, + /* NatFwdRecordParseFunct */ {command: "remove_nat_portfwd", callback: parseNetworkingCommand_remove_nat_portfwd}, + /* DhcpMacRecordParseFunct */ {command: "add_dhcp_mac_to_ip", callback: parseNetworkingCommand_add_dhcp_mac_to_ip}, + /* DhcpMacRecordParseFunct */ {command: "remove_dhcp_mac_to_ip", callback: parseNetworkingCommand_remove_dhcp_mac_to_ip}, + /* BridgeMappingRecordParseFunct */ {command: "add_bridge_mapping", callback: parseNetworkingCommand_add_bridge_mapping}, + /* BridgeMappingRecordParseFunct */ {command: "remove_bridge_mapping", callback: parseNetworkingCommand_remove_bridge_mapping}, + /* NatPrefixRecordParseFunct */ {command: "add_nat_prefix", callback: parseNetworkingCommand_add_nat_prefix}, + /* NatPrefixRecordParseFunct */ {command: "remove_nat_prefix", callback: parseNetworkingCommand_remove_nat_prefix}, +} + +func NetworkingParserByCommand(command string) *func([]string) (*networkingCommandEntry, error) { + for _, p := range NetworkingCommandParsers { + if p.command == command { + return &p.callback + } + } + return nil +} + +func parseNetworkingConfig(eof sentinelSignaller, rows chan []string) (chan networkingCommandEntry, sentinelSignaller) { + var out chan networkingCommandEntry + + eop := make(sentinelSignaller) + + out = make(chan networkingCommandEntry) + go func(in chan []string, out chan networkingCommandEntry) { + for reading := true; reading; { + select { + case <-eof: + reading = false + case row := <-in: + if len(row) >= 1 { + parser := NetworkingParserByCommand(row[0]) + if parser == nil { + log.Printf("Invalid command : %v", row) + continue + } + callback := *parser + entry, err := callback(row[1:]) + if err != nil { + log.Printf("Unable to parse command : %v %v", err, row) + continue + } + out <- *entry + } + } + } + close(eop) + }(rows, out) + return out, eop +} + +type NetworkingConfig struct { + answer map[int]map[string]string + nat_portfwd map[int]map[string]string + dhcp_mac_to_ip map[int]map[string]net.IP + //bridge_mapping map[net.Interface]uint64 // XXX: we don't need the actual interface for anything but informing the user. + bridge_mapping map[string]int + nat_prefix map[int][]int +} + +func (c NetworkingConfig) repr() string { + return fmt.Sprintf("answer -> %v\nnat_portfwd -> %v\ndhcp_mac_to_ip -> %v\nbridge_mapping -> %v\nnat_prefix -> %v", c.answer, c.nat_portfwd, c.dhcp_mac_to_ip, c.bridge_mapping, c.nat_prefix) +} + +func flattenNetworkingConfig(eof sentinelSignaller, in chan networkingCommandEntry) NetworkingConfig { + var result NetworkingConfig + var vmnet int + + result.answer = make(map[int]map[string]string) + result.nat_portfwd = make(map[int]map[string]string) + result.dhcp_mac_to_ip = make(map[int]map[string]net.IP) + result.bridge_mapping = make(map[string]int) + result.nat_prefix = make(map[int][]int) + + for reading := true; reading; { + select { + case <-eof: + reading = false + case e := <-in: + switch e.entry.(type) { + case networkingCommandEntry_answer: + vnet := e.answer.vnet + answers, exists := result.answer[vnet.Number()] + if !exists { + answers = make(map[string]string) + result.answer[vnet.Number()] = answers + } + answers[vnet.Option()] = e.answer.value + case networkingCommandEntry_remove_answer: + vnet := e.remove_answer.vnet + answers, exists := result.answer[vnet.Number()] + if exists { + delete(answers, vnet.Option()) + } else { + log.Printf("Unable to remove answer %s as specified by `remove_answer`.\n", vnet.Repr()) + } + case networkingCommandEntry_add_nat_portfwd: + vmnet = e.add_nat_portfwd.vnet + protoport := fmt.Sprintf("%s/%d", e.add_nat_portfwd.protocol, e.add_nat_portfwd.port) + target := fmt.Sprintf("%s:%d", e.add_nat_portfwd.target_host, e.add_nat_portfwd.target_port) + portfwds, exists := result.nat_portfwd[vmnet] + if !exists { + portfwds = make(map[string]string) + result.nat_portfwd[vmnet] = portfwds + } + portfwds[protoport] = target + case networkingCommandEntry_remove_nat_portfwd: + vmnet = e.remove_nat_portfwd.vnet + protoport := fmt.Sprintf("%s/%d", e.remove_nat_portfwd.protocol, e.remove_nat_portfwd.port) + portfwds, exists := result.nat_portfwd[vmnet] + if exists { + delete(portfwds, protoport) + } else { + log.Printf("Unable to remove nat port-forward %s from interface %s%d as requested by `remove_nat_portfwd`.\n", protoport, NetworkingInterfacePrefix, vmnet) + } + case networkingCommandEntry_add_dhcp_mac_to_ip: + vmnet = e.add_dhcp_mac_to_ip.vnet + dhcpmacs, exists := result.dhcp_mac_to_ip[vmnet] + if !exists { + dhcpmacs = make(map[string]net.IP) + result.dhcp_mac_to_ip[vmnet] = dhcpmacs + } + dhcpmacs[e.add_dhcp_mac_to_ip.mac.String()] = e.add_dhcp_mac_to_ip.ip + case networkingCommandEntry_remove_dhcp_mac_to_ip: + vmnet = e.remove_dhcp_mac_to_ip.vnet + dhcpmacs, exists := result.dhcp_mac_to_ip[vmnet] + if exists { + delete(dhcpmacs, e.remove_dhcp_mac_to_ip.mac.String()) + } else { + log.Printf("Unable to remove dhcp_mac_to_ip entry %v from interface %s%d as specified by `remove_dhcp_mac_to_ip`.\n", e.remove_dhcp_mac_to_ip, NetworkingInterfacePrefix, vmnet) + } + case networkingCommandEntry_add_bridge_mapping: + intf := e.add_bridge_mapping.intf + if _, err := intf.Interface(); err != nil { + log.Printf("Interface \"%s\" as specified by `add_bridge_mapping` was not found on the current platform. This is a non-critical error. Ignoring.", intf.name) + } + result.bridge_mapping[intf.name] = e.add_bridge_mapping.vnet + case networkingCommandEntry_remove_bridge_mapping: + intf := e.remove_bridge_mapping.intf + if _, err := intf.Interface(); err != nil { + log.Printf("Interface \"%s\" as specified by `remove_bridge_mapping` was not found on the current platform. This is a non-critical error. Ignoring.", intf.name) + } + delete(result.bridge_mapping, intf.name) + case networkingCommandEntry_add_nat_prefix: + vmnet = e.add_nat_prefix.vnet + _, exists := result.nat_prefix[vmnet] + if exists { + result.nat_prefix[vmnet] = append(result.nat_prefix[vmnet], e.add_nat_prefix.prefix) + } else { + result.nat_prefix[vmnet] = []int{e.add_nat_prefix.prefix} + } + case networkingCommandEntry_remove_nat_prefix: + vmnet = e.remove_nat_prefix.vnet + prefixes, exists := result.nat_prefix[vmnet] + if exists { + for index := 0; index < len(prefixes); index++ { + if prefixes[index] == e.remove_nat_prefix.prefix { + result.nat_prefix[vmnet] = append(prefixes[:index], prefixes[index+1:]...) + break + } + } + } else { + log.Printf("Unable to remove nat prefix /%d from interface %s%d as specified by `remove_nat_prefix`.\n", e.remove_nat_prefix.prefix, NetworkingInterfacePrefix, vmnet) + } + } + } + } + return result +} + +// Constructor for networking file +func ReadNetworkingConfig(fd *os.File) (NetworkingConfig, error) { + // start piecing together different parts of the file + fromfile, eof := consumeFile(fd) + tokenized, eot := tokenizeNetworkingConfig(eof, fromfile) + rows, eos := splitNetworkingConfig(eot, tokenized) + entries, eop := parseNetworkingConfig(eos, rows) + + // parse the version + parsed_version, err := networkingReadVersion(<-rows) + if err != nil { + return NetworkingConfig{}, err + } + + // verify that it's 1.0 since that's all we support. + version := parsed_version.Number() + if version != 1.0 { + return NetworkingConfig{}, fmt.Errorf("Expected version %f of networking file. Received version %f.", 1.0, version) + } + + // convert to a configuration + result := flattenNetworkingConfig(eop, entries) + return result, nil +} + +// netmapper interface +type NetworkingType int + +const ( + NetworkingType_HOSTONLY = iota + 1 + NetworkingType_NAT + NetworkingType_BRIDGED +) + +func networkingConfig_InterfaceTypes(config NetworkingConfig) map[int]NetworkingType { + var result map[int]NetworkingType + result = make(map[int]NetworkingType) + + // defaults + result[0] = NetworkingType_BRIDGED + result[1] = NetworkingType_HOSTONLY + result[8] = NetworkingType_NAT + + // walk through config collecting bridged interfaces + for _, vmnet := range config.bridge_mapping { + result[vmnet] = NetworkingType_BRIDGED + } + + // walk through answers finding out which ones are nat versus hostonly + for vmnet, table := range config.answer { + // everything should be defined as a virtual adapter... + if table["VIRTUAL_ADAPTER"] == "yes" { + + // validate that the VNET entry contains everything we expect it to + _, subnetQ := table["HOSTONLY_SUBNET"] + _, netmaskQ := table["HOSTONLY_NETMASK"] + if !(subnetQ && netmaskQ) { + log.Printf("Interface %s%d is missing some expected keys (HOSTONLY_SUBNET, HOSTONLY_NETMASK). This is non-critical. Ignoring..", NetworkingInterfacePrefix, vmnet) + } + + // distinguish between nat or hostonly + if table["NAT"] == "yes" { + result[vmnet] = NetworkingType_NAT + } else { + result[vmnet] = NetworkingType_HOSTONLY + } + + // if it's not a virtual_adapter, then it must be an alias (really a bridge). + } else { + result[vmnet] = NetworkingType_BRIDGED + } + } + return result +} + +func networkingConfig_NamesToVmnet(config NetworkingConfig) map[NetworkingType][]int { + types := networkingConfig_InterfaceTypes(config) + + // now sort the keys + var keys []int + for vmnet := range types { + keys = append(keys, vmnet) + } + sort.Ints(keys) + + // build result dictionary + var result map[NetworkingType][]int + result = make(map[NetworkingType][]int) + for i := 0; i < len(keys); i++ { + t := types[keys[i]] + result[t] = append(result[t], keys[i]) + } + return result +} + +const NetworkingInterfacePrefix = "vmnet" + +func (e NetworkingConfig) NameIntoDevice(name string) (string, error) { + netmapper := networkingConfig_NamesToVmnet(e) + name = strings.ToLower(name) + + var vmnet int + if name == "hostonly" && len(netmapper[NetworkingType_HOSTONLY]) > 0 { + vmnet = netmapper[NetworkingType_HOSTONLY][0] + } else if name == "nat" && len(netmapper[NetworkingType_NAT]) > 0 { + vmnet = netmapper[NetworkingType_NAT][0] + } else if name == "bridged" && len(netmapper[NetworkingType_BRIDGED]) > 0 { + vmnet = netmapper[NetworkingType_BRIDGED][0] + } else { + return "", fmt.Errorf("Network name not found : %v", name) + } + return fmt.Sprintf("%s%d", NetworkingInterfacePrefix, vmnet), nil +} + +func (e NetworkingConfig) DeviceIntoName(device string) (string, error) { + types := networkingConfig_InterfaceTypes(e) + + lowerdevice := strings.ToLower(device) + if !strings.HasPrefix(lowerdevice, NetworkingInterfacePrefix) { + return device, nil + } + vmnet, err := strconv.Atoi(lowerdevice[len(NetworkingInterfacePrefix):]) + if err != nil { + return "", err + } + network := types[vmnet] + switch network { + case NetworkingType_HOSTONLY: + return "hostonly", nil + case NetworkingType_NAT: + return "nat", nil + case NetworkingType_BRIDGED: + return "bridged", nil + } + return "", fmt.Errorf("Unable to determine network type for device %s%d.", NetworkingInterfacePrefix, vmnet) +} + +/** generic async file reader */ +func consumeFile(fd *os.File) (chan byte, sentinelSignaller) { + fromfile := make(chan byte) + eof := make(sentinelSignaller) + go func() { + b := make([]byte, 1) + for { + _, err := fd.Read(b) + if err == io.EOF { + break + } + fromfile <- b[0] + } + close(eof) + }() + return fromfile, eof +} diff --git a/builder/vmware/common/driver_player5.go b/builder/vmware/common/driver_player5.go index 311e5019c..fb14dcb52 100644 --- a/builder/vmware/common/driver_player5.go +++ b/builder/vmware/common/driver_player5.go @@ -14,6 +14,8 @@ import ( // Player5Driver is a driver that can run VMware Player 5 on Linux. type Player5Driver struct { + VmwareDriver + AppPath string VdiskManagerPath string QemuImgPath string @@ -62,12 +64,12 @@ func (d *Player5Driver) qemuCompactDisk(diskPath string) error { return nil } -func (d *Player5Driver) CreateDisk(output string, size string, type_id string) error { +func (d *Player5Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error { var cmd *exec.Cmd if d.QemuImgPath != "" { cmd = exec.Command(d.QemuImgPath, "create", "-f", "vmdk", "-o", "compat6", output, size) } else { - cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output) + cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", adapter_type, "-t", type_id, output) } if _, _, err := runAndLog(cmd); err != nil { return err @@ -181,6 +183,33 @@ func (d *Player5Driver) Verify() error { "One of these is required to configure disks for VMware Player.") } + // Assigning the path callbacks to VmwareDriver + d.VmwareDriver.DhcpLeasesPath = func(device string) string { + return playerDhcpLeasesPath(device) + } + + d.VmwareDriver.DhcpConfPath = func(device string) string { + return playerVmDhcpConfPath(device) + } + + d.VmwareDriver.VmnetnatConfPath = func(device string) string { + return playerVmnetnatConfPath(device) + } + + d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) { + pathNetmap := playerNetmapConfPath() + if _, err := os.Stat(pathNetmap); err != nil { + return nil, fmt.Errorf("Could not find netmap conf file: %s", pathNetmap) + } + + fd, err := os.Open(pathNetmap) + if err != nil { + return nil, err + } + defer fd.Close() + + return ReadNetworkMap(fd) + } return nil } @@ -192,10 +221,6 @@ func (d *Player5Driver) ToolsInstall() error { return nil } -func (d *Player5Driver) DhcpLeasesPath(device string) string { - return playerDhcpLeasesPath(device) -} - -func (d *Player5Driver) VmnetnatConfPath() string { - return playerVmnetnatConfPath() +func (d *Player5Driver) GetVmwareDriver() VmwareDriver { + return d.VmwareDriver } diff --git a/builder/vmware/common/driver_player5_windows.go b/builder/vmware/common/driver_player5_windows.go index e16d275db..790a4c46c 100644 --- a/builder/vmware/common/driver_player5_windows.go +++ b/builder/vmware/common/driver_player5_windows.go @@ -57,14 +57,29 @@ func playerDhcpLeasesPath(device string) string { } else if _, err := os.Stat(path); err == nil { return path } - return findFile("vmnetdhcp.leases", playerDataFilePaths()) } -func playerVmnetnatConfPath() string { +func playerVmDhcpConfPath(device string) string { + // the device isn't actually used on windows hosts + path, err := playerDhcpConfigPathRegistry() + if err != nil { + log.Printf("Error finding configuration in registry: %s", err) + } else if _, err := os.Stat(path); err == nil { + return path + } + return findFile("vmnetdhcp.conf", playerDataFilePaths()) +} + +func playerVmnetnatConfPath(device string) string { + // the device isn't actually used on windows hosts return findFile("vmnetnat.conf", playerDataFilePaths()) } +func playerNetmapConfPath() string { + return findFile("netmap.conf", playerDataFilePaths()) +} + // This reads the VMware installation path from the Windows registry. func playerVMwareRoot() (s string, err error) { key := `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\vmplayer.exe` @@ -87,7 +102,18 @@ func playerDhcpLeasesPathRegistry() (s string, err error) { log.Printf(`Unable to read registry key %s\%s`, key, subkey) return } + return normalizePath(s), nil +} +// This reads the VMware DHCP configuration path from the Windows registry. +func playerDhcpConfigPathRegistry() (s string, err error) { + key := "SYSTEM\\CurrentControlSet\\services\\VMnetDHCP\\Parameters" + subkey := "ConfFile" + s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey) + if err != nil { + log.Printf(`Unable to read registry key %s\%s`, key, subkey) + return + } return normalizePath(s), nil } diff --git a/builder/vmware/common/driver_player6.go b/builder/vmware/common/driver_player6.go index d1cc7be88..1ccb84b85 100644 --- a/builder/vmware/common/driver_player6.go +++ b/builder/vmware/common/driver_player6.go @@ -35,3 +35,7 @@ func (d *Player6Driver) Verify() error { return playerVerifyVersion(VMWARE_PLAYER_VERSION) } + +func (d *Player6Driver) GetVmwareDriver() VmwareDriver { + return d.Player5Driver.VmwareDriver +} diff --git a/builder/vmware/common/driver_player_unix.go b/builder/vmware/common/driver_player_unix.go index 0c2cc8635..068720a6c 100644 --- a/builder/vmware/common/driver_player_unix.go +++ b/builder/vmware/common/driver_player_unix.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "os/exec" + "path/filepath" "regexp" "runtime" ) @@ -28,16 +29,49 @@ func playerFindVmrun() (string, error) { return exec.LookPath("vmrun") } -func playerDhcpLeasesPath(device string) string { - return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases" -} - func playerToolsIsoPath(flavor string) string { return "/usr/lib/vmware/isoimages/" + flavor + ".iso" } -func playerVmnetnatConfPath() string { - return "" +// return the base path to vmware's config on the host +func playerVMwareRoot() (s string, err error) { + return "/etc/vmware", nil +} + +func playerDhcpLeasesPath(device string) string { + base, err := playerVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "dhcpd/dhcpd.leases") +} + +func playerVmDhcpConfPath(device string) string { + base, err := playerVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "dhcp/dhcp.conf") +} + +func playerVmnetnatConfPath(device string) string { + base, err := playerVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "nat/nat.conf") +} + +func playerNetmapConfPath() string { + base, err := playerVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, "netmap.conf") } func playerVerifyVersion(version string) error { diff --git a/builder/vmware/common/driver_workstation10.go b/builder/vmware/common/driver_workstation10.go index 7e6ac8b8f..471fce37d 100644 --- a/builder/vmware/common/driver_workstation10.go +++ b/builder/vmware/common/driver_workstation10.go @@ -33,3 +33,7 @@ func (d *Workstation10Driver) Verify() error { return workstationVerifyVersion(VMWARE_WS_VERSION) } + +func (d *Workstation10Driver) GetVmwareDriver() VmwareDriver { + return d.Workstation9Driver.VmwareDriver +} diff --git a/builder/vmware/common/driver_workstation9.go b/builder/vmware/common/driver_workstation9.go index d870a4cda..25b4ddd2c 100644 --- a/builder/vmware/common/driver_workstation9.go +++ b/builder/vmware/common/driver_workstation9.go @@ -14,6 +14,8 @@ import ( // Workstation9Driver is a driver that can run VMware Workstation 9 type Workstation9Driver struct { + VmwareDriver + AppPath string VdiskManagerPath string VmrunPath string @@ -40,8 +42,8 @@ func (d *Workstation9Driver) CompactDisk(diskPath string) error { return nil } -func (d *Workstation9Driver) CreateDisk(output string, size string, type_id string) error { - cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output) +func (d *Workstation9Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error { + cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", adapter_type, "-t", type_id, output) if _, _, err := runAndLog(cmd); err != nil { return err } @@ -142,6 +144,33 @@ func (d *Workstation9Driver) Verify() error { return err } + // Assigning the path callbacks to VmwareDriver + d.VmwareDriver.DhcpLeasesPath = func(device string) string { + return workstationDhcpLeasesPath(device) + } + + d.VmwareDriver.DhcpConfPath = func(device string) string { + return workstationDhcpConfPath(device) + } + + d.VmwareDriver.VmnetnatConfPath = func(device string) string { + return workstationVmnetnatConfPath(device) + } + + d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) { + pathNetmap := workstationNetmapConfPath() + if _, err := os.Stat(pathNetmap); err != nil { + return nil, fmt.Errorf("Could not find netmap conf file: %s", pathNetmap) + } + + fd, err := os.Open(pathNetmap) + if err != nil { + return nil, err + } + defer fd.Close() + + return ReadNetworkMap(fd) + } return nil } @@ -153,10 +182,6 @@ func (d *Workstation9Driver) ToolsInstall() error { return nil } -func (d *Workstation9Driver) DhcpLeasesPath(device string) string { - return workstationDhcpLeasesPath(device) -} - -func (d *Workstation9Driver) VmnetnatConfPath() string { - return workstationVmnetnatConfPath() +func (d *Workstation9Driver) GetVmwareDriver() VmwareDriver { + return d.VmwareDriver } diff --git a/builder/vmware/common/driver_workstation9_windows.go b/builder/vmware/common/driver_workstation9_windows.go index 17095badc..b8d6d2b61 100644 --- a/builder/vmware/common/driver_workstation9_windows.go +++ b/builder/vmware/common/driver_workstation9_windows.go @@ -59,10 +59,20 @@ func workstationDhcpLeasesPath(device string) string { return findFile("vmnetdhcp.leases", workstationDataFilePaths()) } -func workstationVmnetnatConfPath() string { +func workstationDhcpConfPath(device string) string { + // device isn't used on a windows host + return findFile("vmnetdhcp.conf", workstationDataFilePaths()) +} + +func workstationVmnetnatConfPath(device string) string { + // device isn't used on a windows host return findFile("vmnetnat.conf", workstationDataFilePaths()) } +func workstationNetmapConfPath() string { + return findFile("netmap.conf", workstationDataFilePaths()) +} + // See http://blog.natefinch.com/2012/11/go-win-stuff.html // // This is used by workstationVMwareRoot in order to read some registry data. diff --git a/builder/vmware/common/driver_workstation_unix.go b/builder/vmware/common/driver_workstation_unix.go index 2931396a4..45e94d21e 100644 --- a/builder/vmware/common/driver_workstation_unix.go +++ b/builder/vmware/common/driver_workstation_unix.go @@ -39,18 +39,51 @@ func workstationFindVmrun() (string, error) { return exec.LookPath("vmrun") } +// return the base path to vmware's config on the host +func workstationVMwareRoot() (s string, err error) { + return "/etc/vmware", nil +} + func workstationDhcpLeasesPath(device string) string { - return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases" + base, err := workstationVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "dhcpd/dhcpd.leases") +} + +func workstationDhcpConfPath(device string) string { + base, err := workstationVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "dhcp/dhcpd.conf") +} + +func workstationVmnetnatConfPath(device string) string { + base, err := workstationVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, device, "nat/nat.conf") +} + +func workstationNetmapConfPath() string { + base, err := workstationVMwareRoot() + if err != nil { + log.Printf("Error finding VMware root: %s", err) + return "" + } + return filepath.Join(base, "netmap.conf") } func workstationToolsIsoPath(flavor string) string { return "/usr/lib/vmware/isoimages/" + flavor + ".iso" } -func workstationVmnetnatConfPath() string { - return "" -} - func workstationVerifyVersion(version string) error { if runtime.GOOS != "linux" { return fmt.Errorf("The VMware WS version %s driver is only supported on Linux, and Windows, at the moment. Your OS: %s", version, runtime.GOOS) diff --git a/builder/vmware/common/guest_ip.go b/builder/vmware/common/guest_ip.go deleted file mode 100644 index 25ca3b795..000000000 --- a/builder/vmware/common/guest_ip.go +++ /dev/null @@ -1,90 +0,0 @@ -package common - -import ( - "errors" - "fmt" - "io/ioutil" - "log" - "os" - "regexp" - "strings" - "time" -) - -// Interface to help find the IP address of a running virtual machine. -type GuestIPFinder interface { - GuestIP() (string, error) -} - -// DHCPLeaseGuestLookup looks up the IP address of a guest using DHCP -// lease information from the VMware network devices. -type DHCPLeaseGuestLookup struct { - // Driver that is being used (to find leases path) - Driver Driver - - // Device that the guest is connected to. - Device string - - // MAC address of the guest. - MACAddress string -} - -func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) { - dhcpLeasesPath := f.Driver.DhcpLeasesPath(f.Device) - log.Printf("DHCP leases path: %s", dhcpLeasesPath) - if dhcpLeasesPath == "" { - return "", errors.New("no DHCP leases path found.") - } - - fh, err := os.Open(dhcpLeasesPath) - if err != nil { - return "", err - } - defer fh.Close() - - dhcpBytes, err := ioutil.ReadAll(fh) - if err != nil { - return "", err - } - - var lastIp string - var lastLeaseEnd time.Time - - var curIp string - var curLeaseEnd time.Time - - ipLineRe := regexp.MustCompile(`^lease (.+?) {$`) - endTimeLineRe := regexp.MustCompile(`^\s*ends \d (.+?);$`) - macLineRe := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`) - - for _, line := range strings.Split(string(dhcpBytes), "\n") { - // Need to trim off CR character when running in windows - line = strings.TrimRight(line, "\r") - - matches := ipLineRe.FindStringSubmatch(line) - if matches != nil { - lastIp = matches[1] - continue - } - - matches = endTimeLineRe.FindStringSubmatch(line) - if matches != nil { - lastLeaseEnd, _ = time.Parse("2006/01/02 15:04:05", matches[1]) - continue - } - - // If the mac address matches and this lease ends farther in the - // future than the last match we might have, then choose it. - matches = macLineRe.FindStringSubmatch(line) - if matches != nil && strings.EqualFold(matches[1], f.MACAddress) && curLeaseEnd.Before(lastLeaseEnd) { - curIp = lastIp - curLeaseEnd = lastLeaseEnd - } - } - - if curIp == "" { - return "", fmt.Errorf("IP not found for MAC %s in DHCP leases at %s", f.MACAddress, dhcpLeasesPath) - } - - return curIp, nil -} diff --git a/builder/vmware/common/guest_ip_test.go b/builder/vmware/common/guest_ip_test.go deleted file mode 100644 index fdd6b4c9c..000000000 --- a/builder/vmware/common/guest_ip_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package common - -import ( - "io/ioutil" - "os" - "testing" -) - -func TestDHCPLeaseGuestLookup_impl(t *testing.T) { - var _ GuestIPFinder = new(DHCPLeaseGuestLookup) -} - -func TestDHCPLeaseGuestLookup(t *testing.T) { - tf, err := ioutil.TempFile("", "packer") - if err != nil { - t.Fatalf("err: %s", err) - } - if _, err := tf.Write([]byte(testLeaseContents)); err != nil { - t.Fatalf("err: %s", err) - } - tf.Close() - defer os.Remove(tf.Name()) - - driver := new(DriverMock) - driver.DhcpLeasesPathResult = tf.Name() - - finder := &DHCPLeaseGuestLookup{ - Driver: driver, - Device: "vmnet8", - MACAddress: "00:0c:29:59:91:02", - } - - ip, err := finder.GuestIP() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !driver.DhcpLeasesPathCalled { - t.Fatal("should ask for DHCP leases path") - } - if driver.DhcpLeasesPathDevice != "vmnet8" { - t.Fatal("should be vmnet8") - } - - if ip != "192.168.126.130" { - t.Fatalf("bad: %#v", ip) - } -} - -const testLeaseContents = ` -# All times in this file are in UTC (GMT), not your local timezone. This is -# not a bug, so please don't ask about it. There is no portable way to -# store leases in the local timezone, so please don't request this as a -# feature. If this is inconvenient or confusing to you, we sincerely -# apologize. Seriously, though - don't ask. -# The format of this file is documented in the dhcpd.leases(5) manual page. - -lease 192.168.126.129 { - starts 0 2013/09/15 23:58:51; - ends 1 2013/09/16 00:28:51; - hardware ethernet 00:0c:29:59:91:02; - client-hostname "precise64"; -} -lease 192.168.126.130 { - starts 2 2013/09/17 21:39:07; - ends 2 2013/09/17 22:09:07; - hardware ethernet 00:0c:29:59:91:02; - client-hostname "precise64"; -} -lease 192.168.126.128 { - starts 0 2013/09/15 20:09:59; - ends 0 2013/09/15 20:21:58; - hardware ethernet 00:0c:29:59:91:02; - client-hostname "precise64"; -} -lease 192.168.126.127 { - starts 0 2013/09/15 20:09:59; - ends 0 2013/09/15 20:21:58; - hardware ethernet 01:0c:29:59:91:02; - client-hostname "precise64"; - -` diff --git a/builder/vmware/common/host_ip.go b/builder/vmware/common/host_ip.go deleted file mode 100644 index f920043e7..000000000 --- a/builder/vmware/common/host_ip.go +++ /dev/null @@ -1,7 +0,0 @@ -package common - -// Interface to help find the host IP that is available from within -// the VMware virtual machines. -type HostIPFinder interface { - HostIP() (string, error) -} diff --git a/builder/vmware/common/host_ip_ifconfig_test.go b/builder/vmware/common/host_ip_ifconfig_test.go deleted file mode 100644 index a69104e69..000000000 --- a/builder/vmware/common/host_ip_ifconfig_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -import "testing" - -func TestIfconfigIPFinder_Impl(t *testing.T) { - var raw interface{} - raw = &IfconfigIPFinder{} - if _, ok := raw.(HostIPFinder); !ok { - t.Fatalf("IfconfigIPFinder is not a host IP finder") - } -} diff --git a/builder/vmware/common/host_ip_vmnetnatconf.go b/builder/vmware/common/host_ip_vmnetnatconf.go deleted file mode 100644 index e07227a5e..000000000 --- a/builder/vmware/common/host_ip_vmnetnatconf.go +++ /dev/null @@ -1,65 +0,0 @@ -package common - -import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "regexp" - "strings" -) - -// VMnetNatConfIPFinder finds the IP address of the host machine by -// retrieving the IP from the vmnetnat.conf. This isn't a full proof -// technique but so far it has not failed. -type VMnetNatConfIPFinder struct{} - -func (*VMnetNatConfIPFinder) HostIP() (string, error) { - driver := &Workstation9Driver{} - - vmnetnat := driver.VmnetnatConfPath() - if vmnetnat == "" { - return "", errors.New("Could not find NAT vmnet conf file") - } - - if _, err := os.Stat(vmnetnat); err != nil { - return "", fmt.Errorf("Could not find NAT vmnet conf file: %s", vmnetnat) - } - - f, err := os.Open(vmnetnat) - if err != nil { - return "", err - } - defer f.Close() - - ipRe := regexp.MustCompile(`^\s*ip\s*=\s*(.+?)\s*$`) - - r := bufio.NewReader(f) - for { - line, err := r.ReadString('\n') - if line != "" { - matches := ipRe.FindStringSubmatch(line) - if matches != nil { - ip := matches[1] - dotIndex := strings.LastIndex(ip, ".") - if dotIndex == -1 { - continue - } - - ip = ip[0:dotIndex] + ".1" - return ip, nil - } - } - - if err == io.EOF { - break - } - - if err != nil { - return "", err - } - } - - return "", errors.New("host IP not found in " + vmnetnat) -} diff --git a/builder/vmware/common/host_ip_vmnetnatconf_test.go b/builder/vmware/common/host_ip_vmnetnatconf_test.go deleted file mode 100644 index 9f3114a92..000000000 --- a/builder/vmware/common/host_ip_vmnetnatconf_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -import "testing" - -func TestVMnetNatConfIPFinder_Impl(t *testing.T) { - var raw interface{} - raw = &VMnetNatConfIPFinder{} - if _, ok := raw.(HostIPFinder); !ok { - t.Fatalf("VMnetNatConfIPFinder is not a host IP finder") - } -} diff --git a/builder/vmware/common/ssh.go b/builder/vmware/common/ssh.go index 4e339edb9..e03f9b6bf 100644 --- a/builder/vmware/common/ssh.go +++ b/builder/vmware/common/ssh.go @@ -14,34 +14,12 @@ import ( func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) { driver := state.Get("driver").(Driver) - vmxPath := state.Get("vmx_path").(string) if config.Comm.SSHHost != "" { return config.Comm.SSHHost, nil } - log.Println("Lookup up IP information...") - - vmxData, err := ReadVMX(vmxPath) - if err != nil { - return "", err - } - - var ok bool - macAddress := "" - if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" { - if macAddress, ok = vmxData["ethernet0.generatedaddress"]; !ok || macAddress == "" { - return "", errors.New("couldn't find MAC address in VMX") - } - } - - ipLookup := &DHCPLeaseGuestLookup{ - Driver: driver, - Device: "vmnet8", - MACAddress: macAddress, - } - - ipAddress, err := ipLookup.GuestIP() + ipAddress, err := driver.GuestIP(state) if err != nil { log.Printf("IP lookup failed: %s", err) return "", fmt.Errorf("IP lookup failed: %s", err) diff --git a/builder/vmware/common/step_shutdown_test.go b/builder/vmware/common/step_shutdown_test.go index ed0eae486..ff775f2f2 100644 --- a/builder/vmware/common/step_shutdown_test.go +++ b/builder/vmware/common/step_shutdown_test.go @@ -116,6 +116,10 @@ func TestStepShutdown_noCommand(t *testing.T) { } func TestStepShutdown_locks(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + state := testStepShutdownState(t) step := new(StepShutdown) step.Testing = true diff --git a/builder/vmware/common/step_type_boot_command.go b/builder/vmware/common/step_type_boot_command.go index 35e30c624..02404af91 100644 --- a/builder/vmware/common/step_type_boot_command.go +++ b/builder/vmware/common/step_type_boot_command.go @@ -7,7 +7,6 @@ import ( "net" "os" "regexp" - "runtime" "strings" "time" "unicode" @@ -64,7 +63,7 @@ func (s *StepTypeBootCommand) Run(_ context.Context, state multistep.StateBag) m } // Connect to VNC - ui.Say("Connecting to VM via VNC") + ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIp, vncPort)) nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort)) if err != nil { err := fmt.Errorf("Error connecting to VNC: %s", err) @@ -94,16 +93,7 @@ func (s *StepTypeBootCommand) Run(_ context.Context, state multistep.StateBag) m log.Printf("Connected to VNC desktop: %s", c.DesktopName) // Determine the host IP - var ipFinder HostIPFinder - if finder, ok := driver.(HostIPFinder); ok { - ipFinder = finder - } else if runtime.GOOS == "windows" { - ipFinder = new(VMnetNatConfIPFinder) - } else { - ipFinder = &IfconfigIPFinder{Device: "vmnet8"} - } - - hostIP, err := ipFinder.HostIP() + hostIP, err := driver.HostIP(state) if err != nil { err := fmt.Errorf("Error detecting host IP: %s", err) state.Put("error", err) diff --git a/builder/vmware/iso/builder.go b/builder/vmware/iso/builder.go index 1d2a29ac5..cc5a9673b 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -38,21 +38,43 @@ type Config struct { vmwcommon.ToolsConfig `mapstructure:",squash"` vmwcommon.VMXConfig `mapstructure:",squash"` - AdditionalDiskSize []uint `mapstructure:"disk_additional_size"` - DiskName string `mapstructure:"vmdk_name"` - DiskSize uint `mapstructure:"disk_size"` - DiskTypeId string `mapstructure:"disk_type_id"` - Format string `mapstructure:"format"` - GuestOSType string `mapstructure:"guest_os_type"` + // 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"` + + // booting a guest KeepRegistered bool `mapstructure:"keep_registered"` OVFToolOptions []string `mapstructure:"ovftool_options"` SkipCompaction bool `mapstructure:"skip_compaction"` SkipExport bool `mapstructure:"skip_export"` - VMName string `mapstructure:"vm_name"` VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"` VMXTemplatePath string `mapstructure:"vmx_template_path"` - Version string `mapstructure:"version"` + // remote vsphere RemoteType string `mapstructure:"remote_type"` RemoteDatastore string `mapstructure:"remote_datastore"` RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"` @@ -109,6 +131,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { b.config.DiskSize = 40000 } + if b.config.DiskAdapterType == "" { + // Default is lsilogic + b.config.DiskAdapterType = "lsilogic" + } + if b.config.DiskTypeId == "" { // Default is growable virtual disk split in 2GB files. b.config.DiskTypeId = "1" @@ -158,6 +185,18 @@ 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 == "" { diff --git a/builder/vmware/iso/driver_esx5.go b/builder/vmware/iso/driver_esx5.go index f30451c65..1dc352999 100644 --- a/builder/vmware/iso/driver_esx5.go +++ b/builder/vmware/iso/driver_esx5.go @@ -14,6 +14,7 @@ import ( "strings" "time" + vmwcommon "github.com/hashicorp/packer/builder/vmware/common" commonssh "github.com/hashicorp/packer/common/ssh" "github.com/hashicorp/packer/communicator/ssh" "github.com/hashicorp/packer/helper/multistep" @@ -24,6 +25,8 @@ import ( // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build // virtual machines. This driver can only manage one machine at a time. type ESX5Driver struct { + base vmwcommon.VmwareDriver + Host string Port uint Username string @@ -46,9 +49,9 @@ func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { return nil } -func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, typeId string) error { +func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, adapter_type string, typeId string) error { diskPath := d.datastorePath(diskPathLocal) - return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", "lsilogic", diskPath) + return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", adapter_type, diskPath) } func (d *ESX5Driver) IsRunning(string) (bool, error) { @@ -143,10 +146,6 @@ func (d *ESX5Driver) ToolsInstall() error { return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId) } -func (d *ESX5Driver) DhcpLeasesPath(string) string { - return "" -} - func (d *ESX5Driver) Verify() error { checks := []func() error{ d.connect, @@ -159,11 +158,10 @@ func (d *ESX5Driver) Verify() error { return err } } - return nil } -func (d *ESX5Driver) HostIP() (string, error) { +func (d *ESX5Driver) HostIP(multistep.StateBag) (string, error) { conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) if err != nil { return "", err @@ -174,6 +172,101 @@ func (d *ESX5Driver) HostIP() (string, error) { return host, err } +func (d *ESX5Driver) GuestIP(multistep.StateBag) (string, error) { + // GuestIP is defined by the user as d.Host..but let's validate it just to be sure + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) + defer conn.Close() + if err != nil { + return "", err + } + + host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) + return host, err +} + +func (d *ESX5Driver) HostAddress(multistep.StateBag) (string, error) { + // make a connection + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) + defer conn.Close() + if err != nil { + return "", err + } + + // get the local address (the host) + host, _, err := net.SplitHostPort(conn.LocalAddr().String()) + if err != nil { + return "", fmt.Errorf("Unable to determine host address for ESXi: %v", err) + } + + // iterate through all the interfaces.. + interfaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("Unable to enumerate host interfaces : %v", err) + } + + for _, intf := range interfaces { + addrs, err := intf.Addrs() + if err != nil { + continue + } + + // ..checking to see if any if it's addrs match the host address + for _, addr := range addrs { + if addr.String() == host { // FIXME: Is this the proper way to compare two HardwareAddrs? + return intf.HardwareAddr.String(), nil + } + } + } + + // ..unfortunately nothing was found + return "", fmt.Errorf("Unable to locate interface matching host address in ESXi: %v", host) +} + +func (d *ESX5Driver) GuestAddress(multistep.StateBag) (string, error) { + // list all the interfaces on the esx host + r, err := d.esxcli("network", "ip", "interface", "list") + if err != nil { + return "", fmt.Errorf("Could not retrieve network interfaces for ESXi: %v", err) + } + + // rip out the interface name and the MAC address from the csv output + addrs := make(map[string]string) + for record, err := r.read(); record != nil && err == nil; record, err = r.read() { + if strings.ToUpper(record["Enabled"]) != "TRUE" { + continue + } + addrs[record["Name"]] = record["MAC Address"] + } + + // list all the addresses on the esx host + r, err = d.esxcli("network", "ip", "interface", "ipv4", "get") + if err != nil { + return "", fmt.Errorf("Could not retrieve network addresses for ESXi: %v", err) + } + + // figure out the interface name that matches the specified d.Host address + var intf string + intf = "" + for record, err := r.read(); record != nil && err == nil; record, err = r.read() { + if record["IPv4 Address"] == d.Host && record["Name"] != "" { + intf = record["Name"] + break + } + } + if intf == "" { + return "", fmt.Errorf("Unable to find matching address for ESXi guest") + } + + // find the MAC address according to the interface name + result, ok := addrs[intf] + if !ok { + return "", fmt.Errorf("Unable to find address for ESXi guest interface") + } + + // ..and we're good + return result, nil +} + func (d *ESX5Driver) VNCAddress(_ string, portMin, portMax uint) (string, uint, error) { var vncPort uint @@ -531,6 +624,10 @@ func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) { return &esxcliReader{r, header}, nil } +func (d *ESX5Driver) GetVmwareDriver() vmwcommon.VmwareDriver { + return d.base +} + type esxcliReader struct { cr *csv.Reader header []string diff --git a/builder/vmware/iso/driver_esx5_test.go b/builder/vmware/iso/driver_esx5_test.go index 0c0ef757e..097d6e186 100644 --- a/builder/vmware/iso/driver_esx5_test.go +++ b/builder/vmware/iso/driver_esx5_test.go @@ -50,8 +50,9 @@ func TestESX5Driver_HostIP(t *testing.T) { defer listen.Close() driver := ESX5Driver{Host: "localhost", Port: uint(port)} + state := new(multistep.BasicStateBag) - if host, _ := driver.HostIP(); host != expected_host { + if host, _ := driver.HostIP(state); host != expected_host { t.Error(fmt.Sprintf("Expected string, %s but got %s", expected_host, host)) } } diff --git a/builder/vmware/iso/step_create_disk.go b/builder/vmware/iso/step_create_disk.go index ddee55c7f..788de2ddc 100644 --- a/builder/vmware/iso/step_create_disk.go +++ b/builder/vmware/iso/step_create_disk.go @@ -28,7 +28,7 @@ func (stepCreateDisk) Run(_ context.Context, state multistep.StateBag) multistep ui.Say("Creating virtual machine disk") full_disk_path := filepath.Join(config.OutputDir, config.DiskName+".vmdk") - if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskTypeId); err != nil { + if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskAdapterType, config.DiskTypeId); err != nil { err := fmt.Errorf("Error creating disk: %s", err) state.Put("error", err) ui.Error(err.Error()) @@ -46,7 +46,7 @@ func (stepCreateDisk) Run(_ context.Context, state multistep.StateBag) multistep additionalpath := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d.vmdk", config.DiskName, i+1)) size := fmt.Sprintf("%dM", uint64(additionalsize)) - if err := driver.CreateDisk(additionalpath, size, config.DiskTypeId); err != nil { + if err := driver.CreateDisk(additionalpath, size, config.DiskAdapterType, config.DiskTypeId); err != nil { err := fmt.Errorf("Error creating additional disk: %s", err) state.Put("error", err) ui.Error(err.Error()) diff --git a/builder/vmware/iso/step_create_vmx.go b/builder/vmware/iso/step_create_vmx.go index 029f2a31e..ffff9d999 100644 --- a/builder/vmware/iso/step_create_vmx.go +++ b/builder/vmware/iso/step_create_vmx.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" + "strings" vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/helper/multistep" @@ -14,11 +16,40 @@ import ( ) type vmxTemplateData struct { - Name string - GuestOS string - DiskName string - ISOPath string - Version string + Name string + GuestOS string + ISOPath string + Version string + + SCSI_Present string + SCSI_diskAdapterType string + SATA_Present string + NVME_Present string + + DiskName string + DiskType string + CDROMType string + CDROMType_MasterSlave string + + Network_Type string + Network_Device string + Network_Adapter 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 { @@ -39,11 +70,238 @@ type stepCreateVMX struct { tempDir string } +/* serial conversions */ +type serialConfigPipe struct { + filename string + endpoint string + host string + yield string +} + +type serialConfigFile struct { + filename string + yield string +} + +type serialConfigDevice struct { + devicename string + yield string +} + +type serialConfigAuto struct { + devicename string + yield string +} + +type serialUnion struct { + serialType interface{} + pipe *serialConfigPipe + file *serialConfigFile + device *serialConfigDevice + auto *serialConfigAuto +} + +func unformat_serial(config string) (*serialUnion, error) { + var defaultSerialPort string + if runtime.GOOS == "windows" { + defaultSerialPort = "COM1" + } else { + defaultSerialPort = "/dev/ttyS0" + } + + input := strings.SplitN(config, ":", 2) + if len(input) < 1 { + return nil, fmt.Errorf("Unexpected format for serial port: %s", config) + } + + var formatType, formatOptions string + formatType = input[0] + if len(input) == 2 { + formatOptions = input[1] + } else { + formatOptions = "" + } + + switch strings.ToUpper(formatType) { + case "PIPE": + comp := strings.Split(formatOptions, ",") + if len(comp) < 3 || len(comp) > 4 { + return nil, fmt.Errorf("Unexpected format for serial port : pipe : %s", config) + } + if res := strings.ToLower(comp[1]); res != "client" && res != "server" { + return nil, fmt.Errorf("Unexpected format for serial port : pipe : endpoint : %s : %s", res, config) + } + if res := strings.ToLower(comp[2]); res != "app" && res != "vm" { + return nil, fmt.Errorf("Unexpected format for serial port : pipe : host : %s : %s", res, config) + } + res := &serialConfigPipe{ + filename: comp[0], + endpoint: comp[1], + host: map[string]string{"app": "TRUE", "vm": "FALSE"}[strings.ToLower(comp[2])], + yield: "FALSE", + } + if len(comp) == 4 { + res.yield = strings.ToUpper(comp[3]) + } + if res.yield != "TRUE" && res.yield != "FALSE" { + return nil, fmt.Errorf("Unexpected format for serial port : pipe : yield : %s : %s", res.yield, config) + } + return &serialUnion{serialType: res, pipe: res}, nil + + case "FILE": + comp := strings.Split(formatOptions, ",") + if len(comp) > 2 { + return nil, fmt.Errorf("Unexpected format for serial port : file : %s", config) + } + + res := &serialConfigFile{yield: "FALSE"} + + res.filename = filepath.FromSlash(comp[0]) + + res.yield = map[bool]string{true: strings.ToUpper(comp[0]), false: "FALSE"}[len(comp) > 1] + if res.yield != "TRUE" && res.yield != "FALSE" { + return nil, fmt.Errorf("Unexpected format for serial port : file : yield : %s : %s", res.yield, config) + } + + return &serialUnion{serialType: res, file: res}, nil + + case "DEVICE": + comp := strings.Split(formatOptions, ",") + if len(comp) > 2 { + return nil, fmt.Errorf("Unexpected format for serial port : device : %s", config) + } + + res := new(serialConfigDevice) + + if len(comp) == 2 { + res.devicename = map[bool]string{true: filepath.FromSlash(comp[0]), false: defaultSerialPort}[len(comp[0]) > 0] + res.yield = strings.ToUpper(comp[1]) + } else if len(comp) == 1 { + res.devicename = map[bool]string{true: filepath.FromSlash(comp[0]), false: defaultSerialPort}[len(comp[0]) > 0] + res.yield = "FALSE" + } else if len(comp) == 0 { + res.devicename = defaultSerialPort + res.yield = "FALSE" + } + + if res.yield != "TRUE" && res.yield != "FALSE" { + return nil, fmt.Errorf("Unexpected format for serial port : device : yield : %s : %s", res.yield, config) + } + + return &serialUnion{serialType: res, device: res}, nil + + case "AUTO": + res := new(serialConfigAuto) + res.devicename = defaultSerialPort + + if len(formatOptions) > 0 { + res.yield = strings.ToUpper(formatOptions) + } else { + res.yield = "FALSE" + } + + if res.yield != "TRUE" && res.yield != "FALSE" { + return nil, fmt.Errorf("Unexpected format for serial port : auto : yield : %s : %s", res.yield, config) + } + + return &serialUnion{serialType: res, auto: res}, nil + + case "NONE": + return &serialUnion{serialType: nil}, nil + + default: + return nil, fmt.Errorf("Unknown serial type : %s : %s", strings.ToUpper(formatType), config) + } +} + +/* parallel port */ +type parallelUnion struct { + parallelType interface{} + file *parallelPortFile + device *parallelPortDevice + auto *parallelPortAuto +} +type parallelPortFile struct { + filename string +} +type parallelPortDevice struct { + bidirectional string + devicename string +} +type parallelPortAuto struct { + bidirectional string +} + +func unformat_parallel(config string) (*parallelUnion, error) { + input := strings.SplitN(config, ":", 2) + if len(input) < 1 { + return nil, fmt.Errorf("Unexpected format for parallel port: %s", config) + } + + var formatType, formatOptions string + formatType = input[0] + if len(input) == 2 { + formatOptions = input[1] + } else { + formatOptions = "" + } + + switch strings.ToUpper(formatType) { + case "FILE": + res := ¶llelPortFile{filename: filepath.FromSlash(formatOptions)} + return ¶llelUnion{parallelType: res, file: res}, nil + case "DEVICE": + comp := strings.Split(formatOptions, ",") + if len(comp) < 1 || len(comp) > 2 { + return nil, fmt.Errorf("Unexpected format for parallel port: %s", config) + } + res := new(parallelPortDevice) + res.bidirectional = "FALSE" + res.devicename = filepath.FromSlash(comp[0]) + if len(comp) > 1 { + switch strings.ToUpper(comp[1]) { + case "BI": + res.bidirectional = "TRUE" + case "UNI": + res.bidirectional = "FALSE" + default: + return nil, fmt.Errorf("Unknown parallel port direction : %s : %s", strings.ToUpper(comp[0]), config) + } + } + return ¶llelUnion{parallelType: res, device: res}, nil + + case "AUTO": + res := new(parallelPortAuto) + switch strings.ToUpper(formatOptions) { + case "": + fallthrough + case "UNI": + res.bidirectional = "FALSE" + case "BI": + res.bidirectional = "TRUE" + default: + return nil, fmt.Errorf("Unknown parallel port direction : %s : %s", strings.ToUpper(formatOptions), config) + } + return ¶llelUnion{parallelType: res, auto: res}, nil + + case "NONE": + return ¶llelUnion{parallelType: nil}, nil + } + + return nil, fmt.Errorf("Unexpected format for parallel port: %s", config) +} + +/* regular steps */ func (s *stepCreateVMX) Run(_ 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 @@ -111,14 +369,211 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist } } - ctx.Data = &vmxTemplateData{ + 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_MasterSlave: "0", + + Network_Adapter: "e1000", + + Sound_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.Sound)], + Usb_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.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. + diskAdapterType := strings.ToLower(config.DiskAdapterType) + switch diskAdapterType { + case "ide": + templateData.DiskType = "ide" + templateData.CDROMType = "ide" + templateData.CDROMType_MasterSlave = "1" + case "sata": + templateData.SATA_Present = "TRUE" + templateData.DiskType = "sata" + templateData.CDROMType = "sata" + templateData.CDROMType_MasterSlave = "1" + case "nvme": + templateData.NVME_Present = "TRUE" + templateData.DiskType = "nvme" + templateData.SATA_Present = "TRUE" + templateData.CDROMType = "sata" + templateData.CDROMType_MasterSlave = "0" + case "scsi": + diskAdapterType = "lsilogic" + fallthrough + default: + templateData.SCSI_Present = "TRUE" + templateData.SCSI_diskAdapterType = diskAdapterType + templateData.DiskType = "scsi" + templateData.CDROMType = "ide" + templateData.CDROMType_MasterSlave = "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 + // slave 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_MasterSlave = "1" + } else { + templateData.CDROMType_MasterSlave = "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 procesing VMX template: %s", cdromAdapterType) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + /// Assign the network adapter type into the template if one was specified. + network_adapter := strings.ToLower(config.NetworkAdapterType) + if network_adapter != "" { + templateData.Network_Adapter = network_adapter + } + + /// Check the network type that the user specified + network := config.Network + driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver() + + // read netmap config + 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 + device, err := netmap.NameIntoDevice(network) + + // success. so we know that it's an actual network type inside netmap.conf + if err == nil { + templateData.Network_Type = network + templateData.Network_Device = device + // we were unable to find the type, so assume it's a custom network device. + } else { + templateData.Network_Type = "custom" + templateData.Network_Device = network + } + // 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.Serial == "" { + templateData.Serial_Present = "FALSE" + } else { + serial, err := unformat_serial(config.Serial) + if err != nil { + err := fmt.Errorf("Error procesing 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" + + switch serial.serialType.(type) { + case *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 *serialConfigFile: + templateData.Serial_Type = "file" + templateData.Serial_Filename = filepath.FromSlash(serial.file.filename) + case *serialConfigDevice: + templateData.Serial_Type = "device" + templateData.Serial_Filename = filepath.FromSlash(serial.device.devicename) + case *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 procesing VMX template: %v", serial) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + /// check if parallel port has been configured + if config.Parallel == "" { + templateData.Parallel_Present = "FALSE" + } else { + parallel, err := unformat_parallel(config.Parallel) + if err != nil { + err := fmt.Errorf("Error procesing VMX template: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + templateData.Parallel_Auto = "FALSE" + switch parallel.parallelType.(type) { + case *parallelPortFile: + templateData.Parallel_Present = "TRUE" + templateData.Parallel_Filename = filepath.FromSlash(parallel.file.filename) + case *parallelPortDevice: + templateData.Parallel_Present = "TRUE" + templateData.Parallel_Bidirectional = parallel.device.bidirectional + templateData.Parallel_Filename = filepath.FromSlash(parallel.device.devicename) + case *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 procesing VMX template: %v", parallel) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + ctx.Data = &templateData + + /// render the .vmx template vmxContents, err := interpolate.Render(vmxTemplate, &ctx) if err != nil { err := fmt.Errorf("Error procesing VMX template: %s", err) @@ -176,12 +631,13 @@ ehci.pciSlotNumber = "34" ehci.present = "TRUE" ethernet0.addressType = "generated" ethernet0.bsdName = "en0" -ethernet0.connectionType = "nat" +ethernet0.connectionType = "{{ .Network_Type }}" +ethernet0.vnet = "{{ .Network_Device }}" ethernet0.displayName = "Ethernet" ethernet0.linkStatePropagation.enable = "FALSE" ethernet0.pciSlotNumber = "33" ethernet0.present = "TRUE" -ethernet0.virtualDev = "e1000" +ethernet0.virtualDev = "{{ .Network_Adapter }}" ethernet0.wakeOnPcktRcv = "FALSE" extendedConfigFile = "{{ .Name }}.vmxf" floppy0.present = "FALSE" @@ -190,9 +646,21 @@ gui.fullScreenAtPowerOn = "FALSE" gui.viewModeAtPowerOn = "windowed" hgfs.linkRootShare = "TRUE" hgfs.mapRootShare = "TRUE" -ide1:0.present = "TRUE" -ide1:0.fileName = "{{ .ISOPath }}" -ide1:0.deviceType = "cdrom-image" + +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_MasterSlave }}.present = "TRUE" +{{ .CDROMType }}0:{{ .CDROMType_MasterSlave }}.fileName = "{{ .ISOPath }}" +{{ .CDROMType }}0:{{ .CDROMType_MasterSlave }}.deviceType = "cdrom-image" + isolation.tools.hgfs.disable = "FALSE" memsize = "512" nvram = "{{ .Name }}.nvram" @@ -221,17 +689,38 @@ powerType.suspend = "soft" proxyApps.publishToHost = "FALSE" replay.filename = "" replay.supported = "FALSE" -scsi0.pciSlotNumber = "16" -scsi0.present = "TRUE" -scsi0.virtualDev = "lsilogic" -scsi0:0.fileName = "{{ .DiskName }}.vmdk" -scsi0:0.present = "TRUE" -scsi0:0.redo = "" -sound.startConnected = "FALSE" + +// Sound +sound.startConnected = "{{ .Sound_Present }}" +sound.present = "{{ .Sound_Present }}" +sound.fileName = "-1" +sound.autodetect = "TRUE" + tools.syncTime = "TRUE" tools.upgrade.policy = "upgradeAtPowerCycle" + +// USB usb.pciSlotNumber = "32" -usb.present = "FALSE" +usb.present = "{{ .Usb_Present }}" +usb_xhci.present = "TRUE" + +// 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 }}" + virtualHW.productCompatibility = "hosted" virtualHW.version = "{{ .Version }}" vmci0.id = "1861462627" diff --git a/builder/vmware/iso/step_create_vmx_test.go b/builder/vmware/iso/step_create_vmx_test.go new file mode 100644 index 000000000..7553dff82 --- /dev/null +++ b/builder/vmware/iso/step_create_vmx_test.go @@ -0,0 +1,409 @@ +package iso + +import ( + "bytes" + "fmt" + "io/ioutil" + "math" + "math/rand" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/provisioner/shell" + "github.com/hashicorp/packer/template" + "testing" +) + +var vmxTestBuilderConfig = map[string]string{ + "type": `"vmware-iso"`, + "iso_url": `"https://archive.org/download/ut-ttylinux-i686-12.6/ut-ttylinux-i686-12.6.iso"`, + "iso_checksum_type": `"md5"`, + "iso_checksum": `"43c1feeae55a44c6ef694b8eb18408a6"`, + "ssh_username": `"root"`, + "ssh_password": `"password"`, + "ssh_wait_timeout": `"45s"`, + "boot_command": `["","rootpassword","udhcpc"]`, + "shutdown_command": `"/sbin/shutdown -h; exit 0"`, +} + +var vmxTestProvisionerConfig = map[string]string{ + "type": `"shell"`, + "inline": `["echo hola mundo"]`, +} + +const vmxTestTemplate string = `{"builders":[{%s}],"provisioners":[{%s}]}` + +func tmpnam(prefix string) string { + var path string + var err error + + const length = 16 + + dir := os.TempDir() + max := int(math.Pow(2, float64(length))) + + n, err := rand.Intn(max), nil + for path = filepath.Join(dir, prefix+strconv.Itoa(n)); err == nil; _, err = os.Stat(path) { + n = rand.Intn(max) + path = filepath.Join(dir, prefix+strconv.Itoa(n)) + } + return path +} + +func createFloppyOutput(prefix string) (string, string, error) { + output := tmpnam(prefix) + f, err := os.Create(output) + if err != nil { + return "", "", fmt.Errorf("Unable to create empty %s: %s", output, err) + } + f.Close() + + vmxData := []string{ + `"floppy0.present":"TRUE"`, + `"floppy0.fileType":"file"`, + `"floppy0.clientDevice":"FALSE"`, + `"floppy0.fileName":"%s"`, + `"floppy0.startConnected":"TRUE"`, + } + + outputFile := strings.Replace(output, "\\", "\\\\", -1) + vmxString := fmt.Sprintf("{"+strings.Join(vmxData, ",")+"}", outputFile) + return output, vmxString, nil +} + +func readFloppyOutput(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("Unable to open file %s", path) + } + defer f.Close() + data, err := ioutil.ReadAll(f) + if err != nil { + return "", fmt.Errorf("Unable to read file: %s", err) + } + if len(data) == 0 { + return "", nil + } + return string(data[:bytes.IndexByte(data, 0)]), nil +} + +func setupVMwareBuild(t *testing.T, builderConfig map[string]string, provisionerConfig map[string]string) error { + ui := packer.TestUi(t) + + // create builder config and update with user-supplied options + cfgBuilder := map[string]string{} + for k, v := range vmxTestBuilderConfig { + cfgBuilder[k] = v + } + for k, v := range builderConfig { + cfgBuilder[k] = v + } + + // convert our builder config into a single sprintfable string + builderLines := []string{} + for k, v := range cfgBuilder { + builderLines = append(builderLines, fmt.Sprintf(`"%s":%s`, k, v)) + } + + // create provisioner config and update with user-supplied options + cfgProvisioner := map[string]string{} + for k, v := range vmxTestProvisionerConfig { + cfgProvisioner[k] = v + } + for k, v := range provisionerConfig { + cfgProvisioner[k] = v + } + + // convert our provisioner config into a single sprintfable string + provisionerLines := []string{} + for k, v := range cfgProvisioner { + provisionerLines = append(provisionerLines, fmt.Sprintf(`"%s":%s`, k, v)) + } + + // and now parse them into a template + configString := fmt.Sprintf(vmxTestTemplate, strings.Join(builderLines, `,`), strings.Join(provisionerLines, `,`)) + + tpl, err := template.Parse(strings.NewReader(configString)) + if err != nil { + t.Fatalf("Unable to parse test config: %s", err) + } + + // create our config to test the vmware-iso builder + components := packer.ComponentFinder{ + Builder: func(n string) (packer.Builder, error) { + return &Builder{}, nil + }, + Hook: func(n string) (packer.Hook, error) { + return &packer.DispatchHook{}, nil + }, + PostProcessor: func(n string) (packer.PostProcessor, error) { + return &packer.MockPostProcessor{}, nil + }, + Provisioner: func(n string) (packer.Provisioner, error) { + return &shell.Provisioner{}, nil + }, + } + config := packer.CoreConfig{ + Template: tpl, + Components: components, + } + + // create a core using our template + core, err := packer.NewCore(&config) + if err != nil { + t.Fatalf("Unable to create core: %s", err) + } + + // now we can prepare our build + b, err := core.Build("vmware-iso") + if err != nil { + t.Fatalf("Unable to create build: %s", err) + } + + warn, err := b.Prepare() + if len(warn) > 0 { + for _, w := range warn { + t.Logf("Configuration warning: %s", w) + } + } + + // and then finally build it + cache := &packer.FileCache{CacheDir: os.TempDir()} + artifacts, err := b.Run(ui, cache) + if err != nil { + t.Fatalf("Failed to build artifact: %s", err) + } + + // check to see that we only got one artifact back + if len(artifacts) == 1 { + return artifacts[0].Destroy() + } + + // otherwise some number of errors happened + t.Logf("Unexpected number of artifacts returned: %d", len(artifacts)) + errors := make([]error, 0) + for _, artifact := range artifacts { + if err := artifact.Destroy(); err != nil { + errors = append(errors, err) + } + } + if len(errors) > 0 { + t.Errorf("%d Errors returned while trying to destroy artifacts", len(errors)) + return fmt.Errorf("Error while trying to destroy artifacts: %v", errors) + } + return nil +} + +func TestStepCreateVmx_SerialFile(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + + tmpfile := tmpnam("SerialFileInput.") + + serialConfig := map[string]string{ + "serial": fmt.Sprintf(`"file:%s"`, filepath.ToSlash(tmpfile)), + } + + error := setupVMwareBuild(t, serialConfig, map[string]string{}) + if error != nil { + t.Errorf("Unable to read file: %s", error) + } + + f, err := os.Stat(tmpfile) + if err != nil { + t.Errorf("VMware builder did not create a file for serial port: %s", err) + } + + if f != nil { + if err := os.Remove(tmpfile); err != nil { + t.Fatalf("Unable to remove file %s: %s", tmpfile, err) + } + } +} + +func TestStepCreateVmx_SerialPort(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + + var defaultSerial string + if runtime.GOOS == "windows" { + defaultSerial = "COM1" + } else { + defaultSerial = "/dev/ttyS0" + } + + config := map[string]string{ + "serial": fmt.Sprintf(`"device:%s"`, filepath.ToSlash(defaultSerial)), + } + provision := map[string]string{ + "inline": `"dmesg | egrep -o '^serial8250: ttyS1 at' > /dev/fd0"`, + } + + // where to write output + output, vmxData, err := createFloppyOutput("SerialPortOutput.") + if err != nil { + t.Fatalf("Error creating output: %s", err) + } + defer func() { + if _, err := os.Stat(output); err == nil { + os.Remove(output) + } + }() + config["vmx_data"] = vmxData + t.Logf("Preparing to write output to %s", output) + + // whee + err = setupVMwareBuild(t, config, provision) + if err != nil { + t.Errorf("%s", err) + } + + // check the output + data, err := readFloppyOutput(output) + if err != nil { + t.Errorf("%s", err) + } + + if data != "serial8250: ttyS1 at\n" { + t.Errorf("Serial port not detected : %v", data) + } +} + +func TestStepCreateVmx_ParallelPort(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + + var defaultParallel string + if runtime.GOOS == "windows" { + defaultParallel = "LPT1" + } else { + defaultParallel = "/dev/lp0" + } + + config := map[string]string{ + "parallel": fmt.Sprintf(`"device:%s,uni"`, filepath.ToSlash(defaultParallel)), + } + provision := map[string]string{ + "inline": `"cat /proc/modules | egrep -o '^parport ' > /dev/fd0"`, + } + + // where to write output + output, vmxData, err := createFloppyOutput("ParallelPortOutput.") + if err != nil { + t.Fatalf("Error creating output: %s", err) + } + defer func() { + if _, err := os.Stat(output); err == nil { + os.Remove(output) + } + }() + config["vmx_data"] = vmxData + t.Logf("Preparing to write output to %s", output) + + // whee + error := setupVMwareBuild(t, config, provision) + if error != nil { + t.Errorf("%s", error) + } + + // check the output + data, err := readFloppyOutput(output) + if err != nil { + t.Errorf("%s", err) + } + + if data != "parport \n" { + t.Errorf("Parallel port not detected : %v", data) + } +} + +func TestStepCreateVmx_Usb(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + + config := map[string]string{ + "usb": `"TRUE"`, + } + provision := map[string]string{ + "inline": `"dmesg | egrep -m1 -o 'USB hub found$' > /dev/fd0"`, + } + + // where to write output + output, vmxData, err := createFloppyOutput("UsbOutput.") + if err != nil { + t.Fatalf("Error creating output: %s", err) + } + defer func() { + if _, err := os.Stat(output); err == nil { + os.Remove(output) + } + }() + config["vmx_data"] = vmxData + t.Logf("Preparing to write output to %s", output) + + // whee + error := setupVMwareBuild(t, config, provision) + if error != nil { + t.Errorf("%s", error) + } + + // check the output + data, err := readFloppyOutput(output) + if err != nil { + t.Errorf("%s", err) + } + + if data != "USB hub found\n" { + t.Errorf("USB support not detected : %v", data) + } +} + +func TestStepCreateVmx_Sound(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.") + } + + config := map[string]string{ + "sound": `"TRUE"`, + } + provision := map[string]string{ + "inline": `"cat /proc/modules | egrep -o '^soundcore' > /dev/fd0"`, + } + + // where to write output + output, vmxData, err := createFloppyOutput("SoundOutput.") + if err != nil { + t.Fatalf("Error creating output: %s", err) + } + defer func() { + if _, err := os.Stat(output); err == nil { + os.Remove(output) + } + }() + config["vmx_data"] = vmxData + t.Logf("Preparing to write output to %s", output) + + // whee + error := setupVMwareBuild(t, config, provision) + if error != nil { + t.Errorf("Unable to read file: %s", error) + } + + // check the output + data, err := readFloppyOutput(output) + if err != nil { + t.Errorf("%s", err) + } + + if data != "soundcore\n" { + t.Errorf("Soundcard not detected : %v", data) + } +} diff --git a/builder/vmware/vmx/step_clone_vmx.go b/builder/vmware/vmx/step_clone_vmx.go index f7874067c..bd644ca30 100644 --- a/builder/vmware/vmx/step_clone_vmx.go +++ b/builder/vmware/vmx/step_clone_vmx.go @@ -54,6 +54,16 @@ func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multiste return multistep.ActionHalt } + var networkType string + if _, ok := vmxData["ethernet0.connectionType"]; ok { + networkType = vmxData["ethernet0.connectionType"] + } + if networkType == "" { + networkType = "nat" + log.Printf("Defaulting to network type : nat") + } + + state.Put("vmnetwork", networkType) state.Put("full_disk_path", filepath.Join(s.OutputDir, diskName)) state.Put("vmx_path", vmxPath) return multistep.ActionContinue diff --git a/website/source/assets/images/Adobe_PDF_file_icon_24x24.png b/website/source/assets/images/Adobe_PDF_file_icon_24x24.png new file mode 100644 index 000000000..a722e5b33 Binary files /dev/null and b/website/source/assets/images/Adobe_PDF_file_icon_24x24.png differ diff --git a/website/source/docs/builders/vmware-iso.html.md b/website/source/docs/builders/vmware-iso.html.md index 256bf63e4..2f4e3e44f 100644 --- a/website/source/docs/builders/vmware-iso.html.md +++ b/website/source/docs/builders/vmware-iso.html.md @@ -131,7 +131,22 @@ builder. Guide](https://www.vmware.com/pdf/VirtualDiskManager.pdf) for desktop VMware clients. For ESXi, refer to the proper ESXi documentation. -* `disable_vnc` (boolean) - Whether to create a VNC connection or not. +- `disk_adapter_type` (string) - The adapter type of the VMware virtual disk + to create. This option is for advanced usage, modify only if you know what + you're doing. Some of the options you can specify are "ide", "sata", "nvme" + or "scsi" (which uses the "lsilogic" scsi interface by default). If you + specify another option, Packer will assume that you're specifying a "scsi" + interface of that specified type. For more information, please consult the + Virtual Disk Manager User's Guide for desktop VMware clients. + For ESXi, refer to the proper ESXi documentation. + +- `cdrom_adapter_type` (string) - The adapter type (or bus) that will be used + by the cdrom device. This is chosen by default based on the disk adapter + type. VMware tends to lean towards "ide" for the cdrom device unless + "sata" is chosen for the disk adapter and so Packer attempts to mirror + this logic. This field can be specified as either "ide", "sata", or "scsi". + +- `disable_vnc` (boolean) - Whether to create a VNC connection or not. A `boot_command` cannot be used when this is `false`. Defaults to `false`. - `floppy_files` (array of strings) - A list of files to place onto a floppy @@ -193,6 +208,17 @@ builder. URLs must point to the same file (same checksum). By default this is empty and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified. +- `network` (string) - This is the network type that the virtual machine will + be created with. This can be one of the generic values that map to a device + such as "hostonly", "nat", or "bridged". If the network is not one of these + values, then it is assumed to be a VMware network device. (VMnet0..x) + +- `network_adapter_type` (string) - This is the ethernet adapter type the the + virtual machine will be created with. By default the "e1000" network adapter + type will be used by Packer. For more information, please consult the + Choosing a network adapter for your virtual machine for desktop VMware + clients. For ESXi, refer to the proper ESXi documentation. + - `output_directory` (string) - This is the path to the directory where the resulting virtual machine will be created. This may be relative or absolute. If relative, the path is relative to the working directory when `packer` @@ -200,6 +226,20 @@ builder. the builder. By default this is "output-BUILDNAME" where "BUILDNAME" is the name of the build. +- `parallel` (string) - This specifies a parallel port to add to the VM. It + has the format of `Type:option1,option2,...`. Type can be one of the + following values: "FILE", "DEVICE", "AUTO", or "NONE". + + * `FILE:path` - Specifies the path to the local file to be used for the + parallel port. + * `DEVICE:path` - Specifies the path to the local device to be used for the + parallel port. + * `AUTO:direction` - Specifies to use auto-detection to determine the + parallel port. Direction can be `BI` to specify + bidirectional communication or `UNI` to specify + unidirectional communication. + * `NONE` - Specifies to not use a parallel port. (default) + - `remote_cache_datastore` (string) - The path to the datastore where supporting files will be stored during the build on the remote machine. By default this is the same as the `remote_datastore` option. This only has an @@ -234,6 +274,41 @@ builder. - `remote_username` (string) - The username for the SSH user that will access the remote machine. This is required if `remote_type` is enabled. +- `serial` (string) - This specifies a serial port to add to the VM. + It has a format of `Type:option1,option2,...`. The field `Type` can be one + of the following values: `FILE`, `DEVICE`, `PIPE`, `AUTO`, or `NONE`. + + * `FILE:path(,yield)` - Specifies the path to the local file to be used as the + serial port. + * `yield` (bool) - This is an optional boolean that specifies whether + the vm should yield the cpu when polling the port. + By default, the builder will assume this as `FALSE`. + * `DEVICE:path(,yield)` - Specifies the path to the local device to be used + as the serial port. If `path` is empty, then + default to the first serial port. + * `yield` (bool) - This is an optional boolean that specifies whether + the vm should yield the cpu when polling the port. + By default, the builder will assume this as `FALSE`. + * `PIPE:path,endpoint,host(,yield)` - Specifies to use the named-pipe "path" + as a serial port. This has a few + options that determine how the VM + should use the named-pipe. + * `endpoint` (string) - Chooses the type of the VM-end, which can be + either a `client` or `server`. + * `host` (string) - Chooses the type of the host-end, which can be either + an `app` (application) or `vm` (another virtual-machine). + * `yield` (bool) - This is an optional boolean that specifies whether + the vm should yield the cpu when polling the port. + By default, the builder will assume this as `FALSE`. + + * `AUTO:(yield)` - Specifies to use auto-detection to determine the serial + port to use. This has one option to determine how the VM + should support the serial port. + * `yield` (bool) - This is an optional boolean that specifies whether + the vm should yield the cpu when polling the port. + By default, the builder will assume this as `FALSE`. + * `NONE` - Specifies to not use a serial port. (default) + - `shutdown_command` (string) - The command to use to gracefully shut down the machine once all the provisioning is done. By default this is an empty string, which tells Packer to just forcefully shut down the machine. @@ -264,6 +339,8 @@ builder. `--noSSLVerify`, `--skipManifestCheck`, and `--targetType` are reserved, and should not be passed to this argument. +- `sound` (boolean) - Enable VMware's virtual soundcard device for the VM. + - `tools_upload_flavor` (string) - The flavor of the VMware Tools ISO to upload into the VM. Valid values are "darwin", "linux", and "windows". By default, this is empty, which means VMware tools won't be uploaded. @@ -276,6 +353,8 @@ builder. By default the upload path is set to `{{.Flavor}}.iso`. This setting is not used when `remote_type` is "esx5". +- `usb` (boolean) - Enable VMware's USB bus for the VM. + - `version` (string) - The [vmx hardware version](http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=1003746) for the new virtual machine. Only the default value has been tested, any