From 7201ce92481454894015cd7facc3bdf671c1542d Mon Sep 17 00:00:00 2001 From: gla Date: Sun, 13 Dec 2020 23:26:53 +0100 Subject: [PATCH 1/3] make packer compatible MacOS BigSur by making vmware fusion drivers able to lookup the VM IP address in apple dhcpd leases instead of vmware leases. --- builder/vmware/common/driver.go | 50 ++++++ builder/vmware/common/driver_parser.go | 141 +++++++++++++++ builder/vmware/common/driver_parser_test.go | 160 ++++++++++++++++++ .../testdata/apple-dhcpd-example.leases | 33 ++++ 4 files changed, 384 insertions(+) create mode 100644 builder/vmware/common/testdata/apple-dhcpd-example.leases diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index 6f3a40393..811ed6962 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -430,6 +430,56 @@ func (d *VmwareDriver) PotentialGuestIP(state multistep.StateBag) ([]string, err return addrs, nil } + if runtime.GOOS == "darwin" { + // We have match no vmware DHCP lease for this MAC. We'll try to match it in Apple DHCP leases. + // As a remember, VMWare is no longer able to rely on its own dhcpd server on MacOS BigSur and is + // forced to use Apple DHCPD server instead. + // https://communities.vmware.com/t5/VMware-Fusion-Discussions/Big-Sur-hosts-with-Fusion-Is-vmnet-dhcpd-vmnet8-leases-file/m-p/2298927/highlight/true#M140003 + + // set the apple dhcp leases path + appleDhcpLeasesPath := "/var/db/dhcpd_leases" + log.Printf("Trying Apple DHCP leases path: %s", appleDhcpLeasesPath) + + // open up the path to the apple dhcpd leases + fh, err := os.Open(appleDhcpLeasesPath) + if err != nil { + log.Printf("Error while reading apple DHCP lease path file %s: %s", appleDhcpLeasesPath, err.Error()) + } else { + defer fh.Close() + + // and then read its contents + leaseEntries, err := ReadAppleDhcpdLeaseEntries(fh) + if err != nil { + return []string{}, err + } + + // Parse our MAC address again. There's no need to check for an + // error because we've already parsed this successfully. + hwaddr, _ := net.ParseMAC(MACAddress) + + // Go through our available lease entries and see which ones are within + // scope, and that match to our hardware address. + available_lease_entries := make([]appleDhcpLeaseEntry, 0) + for _, entry := range leaseEntries { + // Next check for any where the hardware address matches. + if bytes.Equal(hwaddr, []byte(entry.hwAddress)) { + available_lease_entries = append(available_lease_entries, entry) + } + } + + // Check if we found any lease entries that correspond to us. If so, then we + // need to map() them in order to extract the address field to return to the + // caller. + if len(available_lease_entries) > 0 { + addrs := make([]string, 0) + for _, entry := range available_lease_entries { + addrs = append(addrs, entry.ipAddress) + } + return addrs, nil + } + } + } + return []string{}, fmt.Errorf("None of the found device(s) %v has a DHCP lease for MAC %s", devices, MACAddress) } diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go index 8b1e5ebcc..839acc20f 100644 --- a/builder/vmware/common/driver_parser.go +++ b/builder/vmware/common/driver_parser.go @@ -2430,3 +2430,144 @@ func ReadDhcpdLeaseEntries(fd *os.File) ([]dhcpLeaseEntry, error) { } return result, nil } + +/*** Apple Dhcp Leases */ + +// Here is what an Apple DHCPD lease entry looks like: +// { +// ip_address=192.168.111.2 +// hw_address=1,0:50:56:20:ac:33 +// identifier=1,0:50:56:20:ac:33 +// lease=0x5fd72edc +// name=vagrant-2019 +// } + +type appleDhcpLeaseEntry struct { + ipAddress string + hwAddress, id []byte + lease string + name string + extra map[string]string +} + +func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err error) { + entry = &appleDhcpLeaseEntry{extra: map[string]string{}} + validFieldCount := 0 + // Read up to the lease item and validate that it actually matches + _, ch := consumeOpenClosePair('{', '}', in) + for insideBraces := true; insideBraces; { + item, ok := consumeUntilSentinel('\n', ch) + item_s := strings.TrimSpace(string(item)) + + if !ok { + insideBraces = false + } + if item_s == "{" || item_s == "}" { + continue + } + splittedLine := strings.Split(item_s, "=") + var key, val string + switch len(splittedLine) { + case 0: + // should never happens as Split always returns at least 1 item + fallthrough + case 1: + log.Printf("Error parsing invalid line: `%s`", item_s) + continue + case 2: + key = strings.TrimSpace(splittedLine[0]) + val = strings.TrimSpace(splittedLine[1]) + default: + // There were more than one '=' on this line, we'll keep the part before the first '=' as the key and + // the rest will be the value + key = strings.TrimSpace(splittedLine[0]) + val = strings.TrimSpace(strings.Join(splittedLine[1:], "=")) + } + switch key { + case "ip_address": + entry.ipAddress = val + validFieldCount++ + case "identifier": + fallthrough + case "hw_address": + if strings.Count(val, ",") != 1 { + log.Printf("Error %s `%s` is not properly formatted for entry %s", key, val, entry.name) + break + } + splittedVal := strings.Split(val, ",") + mac := splittedVal[1] + splittedMac := strings.Split(mac, ":") + // Pad the retrieved hw address with '0' when necessary + for idx := range splittedMac { + if len(splittedMac[idx]) == 1 { + splittedMac[idx] = "0" + splittedMac[idx] + } + } + mac = strings.Join(splittedMac, ":") + decodedLease, err := decodeDhcpdLeaseBytes(mac) + if err != nil { + log.Printf("Error trying to parse %s (%v) for entry %s - %v", key, val, entry.name, mac) + break + } + if key == "identifier" { + entry.id = decodedLease + } else { + entry.hwAddress = decodedLease + } + validFieldCount++ + case "lease": + entry.lease = val + validFieldCount++ + case "name": + entry.name = val + validFieldCount++ + default: + // Just stash it for now because we have no idea what it is. + entry.extra[key] = val + } + } + // we have most likely parsed the whole file + if validFieldCount == 0 { + return nil, nil + } + // an entry is composed of 5 mandatory fields, we'll check that they all have been set during the parsing + if validFieldCount < 5 { + return entry, fmt.Errorf("Error entry `%v` is missing mandatory information", entry) + } + return entry, nil +} + +func ReadAppleDhcpdLeaseEntries(fd *os.File) ([]appleDhcpLeaseEntry, error) { + fch := consumeFile(fd) + uncommentedch := uncomment(fch) + wch := filterOutCharacters([]byte{'\r', '\v'}, uncommentedch) + + result := make([]appleDhcpLeaseEntry, 0) + errors := make([]error, 0) + + // Consume apple dhcpd lease entries from the channel until we just plain run out. + for i := 0; ; i++ { + if entry, err := readAppleDhcpdLeaseEntry(wch); entry == nil { + // If our entry is nil, then we've run out of input and finished + // parsing the file to completion. + break + + } else if err != nil { + // If we received an error, then log it and keep track of it. This + // way we can warn the user later which entries we had issues with. + log.Printf("Error parsing apple dhcpd lease entry #%d: %s", 1+i, err) + errors = append(errors, err) + + } else { + // If we've parsed an entry successfully, then aggregate it to + // our slice of results. + result = append(result, *entry) + } + } + + // If we received any errors then include alongside our results. + if len(errors) > 0 { + return result, fmt.Errorf("Errors found while parsing apple dhcpd lease entries: %v", errors) + } + return result, nil +} diff --git a/builder/vmware/common/driver_parser_test.go b/builder/vmware/common/driver_parser_test.go index 01abffdbb..c93dc90c4 100644 --- a/builder/vmware/common/driver_parser_test.go +++ b/builder/vmware/common/driver_parser_test.go @@ -865,6 +865,166 @@ func TestParserReadDhcpdLeases(t *testing.T) { } } +func consumeAppleLeaseString(s string) chan byte { + sch := consumeString(s) + uncommentedch := uncomment(sch) + return filterOutCharacters([]byte{'\r', '\v'}, uncommentedch) +} + +func TestParserReadAppleDhcpdLeaseEntry(t *testing.T) { + test_1 := `{ + ip_address=192.168.111.3 + hw_address=1,0:c:56:3c:e7:22 + identifier=1,0:c:56:3c:e7:22 + lease=0x5fd78ae2 + name=vagrant-2019 + fake=field + }` + expected_1 := map[string]string{ + "ipAddress": "192.168.111.3", + "hwAddress": "000c563ce722", + "id": "000c563ce722", + "lease": "0x5fd78ae2", + "name": "vagrant-2019", + } + expected_extra_1 := map[string]string{ + "fake": "field", + } + + result, err := readAppleDhcpdLeaseEntry(consumeAppleLeaseString(test_1)) + if err != nil { + t.Errorf("error parsing entry: %v", err) + } + if result.ipAddress != expected_1["ipAddress"] { + t.Errorf("expected ipAddress %v, got %v", expected_1["ipAddress"], result.ipAddress) + } + if hex.EncodeToString(result.hwAddress) != expected_1["hwAddress"] { + t.Errorf("expected hwAddress %v, got %v", expected_1["hwAddress"], hex.EncodeToString(result.hwAddress)) + } + if hex.EncodeToString(result.id) != expected_1["id"] { + t.Errorf("expected id %v, got %v", expected_1["id"], hex.EncodeToString(result.id)) + } + if result.lease != expected_1["lease"] { + t.Errorf("expected lease %v, got %v", expected_1["lease"], result.lease) + } + if result.name != expected_1["name"] { + t.Errorf("expected name %v, got %v", expected_1["name"], result.name) + } + if result.extra["fake"] != expected_extra_1["fake"] { + t.Errorf("expected extra %v, got %v", expected_extra_1["fake"], result.extra["fake"]) + } +} + +func TestParserReadAppleDhcpdLeases(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "apple-dhcpd-example.leases")) + if err != nil { + t.Fatalf("Unable to open dhcpd.leases sample: %s", err) + } + defer f.Close() + + results, err := ReadAppleDhcpdLeaseEntries(f) + if err != nil { + t.Fatalf("Error reading lease: %s", err) + } + + // some simple utilities + filter_ipAddr := func(ipAddress string, items []appleDhcpLeaseEntry) (result []appleDhcpLeaseEntry) { + for _, item := range items { + if item.ipAddress == ipAddress { + result = append(result, item) + } + } + return + } + + find_id := func(id string, items []appleDhcpLeaseEntry) *appleDhcpLeaseEntry { + for _, item := range items { + if id == hex.EncodeToString(item.id) { + return &item + } + } + return nil + } + + find_hwAddr := func(hwAddr string, items []appleDhcpLeaseEntry) *appleDhcpLeaseEntry { + for _, item := range items { + if hwAddr == hex.EncodeToString(item.hwAddress) { + return &item + } + } + return nil + } + + // actual unit tests + test_1 := map[string]string{ + "ipAddress": "127.0.0.19", + "id": "0dead099aabb", + "hwAddress": "0dead099aabb", + } + test_1_findings := filter_ipAddr(test_1["ipAddress"], results) + if len(test_1_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_1_findings)) + } else { + res := find_hwAddr(test_1["hwAddress"], test_1_findings) + if res == nil { + t.Errorf("unable to find item with hwAddress %v", test_1["hwAddress"]) + } else if hex.EncodeToString(res.id) != test_1["id"] { + t.Errorf("expected id %s, got %s", test_1["id"], hex.EncodeToString(res.id)) + } + } + + test_2 := map[string]string{ + "ipAddress": "127.0.0.19", + "id": "0dead0667788", + "hwAddress": "0dead0667788", + } + test_2_findings := filter_ipAddr(test_2["ipAddress"], results) + if len(test_2_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_2_findings)) + } else { + res := find_hwAddr(test_2["hwAddress"], test_2_findings) + if res == nil { + t.Errorf("unable to find item with hwAddress %v", test_2["hwAddress"]) + } else if hex.EncodeToString(res.id) != test_2["id"] { + t.Errorf("expected id %s, got %s", test_2["id"], hex.EncodeToString(res.id)) + } + } + + test_3 := map[string]string{ + "ipAddress": "127.0.0.17", + "id": "0dead0334455", + "hwAddress": "0dead0667788", + } + test_3_findings := filter_ipAddr(test_3["ipAddress"], results) + if len(test_3_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_3_findings)) + } else { + res := find_id(test_3["id"], test_3_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_3["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_3["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_3["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } + + test_4 := map[string]string{ + "ipAddress": "127.0.0.17", + "id": "0dead0001122", + "hwAddress": "0dead0667788", + } + test_4_findings := filter_ipAddr(test_4["ipAddress"], results) + if len(test_4_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_4_findings)) + } else { + res := find_id(test_4["id"], test_4_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_4["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_4["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_4["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } +} + func TestParserTokenizeNetworkingConfig(t *testing.T) { tests := []string{ "words words words", diff --git a/builder/vmware/common/testdata/apple-dhcpd-example.leases b/builder/vmware/common/testdata/apple-dhcpd-example.leases new file mode 100644 index 000000000..b6ee35d6e --- /dev/null +++ b/builder/vmware/common/testdata/apple-dhcpd-example.leases @@ -0,0 +1,33 @@ +# this entry is normal +{ + ip_address=127.0.0.17 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:0:11:22 + lease=0x5fd78ae2 + name=vagrant-2019 +} + +# this entry has tabs +{ + ip_address=127.0.0.17 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:33:44:55 + lease=0x5fd7b4e5 + name=vagrant-2019 +} + +# These next two entries have the same address, but different uids +{ + ip_address=127.0.0.19 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:66:77:88 + lease=0x5fd72edc + name=vagrant-2019 +} +{ + ip_address=127.0.0.19 + hw_address=1,d:ea:d0:99:aa:bb + identifier=1,d:ea:d0:99:aa:bb + lease=0x5fd72edc + name=vagrant-2019 +} \ No newline at end of file From f0612e468819ff370a84bc6297318f74b5c84f81 Mon Sep 17 00:00:00 2001 From: Pyrrvs Date: Mon, 14 Dec 2020 01:00:24 +0100 Subject: [PATCH 2/3] improve parser by skipping all lines containing a '{' or a '}'. fix unecessary []byte cast (linter issue). --- builder/vmware/common/driver.go | 2 +- builder/vmware/common/driver_parser.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index 811ed6962..db50a2889 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -462,7 +462,7 @@ func (d *VmwareDriver) PotentialGuestIP(state multistep.StateBag) ([]string, err available_lease_entries := make([]appleDhcpLeaseEntry, 0) for _, entry := range leaseEntries { // Next check for any where the hardware address matches. - if bytes.Equal(hwaddr, []byte(entry.hwAddress)) { + if bytes.Equal(hwaddr, entry.hwAddress) { available_lease_entries = append(available_lease_entries, entry) } } diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go index 839acc20f..7ea077d18 100644 --- a/builder/vmware/common/driver_parser.go +++ b/builder/vmware/common/driver_parser.go @@ -2462,7 +2462,7 @@ func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err err if !ok { insideBraces = false } - if item_s == "{" || item_s == "}" { + if strings.Contains(item_s, "{") || strings.Contains(item_s, "}") { continue } splittedLine := strings.Split(item_s, "=") From 12a6fddbd8c5be4a921be2b70c96296a5b5bcc9f Mon Sep 17 00:00:00 2001 From: Pyrrvs Date: Mon, 14 Dec 2020 19:15:34 +0100 Subject: [PATCH 3/3] handle apple dhcp lease with missing 'name' and 'lease' informations --- builder/vmware/common/driver_parser.go | 14 +++---- builder/vmware/common/driver_parser_test.go | 42 +++++++++++++++++++ .../testdata/apple-dhcpd-example.leases | 7 ++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go index 7ea077d18..825fa7bd5 100644 --- a/builder/vmware/common/driver_parser.go +++ b/builder/vmware/common/driver_parser.go @@ -2452,7 +2452,7 @@ type appleDhcpLeaseEntry struct { func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err error) { entry = &appleDhcpLeaseEntry{extra: map[string]string{}} - validFieldCount := 0 + mandatoryFieldCount := 0 // Read up to the lease item and validate that it actually matches _, ch := consumeOpenClosePair('{', '}', in) for insideBraces := true; insideBraces; { @@ -2486,7 +2486,7 @@ func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err err switch key { case "ip_address": entry.ipAddress = val - validFieldCount++ + mandatoryFieldCount++ case "identifier": fallthrough case "hw_address": @@ -2514,24 +2514,22 @@ func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err err } else { entry.hwAddress = decodedLease } - validFieldCount++ + mandatoryFieldCount++ case "lease": entry.lease = val - validFieldCount++ case "name": entry.name = val - validFieldCount++ default: // Just stash it for now because we have no idea what it is. entry.extra[key] = val } } // we have most likely parsed the whole file - if validFieldCount == 0 { + if mandatoryFieldCount == 0 { return nil, nil } - // an entry is composed of 5 mandatory fields, we'll check that they all have been set during the parsing - if validFieldCount < 5 { + // an entry is composed of 3 mandatory fields, we'll check that they all have been set during the parsing + if mandatoryFieldCount < 3 { return entry, fmt.Errorf("Error entry `%v` is missing mandatory information", entry) } return entry, nil diff --git a/builder/vmware/common/driver_parser_test.go b/builder/vmware/common/driver_parser_test.go index c93dc90c4..dbd24fa18 100644 --- a/builder/vmware/common/driver_parser_test.go +++ b/builder/vmware/common/driver_parser_test.go @@ -913,6 +913,31 @@ func TestParserReadAppleDhcpdLeaseEntry(t *testing.T) { if result.extra["fake"] != expected_extra_1["fake"] { t.Errorf("expected extra %v, got %v", expected_extra_1["fake"], result.extra["fake"]) } + + test_2 := `{ + ip_address=192.168.111.4 + hw_address=1,0:c:56:3c:e7:23 + identifier=1,0:c:56:3c:e7:23 + }` + expected_2 := map[string]string{ + "ipAddress": "192.168.111.4", + "hwAddress": "000c563ce723", + "id": "000c563ce723", + } + + result, err = readAppleDhcpdLeaseEntry(consumeAppleLeaseString(test_2)) + if err != nil { + t.Errorf("error parsing entry: %v", err) + } + if result.ipAddress != expected_2["ipAddress"] { + t.Errorf("expected ipAddress %v, got %v", expected_2["ipAddress"], result.ipAddress) + } + if hex.EncodeToString(result.hwAddress) != expected_2["hwAddress"] { + t.Errorf("expected hwAddress %v, got %v", expected_2["hwAddress"], hex.EncodeToString(result.hwAddress)) + } + if hex.EncodeToString(result.id) != expected_2["id"] { + t.Errorf("expected id %v, got %v", expected_2["id"], hex.EncodeToString(result.id)) + } } func TestParserReadAppleDhcpdLeases(t *testing.T) { @@ -1023,6 +1048,23 @@ func TestParserReadAppleDhcpdLeases(t *testing.T) { t.Errorf("expected hardware address %s, got %s", test_4["hwAddress"], hex.EncodeToString(res.hwAddress)) } } + + test_5 := map[string]string{ + "ipAddress": "127.0.0.20", + "id": "0dead099aabc", + "hwAddress": "0dead099aabc", + } + test_5_findings := filter_ipAddr(test_5["ipAddress"], results) + if len(test_5_findings) != 1 { + t.Errorf("expected %d matching entries, got %d", 1, len(test_5_findings)) + } else { + res := find_id(test_5["id"], test_5_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_5["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_5["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_5["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } } func TestParserTokenizeNetworkingConfig(t *testing.T) { diff --git a/builder/vmware/common/testdata/apple-dhcpd-example.leases b/builder/vmware/common/testdata/apple-dhcpd-example.leases index b6ee35d6e..53aacb19c 100644 --- a/builder/vmware/common/testdata/apple-dhcpd-example.leases +++ b/builder/vmware/common/testdata/apple-dhcpd-example.leases @@ -30,4 +30,11 @@ identifier=1,d:ea:d0:99:aa:bb lease=0x5fd72edc name=vagrant-2019 +} + +# this entry does not have all fields +{ + ip_address=127.0.0.20 + hw_address=1,d:ea:d0:99:aa:bc + identifier=1,d:ea:d0:99:aa:bc } \ No newline at end of file