diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index d5f8b44f4..1098e591f 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -1,14 +1,19 @@ package common import ( + "errors" "bytes" "fmt" "log" + "os" "os/exec" + "io/ioutil" "regexp" "runtime" "strconv" "strings" + "time" + "net" "github.com/hashicorp/packer/helper/multistep" ) @@ -29,10 +34,6 @@ type Driver interface { // 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,32 @@ 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. + + // 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 +211,218 @@ func compareVersions(versionFound string, versionWanted string, product string) return nil } + +// helper functions that read configuration information from a file +func readNetmapConfig(path string) (NetworkMap,error) { + fd,err := os.Open(path) + if err != nil { return nil, err } + defer fd.Close() + return ReadNetworkMap(fd) +} + +func readDhcpConfig(path string) (DhcpConfiguration,error) { + fd,err := os.Open(path) + if err != nil { return nil, err } + defer fd.Close() + return ReadDhcpConfiguration(fd) +} + +// 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 + VmnetnatConfPath func() string + DhcpConfPath func() string + NetmapConfPath func() string +} + +func (d *VmwareDriver) GuestAddress(state multistep.StateBag) (string,error) { + vmxPath := state.Get("vmx_path").(string) + + log.Println("Lookup up IP information...") + f, err := os.Open(vmxPath) + if err != nil { + return "", err + } + defer f.Close() + + vmxBytes, err := ioutil.ReadAll(f) + if err != nil { + return "", err + } + vmxData := ParseVMX(string(vmxBytes)) + + 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) { + + // read netmap config + pathNetmap := d.NetmapConfPath() + if _, err := os.Stat(pathNetmap); err != nil { + return "", fmt.Errorf("Could not find netmap conf file: %s", pathNetmap) + } + netmap,err := readNetmapConfig(pathNetmap) + if err != nil { return "",err } + + // convert the stashed network to a device + network := state.Get("vmnetwork").(string) + device,err := netmap.NameIntoDevice(network) + 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) { + + // parse network<->device mapping + pathNetmap := d.NetmapConfPath() + if _, err := os.Stat(pathNetmap); err != nil { + return "", fmt.Errorf("Could not find netmap conf file: %s", pathNetmap) + } + netmap,err := readNetmapConfig(pathNetmap) + if err != nil { return "",err } + + // parse dhcpd configuration + pathDhcpConfig := d.DhcpConfPath() + 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 } + + // convert network to name + network := state.Get("vmnetwork").(string) + device,err := netmap.NameIntoDevice(network) + 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) { + + // parse network<->device mapping + pathNetmap := d.NetmapConfPath() + if _, err := os.Stat(pathNetmap); err != nil { + return "", fmt.Errorf("Could not find netmap conf file: %s", pathNetmap) + } + netmap,err := readNetmapConfig(pathNetmap) + if err != nil { return "",err } + + // parse dhcpd configuration + pathDhcpConfig := d.DhcpConfPath() + 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 } + + // convert network to name + network := state.Get("vmnetwork").(string) + device,err := netmap.NameIntoDevice(network) + 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..4bc5ae301 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 @@ -139,6 +141,11 @@ func (d *Fusion5Driver) Verify() error { return err } + // default paths + d.VmwareDriver.DhcpLeasesPath = func(device string) string { + return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases" + } + return nil } @@ -158,10 +165,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 = ` diff --git a/builder/vmware/common/driver_mock.go b/builder/vmware/common/driver_mock.go index 9291be976..15e006403 100644 --- a/builder/vmware/common/driver_mock.go +++ b/builder/vmware/common/driver_mock.go @@ -34,6 +34,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,6 +78,12 @@ type DriverMock struct { DhcpLeasesPathDevice string DhcpLeasesPathResult string + NetmapPathCalled bool + NetmapPathResult string + + DhcpConfPathCalled bool + DhcpConfPathResult string + VerifyCalled bool VerifyErr error } @@ -98,6 +124,30 @@ func (d *DriverMock) CommHost(state multistep.StateBag) (string, error) { return d.CommHostResult, d.CommHostErr } +func (d *DriverMock) HostAddress(state multistep.StateBag) (string, error) { + d.HostAddressCalled = true + d.HostAddressState = state + return d.HostAddressResult, d.HostAddressErr +} + +func (d *DriverMock) HostIP(state multistep.StateBag) (string, error) { + 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,6 +184,16 @@ func (d *DriverMock) DhcpLeasesPath(device string) string { return d.DhcpLeasesPathResult } +func (d *DriverMock) NetmapPath(device string) string { + d.NetmapPathCalled = true + return d.NetmapPathResult +} + +func (d *DriverMock) DhcpConfPath(device string) string { + d.DhcpConfPathCalled = true + return d.DhcpConfPathResult +} + func (d *DriverMock) Verify() error { d.VerifyCalled = true return d.VerifyErr diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go new file mode 100644 index 000000000..38470eb5b --- /dev/null +++ b/builder/vmware/common/driver_parser.go @@ -0,0 +1,974 @@ +package common +import ( + "fmt" + "os" + "io" + "strings" + "strconv" + "net" + "sort" +) + +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 { + out := make(chan byte) + + 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 } + } + } + }(in, out) + return out +} + +// convert a byte channel into a channel of pseudo-tokens +func tokenizeDhcpConfig(eof sentinelSignaller, in chan byte) chan string { + var ch byte + var state string + var quote bool + + 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 } + }(out) + return out +} + +/** 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 { + var ch byte + var state string + var quote bool + var lastnewline bool + + 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 } + }(out) + return out +} + +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=%s",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< 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 := uncomment(eof, fromfile) + tokenized := tokenizeDhcpConfig(eof, uncommented) + parsetree,err := parseDhcpConfig(eof, 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 +func ReadNetworkMap(fd *os.File) (NetworkMap,error) { + + fromfile,eof := consumeFile(fd) + uncommented := uncomment(eof,fromfile) + tokenized := tokenizeNetworkMapConfig(eof, uncommented) + + result,err := parseNetworkMapConfig(eof, 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") +} + +/** main */ +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..9dce9540c 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 @@ -181,6 +183,20 @@ 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.VmnetnatConfPath = func() string { + return playerVmnetnatConfPath() + } + d.VmwareDriver.DhcpConfPath = func() string { + return playerVmDhcpConfPath() + } + d.VmwareDriver.NetmapConfPath = func() string { + return playerNetmapConfPath() + } + return nil } @@ -191,11 +207,3 @@ func (d *Player5Driver) ToolsIsoPath(flavor string) string { func (d *Player5Driver) ToolsInstall() error { return nil } - -func (d *Player5Driver) DhcpLeasesPath(device string) string { - return playerDhcpLeasesPath(device) -} - -func (d *Player5Driver) VmnetnatConfPath() string { - return playerVmnetnatConfPath() -} diff --git a/builder/vmware/common/driver_player5_windows.go b/builder/vmware/common/driver_player5_windows.go index e16d275db..6ca065792 100644 --- a/builder/vmware/common/driver_player5_windows.go +++ b/builder/vmware/common/driver_player5_windows.go @@ -65,6 +65,20 @@ func playerVmnetnatConfPath() string { return findFile("vmnetnat.conf", playerDataFilePaths()) } +func playerVmDhcpConfPath() string { + 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 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 +101,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_workstation10.go b/builder/vmware/common/driver_workstation10.go index 7e6ac8b8f..716717e39 100644 --- a/builder/vmware/common/driver_workstation10.go +++ b/builder/vmware/common/driver_workstation10.go @@ -33,3 +33,4 @@ func (d *Workstation10Driver) Verify() error { return workstationVerifyVersion(VMWARE_WS_VERSION) } + diff --git a/builder/vmware/common/driver_workstation9.go b/builder/vmware/common/driver_workstation9.go index d870a4cda..0c463a249 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 @@ -142,6 +144,23 @@ 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.VmnetnatConfPath = func() string { + return workstationVmnetnatConfPath() + } + + d.VmwareDriver.DhcpConfPath = func() string { + return workstationDhcpConfPath() + } + + d.VmwareDriver.NetmapConfPath = func() string { + return workstationNetmapConfPath() + } + return nil } @@ -152,11 +171,3 @@ func (d *Workstation9Driver) ToolsIsoPath(flavor string) string { func (d *Workstation9Driver) ToolsInstall() error { return nil } - -func (d *Workstation9Driver) DhcpLeasesPath(device string) string { - return workstationDhcpLeasesPath(device) -} - -func (d *Workstation9Driver) VmnetnatConfPath() string { - return workstationVmnetnatConfPath() -} diff --git a/builder/vmware/common/driver_workstation9_windows.go b/builder/vmware/common/driver_workstation9_windows.go index 17095badc..e69d5069f 100644 --- a/builder/vmware/common/driver_workstation9_windows.go +++ b/builder/vmware/common/driver_workstation9_windows.go @@ -63,6 +63,14 @@ func workstationVmnetnatConfPath() string { return findFile("vmnetnat.conf", workstationDataFilePaths()) } +func workstationNetmapConfPath() string { + return findFile("netmap.conf", workstationDataFilePaths()) +} + +func workstationDhcpConfPath() string { + return findFile("vmnetdhcp.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..2ff94fd37 100644 --- a/builder/vmware/common/driver_workstation_unix.go +++ b/builder/vmware/common/driver_workstation_unix.go @@ -51,6 +51,14 @@ func workstationVmnetnatConfPath() string { return "" } +func workstationNetmapConfPath(device string) string { + return "" // FIXME +} + +func workstationDhcpConfPath(device string) string { + return "/etc/vmware/" + device + "/dhcpd/dhcpd.conf" +} + 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_type_boot_command.go b/builder/vmware/common/step_type_boot_command.go index 35e30c624..c169c59eb 100644 --- a/builder/vmware/common/step_type_boot_command.go +++ b/builder/vmware/common/step_type_boot_command.go @@ -64,7 +64,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 +94,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..2cce8fd3c 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -38,12 +38,31 @@ type Config struct { vmwcommon.ToolsConfig `mapstructure:",squash"` vmwcommon.VMXConfig `mapstructure:",squash"` + // disk drives 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"` + + // platform information GuestOSType string `mapstructure:"guest_os_type"` + Version string `mapstructure:"version"` + VMName string `mapstructure:"vm_name"` + + // Network 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 + BootCommand []string `mapstructure:"boot_command"` KeepRegistered bool `mapstructure:"keep_registered"` OVFToolOptions []string `mapstructure:"ovftool_options"` SkipCompaction bool `mapstructure:"skip_compaction"` @@ -53,6 +72,7 @@ type Config struct { 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"` @@ -158,6 +178,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..6df8271f6 100644 --- a/builder/vmware/iso/driver_esx5.go +++ b/builder/vmware/iso/driver_esx5.go @@ -18,12 +18,16 @@ import ( "github.com/hashicorp/packer/communicator/ssh" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + vmwcommon "github.com/mitchellh/packer/builder/vmware/common" gossh "golang.org/x/crypto/ssh" ) // 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 { + vmwcommon.VmwareDriver + Host string Port uint Username string @@ -143,10 +147,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 +159,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 diff --git a/builder/vmware/iso/step_create_vmx.go b/builder/vmware/iso/step_create_vmx.go index 029f2a31e..8e9f6cda0 100644 --- a/builder/vmware/iso/step_create_vmx.go +++ b/builder/vmware/iso/step_create_vmx.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/helper/multistep" @@ -19,6 +20,21 @@ type vmxTemplateData struct { DiskName string ISOPath string Version string + + Network 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 + + Parallel_Present string + Parallel_Bidirectional string + Parallel_Filename string } type additionalDiskTemplateData struct { @@ -39,6 +55,121 @@ type stepCreateVMX struct { tempDir string } +/* serial conversions */ +type serialConfigPipe struct { + filename string + endpoint string + host string + yield string +} + +type serialConfigFile struct { + filename string +} + +type serialConfigDevice struct { + devicename string +} + +type serialUnion struct { + serialType interface{} + pipe *serialConfigPipe + file *serialConfigFile + device *serialConfigDevice +} + +func unformat_serial(config string) (*serialUnion,error) { + comptype := strings.SplitN(config, ":", 2) + if len(comptype) < 1 { + return nil,fmt.Errorf("Unexpected format for serial port: %s", config) + } + switch strings.ToUpper(comptype[0]) { + case "PIPE": + comp := strings.Split(comptype[1], ",") + 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": + res := &serialConfigFile{ filename : comptype[1] } + return &serialUnion{serialType:res, file:res},nil + + case "DEVICE": + res := new(serialConfigDevice) + res.devicename = map[bool]string{true:strings.ToUpper(comptype[1]), false:"COM1"}[len(comptype[1]) > 0] + return &serialUnion{serialType:res, device:res},nil + + default: + return nil,fmt.Errorf("Unknown serial type : %s : %s", strings.ToUpper(comptype[0]), config) + } +} + +/* parallel port */ +type parallelUnion struct { + parallelType interface{} + file *parallelPortFile + device *parallelPortDevice +} +type parallelPortFile struct { + filename string +} +type parallelPortDevice struct { + bidirectional string + devicename string +} + +func unformat_parallel(config string) (*parallelUnion,error) { + comptype := strings.SplitN(config, ":", 2) + if len(comptype) < 1 { + return nil,fmt.Errorf("Unexpected format for parallel port: %s", config) + } + switch strings.ToUpper(comptype[0]) { + case "FILE": + res := ¶llelPortFile{ filename: comptype[1] } + return ¶llelUnion{ parallelType:res, file: res},nil + case "DEVICE": + comp := strings.Split(comptype[1], ",") + 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 = strings.ToUpper(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 + } + 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) @@ -111,14 +242,90 @@ 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, + + Network: config.Network, + 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", } + // store the network so that we can later figure out what ip address to bind to + state.Put("vmnetwork", config.Network) + + // check if serial port has been configured + if config.Serial != "" { + 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 = "" + + 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) + 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 != "" { + 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 + } + + 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) + 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,7 +383,7 @@ ehci.pciSlotNumber = "34" ehci.present = "TRUE" ethernet0.addressType = "generated" ethernet0.bsdName = "en0" -ethernet0.connectionType = "nat" +ethernet0.connectionType = "{{ .Network }}" ethernet0.displayName = "Ethernet" ethernet0.linkStatePropagation.enable = "FALSE" ethernet0.pciSlotNumber = "33" @@ -227,11 +434,38 @@ 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 = "TRUE" +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 = "TRUE" +parallel0.bidirectional = "{{ .Parallel_Bidirectional }}" + virtualHW.productCompatibility = "hosted" virtualHW.version = "{{ .Version }}" vmci0.id = "1861462627" 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