diff -Nru nplan-0.12~16.04/debian/changelog nplan-0.13~16.04/debian/changelog --- nplan-0.12~16.04/debian/changelog 2016-09-26 09:06:57.000000000 +0000 +++ nplan-0.13~16.04/debian/changelog 2016-10-20 14:44:36.000000000 +0000 @@ -1,3 +1,26 @@ +nplan (0.13~16.04) xenial; urgency=medium + + [ Martin Pitt ] + * Update to 0.13. + * tests/integration.py: Disable search domain check in /etc/resolv.conf with + networkd, until this gets fixed in systemd (see LP: #1635256) + + [ Simon Fels ] + * src/netplan: Use absolute path to the nmcli utility provided by the + network-manager snap. Contributes to LP #1633004. + + -- Martin Pitt Thu, 20 Oct 2016 16:44:36 +0200 + +nplan (0.13) zesty; urgency=medium + + [ Jonathan Cave ] + * Blacklist mwifiex_pcie from rebinds (work around LP: #1630285) + + [ Martin Pitt ] + * Add support for nameservers (LP: #1626617) + + -- Martin Pitt Thu, 20 Oct 2016 16:23:58 +0200 + nplan (0.12~16.04) xenial; urgency=medium [ Martin Pitt ] diff -Nru nplan-0.12~16.04/doc/example-config nplan-0.13~16.04/doc/example-config --- nplan-0.12~16.04/doc/example-config 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/doc/example-config 2016-10-20 14:23:58.000000000 +0000 @@ -15,6 +15,9 @@ - 2001:1::1/64 gateway4: 192.168.14.1 gateway6: 2001:1::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] lom: match: driver: ixgbe @@ -49,12 +52,9 @@ # set-name: allowed br0: # IDs of the components; switchports expands into multiple interfaces - interfaces: [my_ap, switchports] + interfaces: [wlp1s0, switchports] dhcp4: true routes: - to: 0.0.0.0/0 via: 11.0.0.1 metric: 3 - nameservers: - search: [foo.local, bar.local] - addresses: [8.8.8.8] diff -Nru nplan-0.12~16.04/doc/netplan.md nplan-0.13~16.04/doc/netplan.md --- nplan-0.12~16.04/doc/netplan.md 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/doc/netplan.md 2016-10-20 14:23:58.000000000 +0000 @@ -178,6 +178,22 @@ Example for IPv4: ``gateway4: 172.16.0.1`` Example for IPv6: ``gateway6: 2001:4::1`` +``nameservers`` (mapping) + +: Set DNS servers and search domains, for manual address configuration. There +are two supported fields: ``addresses:`` is a list of IPv4 or IPv6 addresses +similar to ``gateway*``, and ``search:`` is a list of search domains. + + Example: + + ethernets: + id0: + [...] + nameservers: + search: [lab, home] + addresses: [8.8.8.8, FEDC::1] + + Properties for device type ``ethernets:`` ========================================= Ethernet device definitions do not support any specific properties beyond the @@ -280,6 +296,9 @@ - 2001:1::1/64 gateway4: 192.168.14.1 gateway6: 2001:1::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] lom: match: driver: ixgbe @@ -320,9 +339,6 @@ - to: 0.0.0.0/0 via: 11.0.0.1 metric: 3 - nameservers: - search: [foo.local, bar.local] - addresses: [8.8.8.8] diff -Nru nplan-0.12~16.04/src/netplan nplan-0.13~16.04/src/netplan --- nplan-0.12~16.04/src/netplan 2016-09-26 09:06:57.000000000 +0000 +++ nplan-0.13~16.04/src/netplan 2016-10-20 14:44:23.000000000 +0000 @@ -78,7 +78,7 @@ binary_name = 'nmcli' if is_nm_snap_enabled(): - binary_name = 'network-manager.nmcli' + binary_name = '/snap/bin/network-manager.nmcli' subprocess.check_call([binary_name] + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -119,9 +119,14 @@ try: # we must resolve symlinks here as the device dir will be gone after unbind driver = os.path.realpath(os.path.join(devdir, 'device', 'driver')) - if os.path.basename(driver) == 'mac80211_hwsim': + driver_name = os.path.basename(driver) + if driver_name == 'mac80211_hwsim': logging.debug('replug %s: mac80211_hwsim does not support rebinding, ignoring', device) return False + # workaround for https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1630285 + if driver_name == 'mwifiex_pcie': + logging.debug('replug %s: mwifiex_pcie crashes on rebinding, ignoring', device) + return False logging.debug('replug %s: unbinding %s from %s', device, devname, driver) with open(os.path.join(driver, 'unbind'), 'w') as f: f.write(devname) diff -Nru nplan-0.12~16.04/src/networkd.c nplan-0.13~16.04/src/networkd.c --- nplan-0.12~16.04/src/networkd.c 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/src/networkd.c 2016-10-20 14:23:58.000000000 +0000 @@ -119,7 +119,7 @@ /* do we need to write a .network file? */ if (!def->dhcp4 && !def->dhcp6 && !def->bridge && !def->bond && !def->ip4_addresses && !def->ip6_addresses && !def->gateway4 && !def->gateway6 && - !def->has_vlans) + !def->ip4_nameservers && !def->ip6_nameservers && !def->has_vlans) return; /* build file contents */ @@ -143,6 +143,18 @@ g_string_append_printf(s, "Gateway=%s\n", def->gateway4); if (def->gateway6) g_string_append_printf(s, "Gateway=%s\n", def->gateway6); + if (def->ip4_nameservers) + for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) + g_string_append_printf(s, "DNS=%s\n", g_array_index(def->ip4_nameservers, char*, i)); + if (def->ip6_nameservers) + for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) + g_string_append_printf(s, "DNS=%s\n", g_array_index(def->ip6_nameservers, char*, i)); + if (def->search_domains) { + g_string_append_printf(s, "Domains=%s", g_array_index(def->search_domains, char*, 0)); + for (unsigned i = 1; i < def->search_domains->len; ++i) + g_string_append_printf(s, " %s", g_array_index(def->search_domains, char*, i)); + g_string_append(s, "\n"); + } if (def->bridge) g_string_append_printf(s, "Bridge=%s\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n", def->bridge); if (def->bond) diff -Nru nplan-0.12~16.04/src/nm.c nplan-0.13~16.04/src/nm.c --- nplan-0.12~16.04/src/nm.c 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/src/nm.c 2016-10-20 14:23:58.000000000 +0000 @@ -105,6 +105,17 @@ } } +static void +write_search_domains(const net_definition* def, GString *s) +{ + if (def->search_domains) { + g_string_append(s, "dns-search="); + for (unsigned i = 0; i < def->search_domains->len; ++i) + g_string_append_printf(s, "%s;", g_array_index(def->search_domains, char*, i)); + g_string_append(s, "\n"); + } +} + /** * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a * particular net_definition and wifi_access_point, as NM requires a separate @@ -217,8 +228,15 @@ g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip4_addresses, char*, i)); if (def->gateway4) g_string_append_printf(s, "gateway=%s\n", def->gateway4); + if (def->ip4_nameservers) { + g_string_append(s, "dns="); + for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) + g_string_append_printf(s, "%s;", g_array_index(def->ip4_nameservers, char*, i)); + g_string_append(s, "\n"); + } + write_search_domains(def, s); - if (def->dhcp6 || def->ip6_addresses || def->gateway6) { + if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers) { g_string_append(s, "\n[ipv6]\n"); g_string_append(s, def->dhcp6 ? "method=auto\n" : "method=manual\n"); if (def->ip6_addresses) @@ -226,6 +244,15 @@ g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip6_addresses, char*, i)); if (def->gateway6) g_string_append_printf(s, "gateway=%s\n", def->gateway6); + if (def->ip6_nameservers) { + g_string_append(s, "dns="); + for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) + g_string_append_printf(s, "%s;", g_array_index(def->ip6_nameservers, char*, i)); + g_string_append(s, "\n"); + } + /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] -- + * do we really need to repeat it here? */ + write_search_domains(def, s); } conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, NULL); diff -Nru nplan-0.12~16.04/src/parse.c nplan-0.13~16.04/src/parse.c --- nplan-0.12~16.04/src/parse.c 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/src/parse.c 2016-10-20 14:23:58.000000000 +0000 @@ -573,6 +573,64 @@ return TRUE; } +static gboolean +handle_nameservers_search(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + if (!cur_netdef->search_domains) + cur_netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->search_domains, s); + } + return TRUE; +} + +static gboolean +handle_nameservers_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + struct in_addr a4; + struct in6_addr a6; + int ret; + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + + /* is it an IPv4 address? */ + ret = inet_pton(AF_INET, scalar(entry), &a4); + g_assert(ret >= 0); + if (ret > 0) { + if (!cur_netdef->ip4_nameservers) + cur_netdef->ip4_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ip4_nameservers, s); + continue; + } + + /* is it an IPv6 address? */ + ret = inet_pton(AF_INET6, scalar(entry), &a6); + g_assert(ret >= 0); + if (ret > 0) { + if (!cur_netdef->ip6_nameservers) + cur_netdef->ip6_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ip6_nameservers, s); + continue; + } + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(entry)); + } + + return TRUE; +} + +const mapping_entry_handler nameservers_handlers[] = { + {"search", YAML_SEQUENCE_NODE, handle_nameservers_search}, + {"addresses", YAML_SEQUENCE_NODE, handle_nameservers_addresses}, + {NULL} +}; + const mapping_entry_handler ethernet_def_handlers[] = { {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, {"match", YAML_MAPPING_NODE, handle_match}, @@ -583,6 +641,7 @@ {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {NULL} }; @@ -596,6 +655,7 @@ {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"access-points", YAML_MAPPING_NODE, handle_wifi_access_points}, {NULL} }; @@ -607,6 +667,7 @@ {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"interfaces", YAML_SEQUENCE_NODE, handle_interfaces, NULL, netdef_offset(bridge)}, {NULL} }; @@ -618,6 +679,7 @@ {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"interfaces", YAML_SEQUENCE_NODE, handle_interfaces, NULL, netdef_offset(bond)}, {NULL} }; @@ -629,6 +691,7 @@ {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"id", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(vlan_id)}, {"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(vlan_link)}, {NULL} diff -Nru nplan-0.12~16.04/src/parse.h nplan-0.13~16.04/src/parse.h --- nplan-0.12~16.04/src/parse.h 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/src/parse.h 2016-10-20 14:23:58.000000000 +0000 @@ -58,6 +58,9 @@ GArray* ip6_addresses; char* gateway4; char* gateway6; + GArray* ip4_nameservers; + GArray* ip6_nameservers; + GArray* search_domains; /* master ID for slave devices */ char* bridge; diff -Nru nplan-0.12~16.04/tests/generate.py nplan-0.13~16.04/tests/generate.py --- nplan-0.12~16.04/tests/generate.py 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/tests/generate.py 2016-10-20 14:23:58.000000000 +0000 @@ -680,6 +680,37 @@ Gateway=2001:FFfe::2 '''}) + def test_nameserver(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, "1234::FFFF"] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + search: [lab, kitchen] + addresses: [8.8.8.8]''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +Address=192.168.14.2/24 +DNS=1.2.3.4 +DNS=1234::FFFF +''', + 'enblue.network': '''[Match] +Name=enblue + +[Network] +Address=192.168.1.3/24 +DNS=8.8.8.8 +Domains=lab kitchen +'''}) + def test_vlan(self): self.generate('''network: version: 2 @@ -1500,6 +1531,54 @@ gateway=2001:FFfe::2 '''}) + def test_nameserver(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"] + search: [lab, kitchen] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + addresses: [8.8.8.8]''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +dns=1.2.3.4;2.3.4.5; +dns-search=lab;kitchen; + +[ipv6] +method=manual +dns=1234::FFFF; +dns-search=lab;kitchen; +''', + 'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.1.3/24 +dns=8.8.8.8; +'''}) + def test_vlan(self): self.generate('''network: version: 2 @@ -1903,7 +1982,7 @@ version: 2 ethernets: engreen: - gateway4: %a''' % a, expect_fail=True) + gateway4: %s''' % a, expect_fail=True) self.assertIn("invalid IPv4 address '%s'" % a, err) def test_invalid_gateway6(self): @@ -1912,9 +1991,29 @@ version: 2 ethernets: engreen: - gateway6: %a''' % a, expect_fail=True) + gateway6: %s''' % a, expect_fail=True) self.assertIn("invalid IPv6 address '%s'" % a, err) + def test_invalid_nameserver_ipv4(self): + for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: [%s]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + + def test_invalid_nameserver_ipv6(self): + for a in ['1234', '1:::c', '1234::1/50']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: ["%s"]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + def test_vlan_missing_id(self): err = self.generate('''network: version: 2 diff -Nru nplan-0.12~16.04/tests/integration.py nplan-0.13~16.04/tests/integration.py --- nplan-0.12~16.04/tests/integration.py 2016-08-30 08:32:17.000000000 +0000 +++ nplan-0.13~16.04/tests/integration.py 2016-10-20 14:44:36.000000000 +0000 @@ -33,6 +33,10 @@ sys.stderr.write('%s is required for this test suite, but not available. Skipping\n' % program) sys.exit(0) +nm_uses_dnsmasq = b'dns=dnsmasq' in subprocess.check_output(['NetworkManager', '--print-config']) +with open('/etc/nsswitch.conf') as f: + nss_resolve = ' resolve' in f.read() + class NetworkTestBase(unittest.TestCase): '''Common functionality for network test cases @@ -413,6 +417,9 @@ %(e2c)s: addresses: ["172.16.1.2/24"] gateway4: "172.16.1.1" + nameservers: + addresses: [172.1.2.3] + search: ["fakesuffix"] ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle() self.assert_iface_up(self.dev_e_client, @@ -437,6 +444,32 @@ for i in [self.dev_e_client, self.dev_e2_client]: self.assertRegex(out, '%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) + with open('/etc/resolv.conf') as f: + resolv_conf = f.read() + + if self.backend == 'NetworkManager' and nm_uses_dnsmasq: + sys.stdout.write('[NM with dnsmasq] ') + sys.stdout.flush() + self.assertRegex(resolv_conf, 'search.*fakesuffix') + # not easy to peek dnsmasq's brain, so check its logging + out = subprocess.check_output(['journalctl', '--quiet', '-tdnsmasq', '-ocat', '--since=-30s'], + universal_newlines=True) + self.assertIn('nameserver 172.1.2.3', out) + elif nss_resolve: + sys.stdout.write('[nss-resolve] ') + sys.stdout.flush() + out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + self.assertIn('DNS Servers: 172.1.2.3', out) + self.assertIn('DNS Domain: fakesuffix', out) + else: + sys.stdout.write('[/etc/resolv.conf] ') + sys.stdout.flush() + # LP: #1635256 + # self.assertRegex(resolv_conf, 'search.*fakesuffix') + # /etc/resolve.conf often already has three nameserver entries + if 'nameserver 172.1.2.3' not in resolv_conf: + self.assertGreaterEqual(resolv_conf.count('nameserver'), 3) + # change the addresses, make sure that "apply" does not leave leftovers with open(self.config, 'w') as f: f.write('''network: