diff -Nru netplan.io-0.36.3/debian/changelog netplan.io-0.40.1~18.04.2/debian/changelog --- netplan.io-0.36.3/debian/changelog 2018-07-05 20:54:52.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/debian/changelog 2018-10-22 19:02:30.000000000 +0000 @@ -1,25 +1,89 @@ -netplan.io (0.36.3) bionic; urgency=medium +netplan.io (0.40.1~18.04.2) bionic; urgency=medium - * Generate udev rules files to rename devices (LP: #1770082) - Due to a systemd issue[1], using link files to rename interfaces - doesn't work as expected. Link files will not rename an interface - if it was already renamed, and interfaces are renamed in initrd, so - set-name will often not work as expected when rebooting. However, - rules files will cause a renaming, even if the interface has been - renamed in initrd. + * Fix typo breaking rename on 'netplan apply'. (LP: #1770082) + + -- Mathieu Trudel-Lapierre Mon, 22 Oct 2018 15:02:30 -0400 + +netplan.io (0.40.1~18.04.1) bionic; urgency=medium + + * Backport netplan 0.40.1 to 18.04. (LP: #1793309) + + -- Mathieu Trudel-Lapierre Thu, 04 Oct 2018 13:59:42 -0400 + +netplan.io (0.40.1) cosmic; urgency=medium + + * tests/generate.py: use random.sample() instead of random.choices() to + better support older pythons. + * Deal gracefully with empty files on 'netplan apply' (LP: #1795343) + + -- Mathieu Trudel-Lapierre Thu, 04 Oct 2018 12:05:49 -0400 + +netplan.io (0.40) cosmic; urgency=medium + + * New upstream release: + - networkd: route source is PreferredSource= not From= + - Improve NetworkManager error reporting on unrenderable routes. + - Don't render ipv4 dns-search unless we have an ipv4 address. + (LP: #1786726) + - Set permissive umask on networkd .network, .link and .netdev files + (LP: #1736965, LP: #1768560) + - Fix support for link-scope routes. (LP: #1747455) + - Update man pages for deletion of replug code. + - Spell Gratuitous ARP correctly and make it work. (LP: #1756701) + - Many typo fixes for documentation. (LP: #1783940) + - Various build system fixes. + - Fix integration tests: + - iproute2 output changes for link-scope routes + - fix stability of networkd igmp-resend test + - fix manual_addresses test now that networkd lists ~. domain + - Deduplicate code for parsing interface options + - Add support for optional-addresses. + + -- Mathieu Trudel-Lapierre Thu, 13 Sep 2018 17:29:41 -0400 + +netplan.io (0.39) cosmic; urgency=medium + + * New upstream release: + - Allow link-local addresses to be configured. (LP: #1771704) + - Forces bridges with no addresses to be brought online. (LP: #1736975) + + -- Mathieu Trudel-Lapierre Mon, 16 Jul 2018 08:15:05 -0400 + +netplan.io (0.38) cosmic; urgency=medium + + * New upstream release: + - Write udev .rules files to /run/udev/rules.d to enforce interface + renaming. (LP: #1770082) + - Don't traceback for 'netplan ip leases' when iface is not managed or + doesn't DHCP (LP: #1768823) + - Fix duplicate "/" path separator in error messages (LP: #1771440) + - Fix incorrect terminal reset in 'netplan try' on Ctrl-C. (LP: #1768798) + - Updated doc entries: mtu, fix fwmark->mark, cleanup optional. + (LP: #1768783) + - Added documentation validation at build. + - Added configuration example for multi-ip interfaces. * tests/integration.py: fix test_eth_and_bridge autopkg test harder. - * tests/integration.py: fix test_mix_bridge_on_bond autopkgtest too. + * debian/control: + - Add iproute2 to Depends. + - Add python3-netifaces to Depends, Build-Depends. + + -- Mathieu Trudel-Lapierre Mon, 04 Jun 2018 14:45:26 -0400 + +netplan.io (0.37.1) cosmic; urgency=medium + + * tests/integration.py: fix autopkgtests to be less flaky, especially given + changes in systemd-networkd's behavior regarding Router Advertisements. - -- Mathieu Trudel-Lapierre Thu, 05 Jul 2018 16:54:52 -0400 + -- Mathieu Trudel-Lapierre Fri, 11 May 2018 09:58:19 -0400 -netplan.io (0.36.2) bionic; urgency=medium +netplan.io (0.37) cosmic; urgency=medium * doc/netplan.md: Clarify the behavior for time-based values for bonds and bridges. (LP: #1756587) * critical: provide a way to set "CriticalConnection=true" on a networkd connection, especially for remote-fs scenarios. (LP: #1769682) - -- Mathieu Trudel-Lapierre Tue, 08 May 2018 15:33:48 -0400 + -- Mathieu Trudel-Lapierre Tue, 08 May 2018 17:02:26 -0400 netplan.io (0.36.1) bionic; urgency=medium diff -Nru netplan.io-0.36.3/debian/control netplan.io-0.40.1~18.04.2/debian/control --- netplan.io-0.36.3/debian/control 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/debian/control 2018-10-04 16:04:57.000000000 +0000 @@ -12,6 +12,7 @@ python3 (>= 3.1), python3-coverage, python3-yaml, + python3-netifaces, systemd, pyflakes3, pycodestyle | pep8, @@ -25,8 +26,10 @@ Multi-Arch: foreign Depends: ${shlibs:Depends}, ${misc:Depends}, + iproute2, python3, python3-yaml, + python3-netifaces, systemd (>= 235-3ubuntu3), Suggests: network-manager | wpasupplicant Conflicts: netplan diff -Nru netplan.io-0.36.3/doc/manpage-footer.md netplan.io-0.40.1~18.04.2/doc/manpage-footer.md --- netplan.io-0.36.3/doc/manpage-footer.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/manpage-footer.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,3 @@ +# SEE ALSO + + **netplan-generate**(8), **netplan-apply**(8), **netplan-try**(8), **systemd-networkd**(8), **NetworkManager**(8) diff -Nru netplan.io-0.36.3/doc/manpage-header.md netplan.io-0.40.1~18.04.2/doc/manpage-header.md --- netplan.io-0.36.3/doc/manpage-header.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/manpage-header.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,21 @@ +--- +title: netplan +section: 5 +author: +- Mathieu Trudel-Lapierre () +- Martin Pitt () +... + +# NAME + +netplan - YAML network configuration abstraction for various backends + +# SYNOPSIS + +**netplan** [ *COMMAND* | help ] + +# COMMANDS + +See **netplan help** for a list of available commands on this system. + +# DESCRIPTION diff -Nru netplan.io-0.36.3/doc/manpage.md netplan.io-0.40.1~18.04.2/doc/manpage.md --- netplan.io-0.36.3/doc/manpage.md 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/manpage.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ ---- -title: netplan -section: 5 -author: -- Mathieu Trudel-Lapierre () -- Martin Pitt () -... - -# NAME - -netplan - YAML network configuration abstraction for various backends - -# SYNOPSIS - -**netplan** [ *COMMAND* | help ] - -# COMMANDS - -See **netplan help** for a list of available commands on this system. - -# DESCRIPTION diff -Nru netplan.io-0.36.3/doc/netplan-apply.md netplan.io-0.40.1~18.04.2/doc/netplan-apply.md --- netplan.io-0.36.3/doc/netplan-apply.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/netplan-apply.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,62 @@ +--- +title: netplan-apply +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-apply - apply configuration from netplan YAML files to a running system + +# SYNOPSIS + + **netplan** [--debug] **apply** -h | --help + + **netplan** [--debug] **apply** + +# DESCRIPTION + +**netplan apply** applies the current netplan configuration to a running system. + +The process works as follows: + + 1. The backend configuration is generated from netplan YAML files. + + 2. The appropriate backends (**systemd-networkd**(8) or + **NetworkManager**(8)) are invoked to bring up configured interfaces. + + 3. **netplan apply** iterates through interfaces that are still down, unbinding + them from their drivers, and rebinding them. This gives **udev**(7) renaming + rules the opportunity to run. + + 4. If any devices have been rebound, the appropriate backends are re-invoked in + case more matches can be done. + +For information about the generation step, see +**netplan-generate**(8). For details of the configuration file format, +see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + +# KNOWN ISSUES + +**netplan apply** will not remove virtual devices such as bridges +and bonds that have been created, even if they are no longer described +in the netplan configuration. + +This can be resolved by manually removing the virtual device (for +example ``ip link delete dev bond0``) and then running **netplan +apply**, or by rebooting. + + +# SEE ALSO + + **netplan**(5), **netplan-generate**(8), **netplan-try**(8), **udev**(7), + **systemd-networkd.service**(8), **NetworkManager**(8) diff -Nru netplan.io-0.36.3/doc/netplan-generate.md netplan.io-0.40.1~18.04.2/doc/netplan-generate.md --- netplan.io-0.36.3/doc/netplan-generate.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/netplan-generate.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,83 @@ +--- +title: netplan-generate +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-generate - generate backend configuration from netplan YAML files + +# SYNOPSIS + + **netplan** [--debug] **generate** -h | --help + + **netplan** [--debug] **generate** [--root-dir _ROOT_DIR_] [--mapping _MAPPING_] + +# DESCRIPTION + +netplan generate converts netplan YAML into configuration files +understood by the backends (**systemd-networkd**(8) or +**NetworkManager**(8)). It *does not* apply the generated +configuration. + +You will not normally need to run this directly as it is run by +**netplan apply**, **netplan try**, or at boot. + +For details of the configuration file format, see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --root-dir _ROOT_DIR_ +: Instead of looking in /{lib,etc,run}/netplan, look in + /_ROOT_DIR_/{lib,etc,run}/netplan + + --mapping _MAPPING_ +: Instead of generating output files, parse the configuration files + and print some internal information about the device specified in + _MAPPING_. + +# HANDLING MULTIPLE FILES + +There are 3 locations that netplan generate considers: + + * /lib/netplan/*.yaml + * /etc/netplan/*.yaml + * /run/netplan/*.yaml + +If there are multiple files with exactly the same name, then only one +will be read. A file in /run/netplan will shadow - completely replace +- a file with the same name in /etc/netplan. A file in /etc/netplan +will itself shadow a file in /lib/netplan. + +Or in other words, /run/netplan is top priority, then /etc/netplan, +with /lib/netplan having the lowest priority. + +If there are files with different names, then they are considered in +lexicographical order - regardless of the directory they are in. Later +files add to or override earlier files. For example, +/run/netplan/10-foo.yaml would be updated by /lib/netplan/20-abc.yaml. + +If you have two files with the same key/setting, the following rules +apply: + + * If the values are YAML boolean or scalar values (numbers and + strings) the old value is overwritten by the new value. + + * If the values are sequences, the sequences are concatenated - the + new values are appended to the old list. + + * If the values are mappings, netplan will examine the elements + of the mappings in turn using these rules. + +# SEE ALSO + + **netplan**(5), **netplan-apply**(8), **netplan-try**(8), + **systemd-networkd**(8), **NetworkManager**(8) diff -Nru netplan.io-0.36.3/doc/netplan.md netplan.io-0.40.1~18.04.2/doc/netplan.md --- netplan.io-0.36.3/doc/netplan.md 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/netplan.md 2018-10-22 18:52:52.000000000 +0000 @@ -153,12 +153,31 @@ If you are in an IPv6-only environment with completely stateless autoconfiguration (SLAAC with RDNSS), this option can be set to cause the interface to be brought up. (Setting accept-ra alone is not sufficient.) - Autoconfiguration will still honour the contents of the router advertisment + Autoconfiguration will still honour the contents of the router advertisement and only use DHCP if requested in the RA. Note that **``rdnssd``**(8) is required to use RDNSS with networkd. No extra software is required for NetworkManager. +``link-local`` (sequence of scalars) + +: Configure the link-local addresses to bring up. Valid options are 'ipv4' + and 'ipv6', which respectively allow enabling IPv4 and IPv6 link local + addressing. If this field is not defined, the default is to enable only + IPv6 link-local addresses. If the field is defined but configured as an + empty set, IPv6 link-local addresses are disabled as well as IPv4 link- + local addresses. + + This feature enables or disables link-local addresses for a protocol, but + the actual implementation differs per backend. On networkd, this directly + changes the behavior and may add an extra address on an interface. When + using the NetworkManager backend, enabling link-local has no effect if the + interface also has DHCP enabled. + + Example to enable only IPv4 link-local: ``link-local: [ ipv4 ]`` + Example to enable all link-local addresses: ``link-local: [ ipv4, ipv6 ]`` + Example to disable all link-local addresses: ``link-local: [ ]`` + ``critical`` (bool) : (networkd backend only) Designate the connection as "critical to the @@ -184,6 +203,10 @@ form ``addr/prefixlen``. ``addr`` is an IPv4 or IPv6 address as recognized by **``inet_pton``**(3) and ``prefixlen`` the number of bits of the subnet. + For virtual devices (bridges, bonds, vlan) if there is no address + configured and DHCP is disabled, the interface may still be brought online, + but will not be addressable from the network. + Example: ``addresses: [192.168.14.2/24, "2001:1::1/64"]`` ``gateway4``, ``gateway6`` (scalar) @@ -222,13 +245,21 @@ [...] macaddress: 52:54:00:6b:3c:59 -``optional`` (boolean) +``mtu`` (scalar) + +: Set the Maximum Transmission Unit for the interface. The default is 1500. + Valid values depend on your network interface. + + **Note:** This will not work reliably for devices matched by name + only, due to interactions with device renaming in udev. Match + devices by MAC when setting MTU. -: An optional device is not required for booting. Normally, networkd -will wait some time for device to become configured before proceeding -with booting. However, if a device is marked as optional, networkd -will not wait for it. This is *only* supported by networkd, and the -default is false. +``optional`` (bool) + +: An optional device is not required for booting. Normally, networkd will + wait some time for device to become configured before proceeding with + booting. However, if a device is marked as optional, networkd will not wait + for it. This is *only* supported by networkd, and the default is false. Example: @@ -239,6 +270,22 @@ dhcp4: true optional: true +``optional-addresses`` (sequence of scalars) + +: Specify types of addresses that are not required for a device to be + considered online. This changes the behavior of backends at boot time to + avoid waiting for addresses that are marked optional, and thus consider + the interface as "usable" sooner. This does not disable these addresses, + which will be brought up anyway. + + Example: + + ethernets: + eth7: + dhcp4: true + dhcp6: true + optional-addresses: [ ipv4-ll, dhcp6 ] + ``routes`` (mapping) : Configure static routing for the device; see the ``Routing`` section below. @@ -281,7 +328,7 @@ ``type`` (scalar) : The type of route. Valid options are "unicast" (default), - "unreachable", "blackhole" or "prohibited". + "unreachable", "blackhole" or "prohibit". ``scope`` (scalar) : The route scope, how wide-ranging it is to the network. Possible @@ -323,7 +370,7 @@ in which routing rules are processed. A higher number means lower priority: rules are processed in order by increasing priority number. - ``fwmark`` (scalar) + ``mark`` (scalar) : Have this routing policy rule match on traffic that has been marked by the iptables firewall with this value. Allowed values are positive integers starting from 1. @@ -362,7 +409,9 @@ ``interfaces`` (sequence of scalars) -: All devices matching this ID list will be added to the bridge. +: All devices matching this ID list will be added to the bridge. This may + be an empty list, in which case the bridge will be brought online with + no member interfaces. Example: @@ -525,13 +574,16 @@ them to the bond, or how else the system should handle MAC addresses. The possible values are ``none``, ``active``, and ``follow``. - ``gratuitious-arp`` (scalar) + ``gratuitous-arp`` (scalar) : Specify how many ARP packets to send after failover. Once a link is up on a new slave, a notification is sent and possibly repeated if this value is set to a number greater than ``1``. The default value is ``1`` and valid values are between ``1`` and ``255``. This only affects ``active-backup`` mode. + For historical reasons, the misspelling ``gratuitious-arp`` is also + accepted and has the same function. + ``packets-per-slave`` (scalar) : In ``balance-rr`` mode, specifies the number of packets to transmit on a slave before switching to the next. When this value is set to @@ -605,6 +657,30 @@ ethernets: eno1: dhcp4: true + +This is an example of a static-configured interface with multiple IPv4 addresses +and multiple gateways with networkd, with equal route metric levels, and static +DNS nameservers (Google DNS for this example): + + network: + version: 2 + renderer: networkd + ethernets: + eno1: + addresses: + - 10.0.0.10/24 + - 11.0.0.11/24 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + routes: + - to: 0.0.0.0/0 + via: 10.0.0.1 + metric: 100 + - to: 0.0.0.0/0 + via: 11.0.0.1 + metric: 100 This is a complex example which shows most available features: diff -Nru netplan.io-0.36.3/doc/netplan-try.md netplan.io-0.40.1~18.04.2/doc/netplan-try.md --- netplan.io-0.36.3/doc/netplan-try.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/doc/netplan-try.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,61 @@ +--- +title: netplan-try +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-try - try a configuration, optionally rolling it back + +# SYNOPSIS + + **netplan** [--debug] **try** -h | --help + + **netplan** [--debug] **try** [--config-file _CONFIG_FILE_] [--timeout _TIMEOUT_] + +# DESCRIPTION + +**netplan try** takes a **netplan**(5) configuration, applies it, and +automatically rolls it back if the user does not confirm the +configuration within a time limit. + +This may be especially useful on remote systems, to prevent an +administrator being permanently locked out of systems in the case of a +network configuration error. + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --config-file _CONFIG_FILE_ +: In addition to the usual configuration, apply _CONFIG_FILE_. It must + be a YAML file in the **netplan**(5) format. + + --timeout _TIMEOUT_ +: Wait for _TIMEOUT_ seconds before reverting. Defaults to 120 + seconds. Note that some network configurations (such as STP) may take + over a minute to settle. + +# KNOWN ISSUES + +**netplan try** uses similar procedures to **netplan apply**, so some +of the same caveats apply around virtual devices. + +There are also some known bugs: if **netplan try** times out or is +cancelled, make sure to verify if the network configuration has in +fact been reverted. + +As with **netplan apply**, a reboot should fix any issues. However, be +sure to verify that the config on disk is in the state you expect +before rebooting! + +# SEE ALSO + + **netplan**(5), **netplan-generate**(8), **netplan-apply**(8) + diff -Nru netplan.io-0.36.3/examples/static_singlenic_multiip_multigateway.yaml netplan.io-0.40.1~18.04.2/examples/static_singlenic_multiip_multigateway.yaml --- netplan.io-0.36.3/examples/static_singlenic_multiip_multigateway.yaml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/examples/static_singlenic_multiip_multigateway.yaml 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,19 @@ +network: + version: 2 + renderer: networkd + ethernets: + eno1: + addresses: + - 10.0.0.10/24 + - 11.0.0.11/24 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + routes: + - to: 0.0.0.0/0 + via: 10.0.0.1 + metric: 100 + - to: 0.0.0.0/0 + via: 11.0.0.1 + metric: 100 diff -Nru netplan.io-0.36.3/.github/pull_request_template.md netplan.io-0.40.1~18.04.2/.github/pull_request_template.md --- netplan.io-0.36.3/.github/pull_request_template.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/.github/pull_request_template.md 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,11 @@ + +## Description + + +## Checklist + +- [ ] Runs `make check` successfully. +- [ ] Retains 100% code coverage (`make check-coverage`). +- [ ] New/changed keys in YAML format are documented. +- [ ] \(Optional\) Closes an open bug in Launchpad. + diff -Nru netplan.io-0.36.3/Makefile netplan.io-0.40.1~18.04.2/Makefile --- netplan.io-0.36.3/Makefile 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/Makefile 2018-10-22 18:52:52.000000000 +0000 @@ -23,7 +23,7 @@ PYFLAKES3 ?= $(shell which pyflakes-3 || which pyflakes3 || echo true) PYCODESTYLE3 ?= $(shell which pycodestyle-3 || which pycodestyle || which pep8 || echo true) -default: generate doc/netplan.5 doc/netplan.html +default: generate doc/netplan.html doc/netplan.5 doc/netplan-generate.8 doc/netplan-apply.8 doc/netplan-try.8 generate: src/generate.[hc] src/parse.[hc] src/util.[hc] src/networkd.[hc] src/nm.[hc] $(CC) $(BUILDFLAGS) $(CFLAGS) -o $@ $(filter %.c, $^) `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid` @@ -37,6 +37,7 @@ tests/generate.py tests/cli.py nosetests3 -v --with-coverage + tests/validate_docs.sh linting: $(PYFLAKES3) $(PYCODE) @@ -66,7 +67,8 @@ install: default mkdir -p $(DESTDIR)/$(SBINDIR) $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan $(DESTDIR)/$(SYSTEMD_GENERATOR_DIR) - mkdir -p $(DESTDIR)/$(MANDIR)/man5 $(DESTDIR)/$(DOCDIR)/netplan/examples + mkdir -p $(DESTDIR)/$(MANDIR)/man5 $(DESTDIR)/$(MANDIR)/man8 + mkdir -p $(DESTDIR)/$(DOCDIR)/netplan/examples mkdir -p $(DESTDIR)/$(DATADIR)/netplan/netplan install -m 755 generate $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan/ find netplan/ -name '*.py' -exec install -Dm 644 "{}" "$(DESTDIR)/$(DATADIR)/netplan/{}" \; @@ -76,13 +78,17 @@ install -m 644 doc/*.html $(DESTDIR)/$(DOCDIR)/netplan/ install -m 644 examples/*.yaml $(DESTDIR)/$(DOCDIR)/netplan/examples/ install -m 644 doc/*.5 $(DESTDIR)/$(MANDIR)/man5/ + install -m 644 doc/*.8 $(DESTDIR)/$(MANDIR)/man8/ install -D -m 644 src/netplan-wpa@.service $(DESTDIR)/$(SYSTEMD_UNIT_DIR)/netplan-wpa@.service install -T -D -m 644 netplan.completions $(DESTDIR)/$(BASH_COMPLETIONS_DIR)/netplan %.html: %.md pandoc -s --toc -o $@ $< -doc/netplan.5: doc/manpage.md doc/netplan.md +doc/netplan.5: doc/manpage-header.md doc/netplan.md doc/manpage-footer.md + pandoc -s -o $@ $^ + +%.8: %.md pandoc -s -o $@ $^ .PHONY: clean diff -Nru netplan.io-0.36.3/netplan/cli/commands/apply.py netplan.io-0.40.1~18.04.2/netplan/cli/commands/apply.py --- netplan.io-0.36.3/netplan/cli/commands/apply.py 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/netplan/cli/commands/apply.py 2018-10-22 19:00:51.000000000 +0000 @@ -24,7 +24,9 @@ import subprocess import netplan.cli.utils as utils -from netplan.configmanager import ConfigurationError +from netplan.configmanager import ConfigManager, ConfigurationError + +import netifaces class NetplanApply(utils.NetplanCommand): @@ -48,7 +50,8 @@ else: raise ConfigurationError("the configuration could not be generated") - devices = os.listdir('/sys/class/net') + config_manager = ConfigManager() + devices = netifaces.interfaces() restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) restart_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*')) @@ -75,23 +78,30 @@ else: logging.debug('no netplan generated NM configuration exists') - # force-hotplug all "down" network interfaces to apply renames - any_replug = False + # evaluate config for extra steps we need to take (like renaming) + # for now, only applies to non-virtual (real) devices. + config_manager.parse() + changes = NetplanApply.process_link_changes(devices, config_manager) + + # if the interface is up, we can still apply some .link file changes for device in devices: - if not os.path.islink('/sys/class/net/' + device): - continue - if NetplanApply.replug(device): - any_replug = True - else: - # if the interface is up, we can still apply .link file changes - logging.debug('netplan triggering .link rules for %s', device) - with open(os.devnull, 'w') as fd: - subprocess.check_call(['udevadm', 'test-builtin', - 'net_setup_link', - '/sys/class/net/' + device], - stdout=fd, stderr=fd) - if any_replug: - subprocess.check_call(['udevadm', 'settle']) + logging.debug('netplan triggering .link rules for %s', device) + subprocess.check_call(['udevadm', 'test-builtin', + 'net_setup_link', + '/sys/class/net/' + device], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + # apply renames to "down" devices + for iface, settings in changes.items(): + if settings.get('name'): + subprocess.check_call(['ip', 'link', 'set', + 'dev', iface, + 'name', settings.get('name')], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + subprocess.check_call(['udevadm', 'settle']) # (re)start backends if restart_networkd: @@ -101,72 +111,74 @@ utils.systemctl_network_manager('start', sync=sync) @staticmethod - def replug(device): # pragma: nocover (covered in autopkgtest) - '''Unbind and rebind device if it is down''' - - devdir = os.path.join('/sys/class/net', device) - - try: - with open(os.path.join(devdir, 'operstate')) as f: - state = f.read().strip() - if state != 'down': - logging.debug('device %s operstate is %s, not replugging', device, state) - return False - except IOError as e: - logging.error('Cannot determine operstate of %s: %s', device, str(e)) - return False + def process_link_changes(interfaces, config_manager): # pragma: nocover (covered in autopkgtest) + """ + Go through the pending changes and pick what needs special + handling. Only applies to "down" interfaces which can be safely + updated. + """ + + changes = {} + phys = dict(config_manager.physical_interfaces) + + # TODO (cyphermox): factor out some of this matching code (and make it + # pretty) in its own module. + matches = {'by-driver': {}, + 'by-mac': {}, + } + for phy, settings in phys.items(): + if not settings: + continue + if phy == 'renderer': + continue + newname = settings.get('set-name') + if not newname: + continue + match = settings.get('match') + if not match: + continue + driver = match.get('driver') + mac = match.get('macaddress') + if driver: + matches['by-driver'][driver] = newname + if mac: + matches['by-mac'][mac] = newname # /sys/class/net/ens3/device -> ../../../virtio0 # /sys/class/net/ens3/device/driver -> ../../../../bus/virtio/drivers/virtio_net - try: - devname = os.path.basename(os.readlink(os.path.join(devdir, 'device'))) - except IOError as e: - logging.debug('Cannot replug %s: cannot read link %s/device: %s', device, devdir, str(e)) - return False - - try: - # we must resolve symlinks here as the device dir will be gone after unbind - subsystem = os.path.realpath(os.path.join(devdir, 'device', 'subsystem')) - subsystem_name = os.path.basename(subsystem) - driver = os.path.realpath(os.path.join(devdir, 'device', 'driver')) - 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 - # workaround for https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1729573 - if subsystem_name == 'xen' and driver_name == 'vif': - logging.debug('replug %s: xen:vif fails on rebinding, ignoring', device) - return False - # workaround for problem with ath9k_htc module: this driver is async and does not support - # sequential unbind / rebind, one soon after the other - if driver_name == 'ath9k_htc': - logging.debug('replug %s: ath9k_htc does not support rebinding, ignoring', device) - return False - # workaround for ath6kl_sdio, interface does not work after unbinding - if 'ath6kl_sdio' in driver_name: - logging.debug('replug %s: ath6kl_sdio driver does not support rebinding, ignoring', device) - return False - # workaround for brcmfmac, interface will be gone after unbind - if 'brcmfmac' in driver_name: - logging.debug('replug %s: brcmfmac drivers do not support rebinding, ignoring', device) - return False - # workaround for qeth: driver does not recognize unbind command - # https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/1756322 - if driver_name == 'qeth': - logging.debug('replug %s: qeth driver do not support rebinding, ignoring (LP: #1756322)', 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) - logging.debug('replug %s: rebinding %s to %s', device, devname, driver) - with open(os.path.join(driver, 'bind'), 'w') as f: - f.write(devname) - except IOError as e: - logging.error('Cannot replug %s: %s', device, str(e)) - return False + for interface in interfaces: + # try to get the device's driver for matching. + devdir = os.path.join('/sys/class/net', interface) + try: + with open(os.path.join(devdir, 'operstate')) as f: + state = f.read().strip() + if state != 'down': + logging.debug('device %s operstate is %s, not changing', interface, state) + continue + except IOError as e: + logging.error('Cannot determine operstate of %s: %s', interface, str(e)) + continue + + try: + driver = os.path.realpath(os.path.join(devdir, 'device', 'driver')) + driver_name = os.path.basename(driver) + except IOError as e: + logging.debug('Cannot replug %s: cannot read link %s/device: %s', interface, devdir, str(e)) + driver_name = None + pass + + link = netifaces.ifaddresses(interface)[netifaces.AF_LINK][0] + macaddress = link.get('addr') + if driver_name in matches['by-driver']: + new_name = matches['by-driver'][driver_name] + logging.debug(new_name) + logging.debug(interface) + if new_name != interface: + changes.update({interface: {'name': new_name}}) + if macaddress in matches['by-mac']: + new_name = matches['by-mac'][macaddress] + if new_name != interface: + changes.update({interface: {'name': new_name}}) - return True + logging.debug(changes) + return changes diff -Nru netplan.io-0.36.3/netplan/cli/commands/ip.py netplan.io-0.40.1~18.04.2/netplan/cli/commands/ip.py --- netplan.io-0.36.3/netplan/cli/commands/ip.py 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/netplan/cli/commands/ip.py 2018-10-04 16:04:57.000000000 +0000 @@ -21,6 +21,7 @@ import os import sys import subprocess +from subprocess import CalledProcessError import netplan.cli.utils as utils @@ -139,7 +140,10 @@ # Extract out of the generator our mapping in a dict. logging.debug('command ip leases: running %s', argv) - out = subprocess.check_output(argv, universal_newlines=True) + try: + out = subprocess.check_output(argv, universal_newlines=True) + except CalledProcessError as e: # pragma: nocover (better be covered in autopkgtest) + sys.exit(1) mapping = {} mapping_s = out.split(',') for keyvalue in mapping_s: diff -Nru netplan.io-0.36.3/netplan/cli/commands/try_command.py netplan.io-0.40.1~18.04.2/netplan/cli/commands/try_command.py --- netplan.io-0.36.3/netplan/cli/commands/try_command.py 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/netplan/cli/commands/try_command.py 2018-10-04 16:04:57.000000000 +0000 @@ -43,7 +43,14 @@ leaf=True) self.configuration_changed = False self.new_interfaces = None - self.config_manager = ConfigManager() + self._config_manager = None + self.t_settings = None + + @property + def config_manager(self): # pragma: nocover (called by later commands) + if not self._config_manager: + self._config_manager = ConfigManager() + return self._config_manager def run(self): # pragma: nocover (requires user input) self.parser.add_argument('--config-file', @@ -63,7 +70,8 @@ try: fd = sys.stdin.fileno() - t = netplan.terminal.Terminal(fd) + self.t = netplan.terminal.Terminal(fd) + self.t.save(self.t_settings) # we really don't want to be interrupted while doing backup/revert operations signal.signal(signal.SIGINT, self._signal_handler) @@ -73,17 +81,18 @@ NetplanApply.command_apply(run_generate=True, sync=True, exit_on_error=False) - t.get_confirmation_input(timeout=self.timeout) + self.t.get_confirmation_input(timeout=self.timeout) except netplan.terminal.InputRejected: print("\nReverting.") self.revert() except netplan.terminal.InputAccepted: print("\nConfiguration accepted.") except Exception as e: - print("\nAn error occured: %s" % e) + print("\nAn error occurred: %s" % e) print("\nReverting.") self.revert() finally: + self.t.reset(self.t_settings) self.cleanup() def backup(self): # pragma: nocover (requires user input) diff -Nru netplan.io-0.36.3/netplan/configmanager.py netplan.io-0.40.1~18.04.2/netplan/configmanager.py --- netplan.io-0.36.3/netplan/configmanager.py 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/netplan/configmanager.py 2018-10-04 17:56:54.000000000 +0000 @@ -52,6 +52,13 @@ return interfaces @property + def physical_interfaces(self): + interfaces = {} + interfaces.update(self.ethernets) + interfaces.update(self.wifis) + return interfaces + + @property def ethernets(self): return self.network['ethernets'] @@ -187,7 +194,9 @@ try: with open(yaml_file) as f: yaml_data = yaml.load(f, Loader=yaml.CSafeLoader) - network = yaml_data.get('network') + network = None + if yaml_data is not None: + network = yaml_data.get('network') if network: if 'ethernets' in network: new = self._merge_interface_config(self.ethernets, network.get('ethernets')) diff -Nru netplan.io-0.36.3/src/generate.c netplan.io-0.40.1~18.04.2/src/generate.c --- netplan.io-0.36.3/src/generate.c 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/src/generate.c 2018-10-04 16:04:57.000000000 +0000 @@ -195,9 +195,9 @@ * To do that, we put all found files in a hash table, then sort it by * file name, and add the entries from /run after the ones from /etc * and those after the ones from /lib. */ - g_autofree char* glob_etc = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "/etc/netplan/*.yaml", NULL); - g_autofree char* glob_run = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "/run/netplan/*.yaml", NULL); - g_autofree char* glob_lib = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "/lib/netplan/*.yaml", NULL); + g_autofree char* glob_etc = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "etc/netplan/*.yaml", NULL); + g_autofree char* glob_run = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "run/netplan/*.yaml", NULL); + g_autofree char* glob_lib = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "lib/netplan/*.yaml", NULL); glob_t gl; int rc; /* keys are strdup()ed, free them; values point into the glob_t, don't free them */ diff -Nru netplan.io-0.36.3/src/networkd.c netplan.io-0.40.1~18.04.2/src/networkd.c --- netplan.io-0.36.3/src/networkd.c 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/src/networkd.c 2018-10-22 18:52:52.000000000 +0000 @@ -86,6 +86,7 @@ write_link_file(net_definition* def, const char* rootdir, const char* path) { GString* s = NULL; + mode_t orig_umask; /* Don't write .link files for virtual devices; they use .netdev instead */ if (def->type >= ND_VIRTUAL) @@ -110,7 +111,9 @@ g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); + orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); + umask(orig_umask); } @@ -187,8 +190,8 @@ } if (def->bond_params.fail_over_mac_policy) g_string_append_printf(params, "\nFailOverMACPolicy=%s", def->bond_params.fail_over_mac_policy); - if (def->bond_params.gratuitious_arp) - g_string_append_printf(params, "\nGratuitiousARP=%d", def->bond_params.gratuitious_arp); + if (def->bond_params.gratuitous_arp) + g_string_append_printf(params, "\nGratuitousARP=%d", def->bond_params.gratuitous_arp); /* TODO: add unsolicited_na, not documented as supported by NM. */ if (def->bond_params.packets_per_slave) g_string_append_printf(params, "\nPacketsPerSlave=%d", def->bond_params.packets_per_slave); @@ -209,6 +212,7 @@ write_netdev_file(net_definition* def, const char* rootdir, const char* path) { GString* s = NULL; + mode_t orig_umask; g_assert(def->type >= ND_VIRTUAL); @@ -242,7 +246,11 @@ // LCOV_EXCL_STOP } + /* these do not contain secrets and need to be readable by + * systemd-networkd - LP: #1736965 */ + orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".netdev"); + umask(orig_umask); } static void @@ -250,13 +258,14 @@ { g_string_append_printf(s, "\n[Route]\n"); - g_string_append_printf(s, "Destination=%s\nGateway=%s\n", - r->to, r->via); + g_string_append_printf(s, "Destination=%s\n", r->to); + if (r->via) + g_string_append_printf(s, "Gateway=%s\n", r->via); if (r->from) - g_string_append_printf(s, "From=%s\n", r->from); + g_string_append_printf(s, "PreferredSource=%s\n", r->from); - if (r->scope) + if (g_strcmp0(r->scope, "global") != 0) g_string_append_printf(s, "Scope=%s\n", r->scope); if (g_strcmp0(r->type, "unicast") != 0) g_string_append_printf(s, "Type=%s\n", r->type); @@ -292,19 +301,30 @@ write_network_file(net_definition* def, const char* rootdir, const char* path) { GString* s = NULL; + mode_t orig_umask; /* 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->ip4_nameservers && !def->ip6_nameservers && !def->has_vlans) + !def->ip4_nameservers && !def->ip6_nameservers && !def->has_vlans && + def->type < ND_VIRTUAL) return; /* build file contents */ s = g_string_sized_new(200); append_match_section(def, s, TRUE); - if (def->optional) - g_string_append(s, "\n[Link]\nRequiredForOnline=no\n"); + if (def->optional || def->optional_addresses) { + g_string_append(s, "\n[Link]\n"); + if (def->optional) { + g_string_append(s, "RequiredForOnline=no\n"); + } + for (unsigned i = 0; optional_address_options[i].name != NULL; ++i) { + if (def->optional_addresses & optional_address_options[i].flag) { + g_string_append_printf(s, "OptionalAddresses=%s\n", optional_address_options[i].name); + } + } + } g_string_append(s, "\n[Network]\n"); if (def->dhcp4 && def->dhcp6) @@ -313,6 +333,21 @@ g_string_append(s, "DHCP=ipv4\n"); else if (def->dhcp6) g_string_append(s, "DHCP=ipv6\n"); + + /* Set link local addressing -- this does not apply to bond and bridge + * member interfaces, which always get it disabled. + */ + if (!def->bond && !def->bridge && (def->linklocal.ipv4 || def->linklocal.ipv6)) { + if (def->linklocal.ipv4 && def->linklocal.ipv6) + g_string_append(s, "LinkLocalAddressing=yes\n"); + else if (def->linklocal.ipv4) + g_string_append(s, "LinkLocalAddressing=ipv4\n"); + else if (def->linklocal.ipv6) + g_string_append(s, "LinkLocalAddressing=ipv6\n"); + } else { + g_string_append(s, "LinkLocalAddressing=no\n"); + } + if (def->ip4_addresses) for (unsigned i = 0; i < def->ip4_addresses->len; ++i) g_string_append_printf(s, "Address=%s\n", g_array_index(def->ip4_addresses, char*, i)); @@ -339,8 +374,12 @@ g_string_append_printf(s, " %s", g_array_index(def->search_domains, char*, i)); g_string_append(s, "\n"); } + + if (def->type >= ND_VIRTUAL) + g_string_append(s, "ConfigureWithoutCarrier=yes\n"); + if (def->bridge) { - g_string_append_printf(s, "Bridge=%s\nLinkLocalAddressing=no\n", def->bridge); + g_string_append_printf(s, "Bridge=%s\n", def->bridge); if (def->bridge_params.path_cost || def->bridge_params.port_priority) g_string_append_printf(s, "\n[Bridge]\n"); @@ -350,7 +389,7 @@ g_string_append_printf(s, "Priority=%u\n", def->bridge_params.port_priority); } if (def->bond) { - g_string_append_printf(s, "Bond=%s\nLinkLocalAddressing=no\n", def->bond); + g_string_append_printf(s, "Bond=%s\n", def->bond); if (def->bond_params.primary_slave) g_string_append_printf(s, "PrimarySlave=true\n"); @@ -391,7 +430,11 @@ g_string_append_printf(s, "CriticalConnection=true\n"); } + /* these do not contain secrets and need to be readable by + * systemd-networkd - LP: #1736965 */ + orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".network"); + umask(orig_umask); } static void @@ -399,6 +442,7 @@ { GString* s = NULL; g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL); + mode_t orig_umask; /* do we need to write a .rules file? * It's only required for reliably setting the name of a physical device @@ -432,7 +476,9 @@ g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name); + orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, NULL); + umask(orig_umask); } static void diff -Nru netplan.io-0.36.3/src/nm.c netplan.io-0.40.1~18.04.2/src/nm.c --- netplan.io-0.36.3/src/nm.c 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/src/nm.c 2018-10-04 16:04:57.000000000 +0000 @@ -134,6 +134,31 @@ if (cur_route->family != family) continue; + if (cur_route->type && g_ascii_strcasecmp(cur_route->type, "unicast") != 0) { + g_fprintf(stderr, "ERROR: %s: NetworkManager only supports unicast routes\n", def->id); + exit(1); + } + + if (cur_route->scope && g_ascii_strcasecmp(cur_route->scope, "global") != 0) { + g_fprintf(stderr, "ERROR: %s: NetworkManager only supports global scoped routes\n", def->id); + exit(1); + } + + if (cur_route->table != ROUTE_TABLE_UNSPEC) { + g_fprintf(stderr, "ERROR: %s: NetworkManager does not support non-default routing tables\n", def->id); + exit(1); + } + + if (cur_route->from) { + g_fprintf(stderr, "ERROR: %s: NetworkManager does not support routes with 'from'\n", def->id); + exit(1); + } + + if (cur_route->onlink) { + g_fprintf(stderr, "ERROR: %s: NetworkManager does not support on-link routes\n", def->id); + exit(1); + } + g_string_append_printf(s, "route%d=%s,%s", j, cur_route->to, cur_route->via); if (cur_route->metric != METRIC_UNSPEC) @@ -185,9 +210,12 @@ g_string_append_printf(params, "\ndowndelay=%s", def->bond_params.down_delay); if (def->bond_params.fail_over_mac_policy) g_string_append_printf(params, "\nfail_over_mac=%s", def->bond_params.fail_over_mac_policy); - if (def->bond_params.gratuitious_arp) - g_string_append_printf(params, "\nnum_grat_arp=%d", def->bond_params.gratuitious_arp); - /* TODO: add unsolicited_na, not documented as supported by NM. */ + if (def->bond_params.gratuitous_arp) { + g_string_append_printf(params, "\nnum_grat_arp=%d", def->bond_params.gratuitous_arp); + /* Work around issue in NM where unset unsolicited_na will overwrite num_grat_arp: + * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ + g_string_append_printf(params, "\nnum_unsol_na=%d", def->bond_params.gratuitous_arp); + } if (def->bond_params.packets_per_slave) g_string_append_printf(params, "\npackets_per_slave=%d", def->bond_params.packets_per_slave); if (def->bond_params.primary_reselect_policy) @@ -402,8 +430,12 @@ g_string_append_printf(s, "%s;", g_array_index(def->ip4_nameservers, char*, i)); g_string_append(s, "\n"); } - write_search_domains(def, s); - write_routes(def, s, AF_INET); + + /* We can only write search domains and routes if we have an address */ + if (def->ip4_addresses || def->dhcp4) { + write_search_domains(def, s); + write_routes(def, s, AF_INET); + } if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers) { g_string_append(s, "\n[ipv6]\n"); @@ -420,7 +452,7 @@ g_string_append(s, "\n"); } /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] -- - * do we really need to repeat it here? */ + * We need to specify it here for the IPv6-only case - see LP: #1786726 */ write_search_domains(def, s); /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */ diff -Nru netplan.io-0.36.3/src/parse.c netplan.io-0.40.1~18.04.2/src/parse.c --- netplan.io-0.36.3/src/parse.c 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/src/parse.c 2018-10-22 18:52:52.000000000 +0000 @@ -294,7 +294,7 @@ } /** - * Generic handler for setting a cur_netdef ID/iface name field refering to an + * Generic handler for setting a cur_netdef ID/iface name field referring to an * existing ID from a scalar node * @data: offset into net_definition where the net_definition* field to write is * located @@ -718,6 +718,61 @@ return TRUE; } +static gboolean +handle_link_local(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ipv4 = FALSE; + gboolean ipv6 = FALSE; + + 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 (g_ascii_strcasecmp(scalar(entry), "ipv4") == 0) + ipv4 = TRUE; + else if (g_ascii_strcasecmp(scalar(entry), "ipv6") == 0) + ipv6 = TRUE; + else + return yaml_error(node, error, "invalid value for link-local: %s", scalar(entry)); + } + + cur_netdef->linklocal.ipv4 = ipv4; + cur_netdef->linklocal.ipv6 = ipv6; + + return TRUE; +} + +struct optional_address_option optional_address_options[] = { + {"ipv4-ll", OPTIONAL_IPV4_LL}, + {"ipv6-ra", OPTIONAL_IPV6_RA}, + {"dhcp4", OPTIONAL_DHCP4}, + {"dhcp6", OPTIONAL_DHCP6}, + {"static", OPTIONAL_STATIC}, + {NULL}, +}; + +static gboolean +handle_optional_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++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + int found = FALSE; + + for (unsigned i = 0; optional_address_options[i].name != NULL; ++i) { + if (g_ascii_strcasecmp(scalar(entry), optional_address_options[i].name) == 0) { + cur_netdef->optional_addresses |= optional_address_options[i].flag; + found = TRUE; + break; + } + } + if (!found) { + return yaml_error(node, error, "invalid value for optional-addresses: %s", scalar(entry)); + } + } + return TRUE; +} static int get_ip_family(const char* address) @@ -1055,6 +1110,7 @@ cur_route = g_new0(ip_route, 1); cur_route->type = g_strdup("unicast"); + cur_route->scope = g_strdup("global"); cur_route->family = G_MAXUINT; /* 0 is a valid family ID */ cur_route->metric = G_MAXUINT; /* 0 is a valid metric */ @@ -1066,8 +1122,13 @@ g_array_append_val(cur_netdef->routes, cur_route); } - if (g_ascii_strcasecmp(cur_route->type, "unicast") == 0 && - (!cur_route->to || !cur_route->via)) + if ( ( g_ascii_strcasecmp(cur_route->scope, "link") == 0 + || g_ascii_strcasecmp(cur_route->scope, "host") == 0) + && !cur_route->to) + return yaml_error(node, error, "link and host routes must specify a 'to' IP"); + else if ( g_ascii_strcasecmp(cur_route->type, "unicast") == 0 + && g_ascii_strcasecmp(cur_route->scope, "global") == 0 + && (!cur_route->to || !cur_route->via)) return yaml_error(node, error, "unicast route must include both a 'to' and 'via' IP"); else if (g_ascii_strcasecmp(cur_route->type, "unicast") != 0 && !cur_route->to) return yaml_error(node, error, "non-unicast routes must specify a 'to' IP"); @@ -1193,7 +1254,9 @@ {"up-delay", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.up_delay)}, {"down-delay", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.down_delay)}, {"fail-over-mac-policy", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.fail_over_mac_policy)}, - {"gratuitious-arp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.gratuitious_arp)}, + {"gratuitous-arp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.gratuitous_arp)}, + /* Handle the old misspelling */ + {"gratuitious-arp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.gratuitous_arp)}, /* TODO: unsolicited_na */ {"packets-per-slave", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.packets_per_slave)}, {"primary-reselect-policy", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.primary_reselect_policy)}, @@ -1233,111 +1296,63 @@ {NULL} }; +/* Handlers shared by all link types */ +#define COMMON_LINK_HANDLERS \ + {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, \ + {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, \ + {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, \ + {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, \ + {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, \ + {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, \ + {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, \ + {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, \ + {"link-local", YAML_SEQUENCE_NODE, handle_link_local}, \ + {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, \ + {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, \ + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, \ + {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, \ + {"optional-addresses", YAML_SEQUENCE_NODE, handle_optional_addresses}, \ + {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, \ + {"routes", YAML_SEQUENCE_NODE, handle_routes}, \ + {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules} + +/* Handlers for physical links */ +#define PHYSICAL_LINK_HANDLERS \ + {"match", YAML_MAPPING_NODE, handle_match}, \ + {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, \ + {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)} + const mapping_entry_handler ethernet_def_handlers[] = { - {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, - {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, - {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, - {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, - {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, - {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, - {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, - {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, - {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, - {"match", YAML_MAPPING_NODE, handle_match}, - {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, - {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, - {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, - {"routes", YAML_SEQUENCE_NODE, handle_routes}, - {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}, - {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, - {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)}, - {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, - {NULL} + COMMON_LINK_HANDLERS, + PHYSICAL_LINK_HANDLERS, + {NULL}, }; const mapping_entry_handler wifi_def_handlers[] = { - {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, + COMMON_LINK_HANDLERS, + PHYSICAL_LINK_HANDLERS, {"access-points", YAML_MAPPING_NODE, handle_wifi_access_points}, - {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, - {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, - {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, - {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, - {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, - {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, - {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, - {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, - {"match", YAML_MAPPING_NODE, handle_match}, - {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, - {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, - {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, - {"routes", YAML_SEQUENCE_NODE, handle_routes}, - {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}, - {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, - {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)}, - {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, {NULL} }; const mapping_entry_handler bridge_def_handlers[] = { - {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, - {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, - {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, - {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, - {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, - {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, - {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, - {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + COMMON_LINK_HANDLERS, {"interfaces", YAML_SEQUENCE_NODE, handle_bridge_interfaces, NULL, NULL}, - {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, - {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, - {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"parameters", YAML_MAPPING_NODE, handle_bridge}, - {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, - {"routes", YAML_SEQUENCE_NODE, handle_routes}, - {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}, - {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, {NULL} }; const mapping_entry_handler bond_def_handlers[] = { - {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, - {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, - {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, - {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, - {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, - {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, - {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, - {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + COMMON_LINK_HANDLERS, {"interfaces", YAML_SEQUENCE_NODE, handle_bond_interfaces, NULL, NULL}, - {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, - {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, - {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, {"parameters", YAML_MAPPING_NODE, handle_bonding}, - {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, - {"routes", YAML_SEQUENCE_NODE, handle_routes}, - {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}, - {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, {NULL} }; const mapping_entry_handler vlan_def_handlers[] = { - {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, - {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, - {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, - {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, - {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, - {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, - {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, - {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, + COMMON_LINK_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)}, - {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, - {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, - {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, - {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, - {"routes", YAML_SEQUENCE_NODE, handle_routes}, - {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}, - {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, {NULL} }; @@ -1439,6 +1454,8 @@ cur_netdef->id = g_strdup(scalar(key)); cur_netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */ cur_netdef->dhcp_identifier = g_strdup("duid"); /* keep networkd's default */ + /* systemd-networkd defaults to IPv6 LL enabled; keep that default */ + cur_netdef->linklocal.ipv6 = TRUE; g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef); } diff -Nru netplan.io-0.36.3/src/parse.h netplan.io-0.40.1~18.04.2/src/parse.h --- netplan.io-0.36.3/src/parse.h 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/src/parse.h 2018-10-22 18:52:52.000000000 +0000 @@ -55,6 +55,21 @@ ACCEPT_RA_DISABLED, } ra_mode; +typedef enum { + OPTIONAL_IPV4_LL = 1<<0, + OPTIONAL_IPV6_RA = 1<<1, + OPTIONAL_DHCP4 = 1<<2, + OPTIONAL_DHCP6 = 1<<3, + OPTIONAL_STATIC = 1<<4, +} optional_addr; + +struct optional_address_option { + char* name; + optional_addr flag; +}; + +extern struct optional_address_option optional_address_options[]; + typedef struct missing_node { char* netdef_id; const yaml_node_t* node; @@ -72,6 +87,7 @@ /* status options */ gboolean optional; + optional_addr optional_addresses; gboolean critical; /* addresses */ @@ -88,6 +104,10 @@ GArray* search_domains; GArray* routes; GArray* ip_rules; + struct { + gboolean ipv4; + gboolean ipv6; + } linklocal; /* master ID for slave devices */ char* bridge; @@ -132,7 +152,7 @@ char* up_delay; char* down_delay; char* fail_over_mac_policy; - guint gratuitious_arp; + guint gratuitous_arp; /* TODO: unsolicited_na */ guint packets_per_slave; char* primary_reselect_policy; diff -Nru netplan.io-0.36.3/tests/cli.py netplan.io-0.40.1~18.04.2/tests/cli.py --- netplan.io-0.36.3/tests/cli.py 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/tests/cli.py 2018-10-04 17:56:54.000000000 +0000 @@ -67,6 +67,20 @@ self.assertEqual(out, b'') self.assertEqual(os.listdir(self.workdir.name), []) + def test_with_empty_config(self): + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + open(os.path.join(c, 'a.yaml'), 'w').close() + with open(os.path.join(c, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + enlol: {dhcp4: yes}''') + out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT) + self.assertEqual(out, b'') + self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), + ['10-netplan-enlol.network']) + def test_with_config(self): c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) @@ -90,11 +104,11 @@ ethernets: enlol: {dhcp4: yes}''') p = subprocess.Popen(exe_cli + - ['generate', '--root-dir', self.workdir.name, '--mapping', 'inexistant'], + ['generate', '--root-dir', self.workdir.name, '--mapping', 'nonexistent'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertNotEqual(p.returncode, 0) - self.assertNotIn(b'inexistant', out) + self.assertNotIn(b'nonexistent', out) def test_mapping_for_interface(self): os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') @@ -541,7 +555,7 @@ self.assertEqual(out, b'') self.assertIn(b'Expected 3 fields for stanza type iface but got 4', err) - def test_write_file_notsupported(self): + def test_write_file_unsupported(self): (out, err) = self.do_test('iface en1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'non-automatic interfaces are not supported', err) @@ -566,7 +580,7 @@ c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: - # match against loopback so as to succesfully get a predictable + # match against loopback so as to successfully get a predictable # ifindex f.write('''network: version: 2 @@ -597,7 +611,7 @@ c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: - # match against loopback so as to succesfully get a predictable + # match against loopback so as to successfully get a predictable # ifindex f.write('''network: version: 2 @@ -621,7 +635,7 @@ c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: - # match against loopback so as to succesfully get a predictable + # match against loopback so as to successfully get a predictable # ifindex f.write('''network: version: 2 diff -Nru netplan.io-0.36.3/tests/generate.py netplan.io-0.40.1~18.04.2/tests/generate.py --- netplan.io-0.36.3/tests/generate.py 2018-07-04 18:02:54.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/tests/generate.py 2018-10-22 18:52:52.000000000 +0000 @@ -19,9 +19,11 @@ # along with this program. If not, see . import os +import random import re import sys import stat +import string import tempfile import textwrap import subprocess @@ -34,10 +36,10 @@ os.environ['G_DEBUG'] = 'fatal-criticals' # common patterns for expected output -ND_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' -ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\n\n[DHCP]\nUseMTU=true\nRouteMetric=600\n' -ND_DHCP6 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' -ND_DHCPYES = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' +ND_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' +ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nUseMTU=true\nRouteMetric=600\n' +ND_DHCP6 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing=ipv6\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' +ND_DHCPYES = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nUseMTU=true\nRouteMetric=100\n' UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' @@ -79,6 +81,14 @@ self.assertEqual(out, '') return err + def eth_name(self): + """Return a link name. + + Use when you need a link name for a test but don't want to + encode a made up name in the test. + """ + return 'eth' + ''.join(random.sample(string.ascii_letters + string.digits, k=4)) + def assert_networkd(self, file_contents_map): networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') if not file_contents_map: @@ -106,6 +116,12 @@ with open(os.path.join(udev_dir, '99-netplan-' + fname)) as f: self.assertEqual(f.read(), contents) + def get_network_config_for_link(self, link_name): + """Return the content of the .network file for `link_name`.""" + networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') + with open(os.path.join(networkd_dir, '10-netplan-{}.network'.format(link_name))) as f: + return f.read() + def assert_nm(self, connections_map=None, conf=None): # check config conf_path = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'conf.d', 'netplan.conf') @@ -307,6 +323,7 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -314,6 +331,39 @@ '''}) self.assert_networkd_udev(None) + def config_with_optional_addresses(self, eth_name, optional_addresses): + return '''network: + version: 2 + ethernets: + {}: + dhcp6: true + optional-addresses: {}'''.format(eth_name, optional_addresses) + + def get_optional_addresses(self, eth_name): + config = self.get_network_config_for_link(eth_name) + r = set() + prefix = "OptionalAddresses=" + for line in config.splitlines(): + if line.startswith(prefix): + r.add(line[len(prefix):]) + return r + + def test_eth_optional_addresses(self): + eth_name = self.eth_name() + self.generate(self.config_with_optional_addresses(eth_name, '["dhcp4"]')) + self.assertEqual(self.get_optional_addresses(eth_name), set(["dhcp4"])) + + def test_eth_optional_addresses_multiple(self): + eth_name = self.eth_name() + self.generate(self.config_with_optional_addresses(eth_name, '[dhcp4, ipv4-ll, ipv6-ra, dhcp6, dhcp4, static]')) + self.assertEqual( + self.get_optional_addresses(eth_name), + set(["ipv4-ll", "ipv6-ra", "dhcp4", "dhcp6", "static"])) + + def test_eth_optional_addresses_invalid(self): + eth_name = self.eth_name() + self.generate(self.config_with_optional_addresses(eth_name, '["invalid"]'), expect_fail=True) + def test_eth_wol(self): self.generate('''network: version: 2 @@ -361,10 +411,24 @@ id: 108""")) self.assert_networkd({ 'bond0.108.netdev': '[NetDev]\nName=bond0.108\nKind=vlan\n\n[VLAN]\nId=108\n', + 'bond0.108.network': '''[Match] +Name=bond0.108 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +''', 'bond0.netdev': '[NetDev]\nName=bond0\nMTUBytes=9000\nKind=bond\n', - 'bond0.network': '[Match]\nName=bond0\n\n[Network]\nVLAN=bond0.108\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +VLAN=bond0.108 +''', 'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n', - 'eth1.network': '[Match]\nName=eth1\n\n[Network]\nBond=bond0\nLinkLocalAddressing=no\n' + 'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n' }) self.assert_networkd_udev(None) @@ -423,6 +487,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -506,7 +571,7 @@ match: {} dhcp4: true''') - self.assert_networkd({'def1.network': '[Match]\n\n[Network]\nDHCP=ipv4\n\n' + self.assert_networkd({'def1.network': '[Match]\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n' '[DHCP]\nUseMTU=true\nRouteMetric=100\n'}) self.assert_networkd_udev(None) self.assert_nm(None, '''[keyfile] @@ -529,6 +594,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -579,7 +645,18 @@ macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'br0.network': ND_DHCP4 % 'br0', + self.assert_networkd({'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'br0.netdev': '[NetDev]\nName=br0\nMACAddress=00:01:02:03:04:05\nKind=bridge\n'}) def test_eth_def_renderer(self): @@ -618,6 +695,7 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 IPv6AcceptRA=no [DHCP] @@ -643,7 +721,9 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 IPv6AcceptRA=no +ConfigureWithoutCarrier=yes [DHCP] UseMTU=true @@ -657,9 +737,9 @@ Name=engreen [Network] +LinkLocalAddressing=no IPv6AcceptRA=no Bridge=br0 -LinkLocalAddressing=no '''}) def test_bond_dhcp6_no_accept_ra(self): @@ -679,7 +759,9 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 IPv6AcceptRA=yes +ConfigureWithoutCarrier=yes [DHCP] UseMTU=true @@ -693,9 +775,9 @@ Name=engreen [Network] +LinkLocalAddressing=no IPv6AcceptRA=no Bond=bond0 -LinkLocalAddressing=no '''}) def test_eth_dhcp6_accept_ra(self): @@ -710,6 +792,7 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 IPv6AcceptRA=yes [DHCP] @@ -728,6 +811,7 @@ [Network] DHCP=ipv6 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -754,6 +838,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 '''}) @@ -773,6 +858,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 @@ -795,6 +881,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -816,6 +903,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -838,6 +926,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [DHCP] UseMTU=true @@ -860,6 +949,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -889,6 +979,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -923,6 +1014,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -949,6 +1041,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -965,7 +1058,6 @@ addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 - via: 192.168.14.20 scope: link metric: 100 ''') @@ -974,11 +1066,11 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 -Gateway=192.168.14.20 Scope=link Metric=100 '''}) @@ -1001,6 +1093,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1027,6 +1120,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1054,6 +1148,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1079,6 +1174,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1097,7 +1193,7 @@ routes: - to: 10.10.10.0/24 via: 192.168.14.20 - from: 192.168.14.2/32 + from: 192.168.14.2 metric: 100 ''') @@ -1105,12 +1201,13 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 -From=192.168.14.2/32 +PreferredSource=192.168.14.2 Metric=100 '''}) @@ -1128,6 +1225,7 @@ Name=enblue [Network] +LinkLocalAddressing=ipv6 Address=192.168.1.3/24 [Route] @@ -1150,6 +1248,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1173,6 +1272,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] @@ -1198,6 +1298,7 @@ Name=enblue [Network] +LinkLocalAddressing=ipv6 Address=192.168.1.3/24 [Route] @@ -1270,6 +1371,7 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 [Route] Destination=10.10.10.0/24 @@ -1318,7 +1420,18 @@ dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0'}) + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd unmanaged-devices+=interface-name:br0,''') @@ -1334,7 +1447,18 @@ dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0'}) + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd unmanaged-devices+=interface-name:br0,''') @@ -1357,7 +1481,9 @@ [Network] DHCP=ipv4 +LinkLocalAddressing=ipv6 Address=1.2.3.4/12 +ConfigureWithoutCarrier=yes [DHCP] UseMTU=true @@ -1383,11 +1509,22 @@ ''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_eth_bridge_nm_blacklist(self): self.generate('''network: @@ -1419,11 +1556,22 @@ dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_bridge_params(self): self.generate('''network: @@ -1456,12 +1604,23 @@ 'HelloTimeSec=6\n' 'MaxAgeSec=24\n' 'STP=true\n', - 'br0.network': ND_DHCP4 % 'br0', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' '[Bridge]\nCost=70\nPriority=14\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_bond_empty(self): self.generate('''network: @@ -1471,7 +1630,18 @@ dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', - 'bn0.network': ND_DHCP4 % 'bn0'}) + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd unmanaged-devices+=interface-name:bn0,''') @@ -1491,11 +1661,22 @@ dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', - 'bn0.network': ND_DHCP4 % 'bn0', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_empty_parameters(self): self.generate('''network: @@ -1512,11 +1693,22 @@ dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', - 'bn0.network': ND_DHCP4 % 'bn0', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_with_parameters(self): self.generate('''network: @@ -1569,16 +1761,27 @@ 'UpDelaySec=20ms\n' 'DownDelaySec=30ms\n' 'FailOverMACPolicy=none\n' - 'GratuitiousARP=10\n' + 'GratuitousARP=10\n' 'PacketsPerSlave=10\n' 'PrimaryReselectPolicy=none\n' 'ResendIGMP=10\n' 'LearnPacketIntervalSec=10\n', - 'bn0.network': ND_DHCP4 % 'bn0', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_with_parameters_all_suffix(self): self.generate('''network: @@ -1606,11 +1809,22 @@ 'ARPIntervalSec=15m\n' 'UpDelaySec=20ms\n' 'DownDelaySec=30s\n', - 'bn0.network': ND_DHCP4 % 'bn0', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_primary_slave(self): self.generate('''network: @@ -1631,11 +1845,60 @@ self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=active-backup\n', - 'bn0.network': ND_DHCP4 % 'bn0', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\nPrimarySlave=true\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_with_gratuitous_spelling(self): + """Validate that the correct spelling of gratuitous also works""" + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: active-backup + gratuitous-arp: 10 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=active-backup\n' + 'GratuitousARP=10\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\nPrimarySlave=true\n', + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBond=bn0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_gateway(self): self.generate('''network: @@ -1650,6 +1913,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 Gateway=192.168.14.1 @@ -1674,6 +1938,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 DNS=1.2.3.4 DNS=1234::FFFF @@ -1682,6 +1947,7 @@ Name=enblue [Network] +LinkLocalAddressing=ipv6 Address=192.168.1.3/24 DNS=8.8.8.8 Domains=lab kitchen @@ -1703,12 +1969,64 @@ macaddress: aa:bb:cc:dd:ee:11 engreen: {id: 2, link: en1, dhcp6: true}''') - self.assert_networkd({'en1.network': '[Match]\nName=en1\n\n[Network]\nVLAN=engreen\nVLAN=enblue\nVLAN=enred\n', - 'enblue.netdev': '[NetDev]\nName=enblue\nKind=vlan\n\n[VLAN]\nId=1\n', - 'engreen.netdev': '[NetDev]\nName=engreen\nKind=vlan\n\n[VLAN]\nId=2\n', - 'enred.netdev': '[NetDev]\nName=enred\nMACAddress=aa:bb:cc:dd:ee:11\nKind=vlan\n\n[VLAN]\nId=3\n', - 'enblue.network': '[Match]\nName=enblue\n\n[Network]\nAddress=1.2.3.4/24\n', - 'engreen.network': ND_DHCP6 % 'engreen'}) + self.assert_networkd({'en1.network': '''[Match] +Name=en1 + +[Network] +LinkLocalAddressing=ipv6 +VLAN=engreen +VLAN=enblue +VLAN=enred +''', + 'enblue.netdev': '''[NetDev] +Name=enblue +Kind=vlan + +[VLAN] +Id=1 +''', + 'engreen.netdev': '''[NetDev] +Name=engreen +Kind=vlan + +[VLAN] +Id=2 +''', + 'enred.netdev': '''[NetDev] +Name=enred +MACAddress=aa:bb:cc:dd:ee:11 +Kind=vlan + +[VLAN] +Id=3 +''', + 'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=1.2.3.4/24 +ConfigureWithoutCarrier=yes +''', + 'enred.network': '''[Match] +Name=enred + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +''', + 'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd unmanaged-devices+=interface-name:engreen,interface-name:en1,interface-name:enblue,interface-name:enred,''') @@ -1729,6 +2047,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] @@ -1751,6 +2070,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] @@ -1773,6 +2093,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] @@ -1795,6 +2116,7 @@ Name=engreen [Network] +LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] @@ -1802,6 +2124,94 @@ TypeOfService=250 '''}) + def test_link_local_all(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv4, ipv6 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) + + def test_link_local_ipv4(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv4 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv4 + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) + + def test_link_local_ipv6(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv6 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) + + def test_link_local_disabled(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=no + +[DHCP] +UseMTU=true +RouteMetric=100 +'''}) + class TestNetworkManager(TestBase): def test_eth_wol(self): @@ -2568,6 +2978,91 @@ route2=2001:f00f:f00f::fe/64,2001:beef:feed::1 '''}) + def test_route_reject_from(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + from: 192.168.14.2 + ''', expect_fail=True) + self.assertIn("NetworkManager does not support routes with 'from'", err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_route_reject_onlink(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + on-link: true + ''', expect_fail=True) + self.assertIn('NetworkManager does not support on-link routes', err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_route_reject_table(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + table: 31337 + ''', expect_fail=True) + self.assertIn('NetworkManager does not support non-default routing tables', err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_route_reject_scope(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + scope: host + ''', expect_fail=True) + self.assertIn('NetworkManager only supports global scoped routes', err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_route_reject_type(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + type: blackhole + ''', expect_fail=True) + self.assertIn('NetworkManager only supports unicast routes', err) + + self.assert_nm({}) + self.assert_networkd({}) + def test_wifi_default(self): self.generate('''network: version: 2 @@ -3282,6 +3777,7 @@ downdelay=10 fail_over_mac=none num_grat_arp=10 +num_unsol_na=10 packets_per_slave=10 primary_reselect=none resend_igmp=10 @@ -4019,6 +4515,19 @@ - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) + def test_device_route_scope_link_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + scope: link + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + def test_device_route_invalid_onlink(self): self.generate('''network: version: 2 @@ -4361,6 +4870,24 @@ accept-ra: j''', expect_fail=True) self.assertIn('invalid boolean', err) + def test_invalid_link_local_set(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: invalid''', expect_fail=True) + + def test_invalid_link_local_value(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ invalid, ]''', expect_fail=True) + class TestForwardDeclaration(TestBase): @@ -4386,18 +4913,35 @@ ''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', - 'bond0.network': '[Match]\nName=bond0\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 +''', 'eth0.link': '[Match]\nMACAddress=00:01:02:03:04:05\n\n' '[Link]\nName=eth0\nWakeOnLan=off\n', 'eth0.network': '[Match]\nMACAddress=00:01:02:03:04:05\nName=eth0\n\n' - '[Network]\nBond=bond0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth1.link': '[Match]\nMACAddress=02:01:02:03:04:05\n\n' '[Link]\nName=eth1\nWakeOnLan=off\n', 'eth1.network': '[Match]\nMACAddress=02:01:02:03:04:05\nName=eth1\n\n' - '[Network]\nBond=bond0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBond=bond0\n'}) def test_fwdecl_feature_blend(self): self.generate('''network: @@ -4437,29 +4981,60 @@ self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n' '[VLAN]\nId=1\n', - 'vlan1.network': ND_DHCP4 % 'vlan1', + 'vlan1.network': '''[Match] +Name=vlan1 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' '[Bridge]\nSTP=true\n', - 'br0.network': '[Match]\nName=br0\n\n' - '[Network]\nVLAN=vlan1\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +VLAN=vlan1 +''', 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', - 'bond0.network': '[Match]\nName=bond0\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n\n' - '[Bridge]\nCost=8888\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 + +[Bridge] +Cost=8888 +''', 'eth2.network': '[Match]\nName=eth2\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' '[Bridge]\nCost=1000\n', 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n', - 'br1.network': '[Match]\nName=br1\n\n' - '[Network]\nBond=bond0\nLinkLocalAddressing=no\n', + 'br1.network': '''[Match] +Name=br1 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bond=bond0 +''', 'eth0.link': '[Match]\nMACAddress=00:01:02:03:04:05\n\n' '[Link]\nName=eth0\nWakeOnLan=off\n', 'eth0.network': '[Match]\nMACAddress=00:01:02:03:04:05\nName=eth0\n\n' - '[Network]\nBond=bond0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth1.link': '[Match]\nMACAddress=02:01:02:03:04:05\n\n' '[Link]\nName=eth1\nWakeOnLan=off\n', 'eth1.network': '[Match]\nMACAddress=02:01:02:03:04:05\nName=eth1\n\n' - '[Network]\nBridge=br1\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBridge=br1\n'}) class TestMerging(TestBase): @@ -4555,11 +5130,22 @@ dhcp4: true'''}) self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': ND_DHCP4 % 'br0', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +UseMTU=true +RouteMetric=100 +''', 'eno1.network': '[Match]\nName=eno1\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n', + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nBridge=br0\nLinkLocalAddressing=no\n'}) + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_def_in_run(self): rundir = os.path.join(self.workdir.name, 'run', 'netplan') diff -Nru netplan.io-0.36.3/tests/integration.py netplan.io-0.40.1~18.04.2/tests/integration.py --- netplan.io-0.36.3/tests/integration.py 2018-07-05 14:54:35.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/tests/integration.py 2018-10-22 19:00:18.000000000 +0000 @@ -365,6 +365,11 @@ class _CommonTests: + def test_empty_yaml_lp1795343(self): + with open(self.config, 'w') as f: + f.write('''''') + self.generate_and_settle() + @unittest.skip("Unsupported matching by driver / wifi matching makes this untestable for now") def test_mapping_for_driver(self): self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) @@ -960,7 +965,7 @@ interfaces: [ethbn, ethb2] parameters: mode: balance-rr - mii-monitor-interval: 5 + mii-monitor-interval: 50s resend-igmp: 100 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle() @@ -1103,7 +1108,7 @@ 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) + self.assertIn('fakesuffix', out) else: sys.stdout.write('[/etc/resolv.conf] ') sys.stdout.flush() @@ -1382,6 +1387,30 @@ class TestNetworkd(NetworkTestBase, _CommonTests): backend = 'networkd' + def test_link_route_v4(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + scope: link + metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet 192.168.5.[0-9]+/24']) # from DHCP + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'10.10.10.0/24 proto static scope link', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 99', # check metric from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + def test_eth_dhcp6_off(self): self.setup_eth('slaac') with open(self.config, 'w') as f: @@ -1644,6 +1673,54 @@ with open('/sys/class/net/mybond/bonding/arp_validate') as f: self.assertEqual(f.read().strip(), 'all 3') + def test_bridge_anonymous(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr]''' % {'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, + ['inet 192.168.5.[0-9]+/24'], + ['master']) + self.assert_iface_up(self.dev_e2_client, + ['master mybr'], + ['inet ']) + self.assert_iface_up('mybr', + [], + ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + + def test_bridge_isolated(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [] + addresses: [10.10.10.10/24]''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + subprocess.check_call(['netplan', 'apply']) + time.sleep(1) + out = subprocess.check_output(['ip', 'a', 'show', 'dev', 'mybr'], + universal_newlines=True) + self.assertIn('inet 10.10.10.10/24', out) + def test_bridge_port_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) diff -Nru netplan.io-0.36.3/tests/test_configmanager.py netplan.io-0.40.1~18.04.2/tests/test_configmanager.py --- netplan.io-0.36.3/tests/test_configmanager.py 2018-07-04 20:05:39.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/tests/test_configmanager.py 2018-10-04 16:04:57.000000000 +0000 @@ -102,7 +102,9 @@ self.configmanager.parse() self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('bond6', self.configmanager.bonds) + self.assertIn('eth0', self.configmanager.physical_interfaces) self.assertNotIn('bond7', self.configmanager.interfaces) + self.assertNotIn('bond6', self.configmanager.physical_interfaces) self.assertNotIn('parameters', self.configmanager.bonds.get('bond5')) self.assertIn('parameters', self.configmanager.bonds.get('bond6')) @@ -190,5 +192,5 @@ @unittest.expectedFailure def test__copy_tree_missing_source(self): - self.configmanager._copy_tree(os.path.join(self.workdir.name, "inexistant"), - os.path.join(self.workdir.name, "inexistant2"), missing_ok=False) + self.configmanager._copy_tree(os.path.join(self.workdir.name, "nonexistent"), + os.path.join(self.workdir.name, "nonexistent2"), missing_ok=False) diff -Nru netplan.io-0.36.3/tests/validate_docs.sh netplan.io-0.40.1~18.04.2/tests/validate_docs.sh --- netplan.io-0.36.3/tests/validate_docs.sh 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/tests/validate_docs.sh 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,47 @@ +#!/bin/bash +# find everything that looks like +# {"driver", YAML_SCALAR_NODE,..., +# extract the thing in quotes. + +# sanity check: make sure none have disappeared, as might happen from a reformat. +count=$(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | uniq | wc -l) +# 71 is based on the 0.36.1 definitions, and should be updated periodically. +if [ $count -lt 71 ]; then + echo "ERROR: fewer YAML keys defined in src/parse.c than expected!" + echo " Has the file been reformatted or refactored? If so, modify" + echo " validate_docs.sh appropriately." + exit 1 +fi + +# iterate through the keys +for term in $(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | uniq); do + # it can be documented in the following ways. + # 1. "Properties for device type ``blah:`` + if egrep "## Properties for device type \`\`$term:\`\`" doc/netplan.md > /dev/null; then + continue + fi + + # 2. "[blah, ]``blah``[, ``blah2``]: (scalar|bool|...) + if egrep "\`\`$term\`\`.*\((scalar|bool|mapping|sequence of scalars)\)" doc/netplan.md > /dev/null; then + continue + fi + + # 3. we give a pass to network and version + if [[ $term = "network" ]] || [[ $term = "version" ]]; then + continue + fi + + # 4. search doesn't get a full description but it's good enough + if [[ $term = "search" ]]; then + continue + fi + + # 5. gratuit_i_ous arp gets a special note + if [[ $term = "gratuitious-arp" ]]; then + continue + fi + + echo ERROR: The key "$term" is defined in the parser but not documented. + exit 1 +done +echo "validate_docs: OK" diff -Nru netplan.io-0.36.3/TODO netplan.io-0.40.1~18.04.2/TODO --- netplan.io-0.36.3/TODO 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.40.1~18.04.2/TODO 2018-10-04 16:04:57.000000000 +0000 @@ -0,0 +1,39 @@ +- support for IPv4 link-local addressing + +- improve IPv6 RA handling + +- support tunnel device types + +- support ethtool/sysctl knobs (TSO, LRO, txqueuelen) + +- inspecting current network config via "netplan show $interface" for a + collated view of each interface's yaml. + +- debugging config generation via "netplan diff [backend|system]": + - netplan diff system: compare generated config with current ip addr output + - netplan diff backend: compare generated config with current config for backend + +- support other devices types from networkd/NetworkManager: + - infiniband + - veth + +- better handle VLAN Q-in-Q (mostly generation tweaks + patching backends) + +- support device aliases (eth0 + eth0.1; add eth0 to multiple bridges) + - workaround for two bridges is to use eth0 and vlan1 + +- make errors translatable + +- "netplan save" to capture kernel state into netplan YAML. + +- better parsing/validation for time-based values (ie. bond, bridge params) + +- openvswitch integration + +- wpa enterprise support + +- better parsing/validation for all schema + +- improve exit codes / behavior on error + +- integrate 'netplan try' in tmux/screen