diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2018-12-29 16:31:02 +0100 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2018-12-29 16:32:39 +0100 |
commit | d218075f5b95421e472b397da901c697903f4405 (patch) | |
tree | e5ac173f0ff0d1737e94ddc65f996b410ceb404a | |
parent | d37bd89307157a5d76f77f2eb89ac5523156794b (diff) | |
parent | 1ea3ec106a8d5f2f6deb3886a1e2dcdbfabfd37c (diff) |
Merge branch 'upstream/latest' into debian/master
49 files changed, 12281 insertions, 7571 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92be020 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +generate +test-coverage +doc/*.html +doc/*.[1-9] +__pycache__ +*.pyc +.coverage +.vscode diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..9712d67 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,56 @@ +# Contributing to netplan.io + +Thanks for taking the time to contribute to netplan! + +Here are the guidelines for contributing to the development of netplan. These +are guidelines, not hard and fast rules; but please exercise judgement. Feel +free to propose changes to this document. + +#### Table Of Contents + +[Code of Conduct](#code-of-conduct) + +[What should I know before I get started](#what-should-i-know-before-i-get-started) + * [Did you find a bug?](#did-you-find-a-bug) + * [Code Quality](#code-quality) + * [Conventions](#conventions) + +## Code of Conduct + +This project and everyone participating in it is governed by the +[Ubuntu Code of Conduct](https://www.ubuntu.com/community/code-of-conduct). +By participating, you are expected to uphold this code. Please report +unacceptable behavior to +[netplan-developers@lists.launchpad.net](mailto:netplan-developers@lists.launchpad.net). + +## What should I know before I get started? + +### Did you find a bug? + +If you've found a bug, please make sure to report it on Launchpad against the +[netplan](http://bugs.launchpad.net/netplan) project. We do use these bug reports +to make it easier to backport features to supported releases of Ubuntu: having +an existing bug report, with people who understand well how the bug appears +helps immensely in making sure the feature or bug fix is well tested when it is +being released to supported Ubuntu releases. + +### Code quality + +We want to maintain the quality of the code in netplan to the highest possible +degree. As such, we do insist on keeping a code coverage with unit tests to 100% +coverage if it is possible. If not, please make sure to explain why when submitting +a pull request, and expect reviewers to challenge you on that decision and suggest +a course of action. + +### Conventions + +The netplan project mixes C and python code. Generator code is generally all +written in C, while the UI / command-line interface is written in python. Please +look at the surrounding code, and make a best effort to follow the general style +used in the code. We do insist on proper indentation (4 spaces), but we will +not block good features and bug fixes on purely style issues. Please exercise +your best judgement: if it looks odd or too clever to you, chances are it will +look odd or too clever to code reviewers. In that case, you may be asked for +some styles changes in a pull request. Similary, if you see code that you +find hard to understand, we do encourage that you submit pull requests that +help make the code easier to understand and maintain. @@ -22,6 +22,7 @@ PYCODE = netplan/ $(wildcard src/*.py) $(wildcard tests/*.py) # Order: Fedora/Mageia/openSUSE || Debian/Ubuntu || null PYFLAKES3 ?= $(shell which pyflakes-3 || which pyflakes3 || echo true) PYCODESTYLE3 ?= $(shell which pycodestyle-3 || which pycodestyle || which pep8 || echo true) +NOSETESTS3 ?= $(shell which nosetests-3 || which nosetests3 || echo true) default: generate doc/netplan.html doc/netplan.5 doc/netplan-generate.8 doc/netplan-apply.8 doc/netplan-try.8 @@ -34,9 +35,8 @@ clean: rm -rf test-coverage .coverage check: default linting - tests/generate.py tests/cli.py - nosetests3 -v --with-coverage + $(NOSETESTS3) -v --with-coverage tests/validate_docs.sh linting: @@ -2,6 +2,7 @@ [![Build Status](https://semaphoreci.com/api/v1/cyphermox/netplan/branches/master/badge.svg)](https://semaphoreci.com/cyphermox/netplan) [![codecov](https://codecov.io/gh/CanonicalLtd/netplan/branch/master/graph/badge.svg)](https://codecov.io/gh/CanonicalLtd/netplan) +[![Snap Status](https://build.snapcraft.io/badge/CanonicalLtd/netplan.svg)](https://build.snapcraft.io/user/CanonicalLtd/netplan) # Website @@ -1,5 +1,3 @@ -- support for IPv4 link-local addressing - - improve IPv6 RA handling - support tunnel device types @@ -37,3 +35,7 @@ - improve exit codes / behavior on error - integrate 'netplan try' in tmux/screen + +- replace nose for python tests with something else, preserving coverage reports + +- add automated integration tests for WPA Enterprise / 802.1x that can run self-contained diff --git a/debian/tests/control b/debian/tests/control index dec67fd..43b2e88 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,11 +1,92 @@ -Tests: integration.py -Tests-Directory: tests +Test-Command: python3 tests/integration/run.py --test=ethernets +Tests-Directory: tests/integration Depends: @, systemd, network-manager, hostapd, dnsmasq-base, Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=ethernets + +Test-Command: python3 tests/integration/run.py --test=bridges +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=bridges + +Test-Command: python3 tests/integration/run.py --test=bonds +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=bonds + +Test-Command: python3 tests/integration/run.py --test=routing +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=routing + +Test-Command: python3 tests/integration/run.py --test=vlans +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=vlans + +Test-Command: python3 tests/integration/run.py --test=wifi +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=wifi + +Test-Command: python3 tests/integration/run.py --test=tunnels +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=tunnels + +Test-Command: python3 tests/integration/run.py --test=scenarios +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=scenarios + +Test-Command: python3 tests/integration/run.py --test=regressions +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + dnsmasq-base, +Restrictions: allow-stderr, needs-root, isolation-machine +Features: test-name=regressions Tests: autostart Restrictions: allow-stderr, needs-root, isolation-container diff --git a/doc/netplan.md b/doc/netplan.md index 8c72c14..fab4f53 100644 --- a/doc/netplan.md +++ b/doc/netplan.md @@ -159,6 +159,12 @@ Virtual devices Note that **``rdnssd``**(8) is required to use RDNSS with networkd. No extra software is required for NetworkManager. +``ipv6-privacy`` (bool) + +: Enable IPv6 Privacy Extensions (RFC 4941) for the specified interface, and + prefer temporary addresses. Defaults to false - no privacy extensions. There + is currently no way to have a private address but prefer the public address. + ``link-local`` (sequence of scalars) : Configure the link-local addresses to bring up. Valid options are 'ipv4' @@ -190,6 +196,16 @@ Virtual devices device's MAC address as a unique identifier rather than a RFC4361-compliant Client ID. This has no effect when NetworkManager is used as a renderer. + ``dhcp4-overrides`` (mapping) + + : (networkd backend only) Overrides default DHCP behavior; see the + ``DHCP Overrides`` section below. + + ``dhcp6-overrides`` (mapping) + + : (networkd backend only) Overrides default DHCP behavior; see the + ``DHCP Overrides`` section below. + ``accept-ra`` (bool) : Accept Router Advertisement that would have the kernel configure IPv6 by itself. @@ -236,12 +252,18 @@ similar to ``gateway*``, and ``search:`` is a list of search domains. ``macaddress`` (scalar) : Set the device's MAC address. The MAC address must be in the form -"XX:XX:XX:XX:XX:XX". + "XX:XX:XX:XX:XX:XX". + + **Note:** This will not work reliably for devices matched by name + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting MAC addresses. Example: ethernets: id0: + match: + macaddress: 52:54:00:6b:3c:58 [...] macaddress: 52:54:00:6b:3c:59 @@ -251,8 +273,8 @@ similar to ``gateway*``, and ``search:`` is a list of search domains. 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. + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting MTU. ``optional`` (bool) @@ -294,6 +316,74 @@ similar to ``gateway*``, and ``search:`` is a list of search domains. : Configure policy routing for the device; see the ``Routing`` section below. +## DHCP Overrides +Several DHCP behavior overrides are available. Most currently only have any +effect when using the ``networkd`` backend, with the exception of ``use-routes`` +and ``route-metric``. + +Overrides only have an effect if the corresponding ``dhcp4`` or ``dhcp6`` is +set to ``true``. + +If both ``dhcp4`` and ``dhcp6`` are ``true``, the ``networkd`` backend requires +that ``dhcp4-overrides`` and ``dhcp6-overrides`` contain the same keys and +values. If the values do not match, an error will be shown and the network +configuration will not be applied. + +When using the NetworkManager backend, different values may be specified for +``dhcp4-overrides`` and ``dhcp6-overrides``, and will be applied to the DHCP +client processes as specified in the netplan YAML. + +: The ``dhcp4-overrides`` and ``dhcp6-overrides`` mappings override the + default DHCP behavior. + + ``use-dns`` (bool) + : Default: ``true``. When ``true``, the DNS servers received from the + DHCP server will be used and take precedence over any statically + configured ones. Currently only has an effect on the ``networkd`` + backend. + + ``use-ntp`` (bool) + : Default: ``true``. When ``true``, the NTP servers received from the + DHCP server will be used by systemd-timesyncd and take precedence + over any statically configured ones. Currently only has an effect on + the ``networkd`` backend. + + ``send-hostname`` (bool) + : Default: ``true``. When ``true``, the machine's hostname will be sent + to the DHCP server. Currently only has an effect on the ``networkd`` + backend. + + ``use-hostname`` (bool) + : Default: ``true``. When ``true``, the hostname received from the DHCP + server will be set as the transient hostname of the system. Currently + only has an effect on the ``networkd`` backend. + + ``use-mtu`` (bool) + : Default: ``true``. When ``true``, the MTU received from the DHCP + server will be set as the MTU of the network interface. When ``false``, + the MTU advertised by the DHCP server will be ignored. Currently only + has an effect on the ``networkd`` backend. + + ``hostname`` (scalar) + : Use this value for the hostname which is sent to the DHCP server, + instead of machine's hostname. Currently only has an effect on the + ``networkd`` backend. + + ``use-routes`` (bool) + : Default: ``true``. When ``true``, the routes received from the DHCP + server will be installed in the routing table normally. When set to + ``false``, routes from the DHCP server will be ignored: in this case, + the user is responsible for adding static routes if necessary for + correct network operation. This allows users to avoid installing a + default gateway for interfaces configured via DHCP. Available for + both the ``networkd`` and ``NetworkManager`` backends. + + ``route-metric`` (scalar) + : Use this value for default metric for automatically-added routes. + Use this to prioritize routes for devices by setting a higher metric + on a preferred interface. Available for both the ``networkd`` and + ``NetworkManager`` backends. + ## Routing Complex routing is possible with netplan. Standard static routes as well @@ -379,6 +469,57 @@ These options are available for all types of interfaces. : Match this policy rule based on the type of service number applied to the traffic. +## Authentication +Netplan supports advanced authentication settings for ethernet and wifi +interfaces, as well as individual wifi networks, by means of the ``auth`` block. + +``auth`` (mapping) + +: Specifies authentication settings for a device of type ``ethernets:``, or + an ``access-points:`` entry on a ``wifis:`` device. + + The ``auth`` block supports the following properties: + + ``key-management`` (scalar) + : The supported key management modes are ``none`` (no key management); + ``psk`` (WPA with pre-shared key, common for home wifi); ``eap`` (WPA + with EAP, common for enterprise wifi); and ``802.1x`` (used primarily + for wired Ethernet connections). + + ``password`` (scalar) + : The password string for EAP, or the pre-shared key for WPA-PSK. + + The following properties can be used if ``key-management`` is ``eap`` + or ``802.1x``: + + ``method`` (scalar) + : The EAP method to use. The supported EAP methods are ``tls`` (TLS), + ``peap`` (Protected EAP), and ``ttls`` (Tunneled TLS). + + ``identity`` (scalar) + : The identity to use for EAP. + + ``anonymous-identity`` (scalar) + : The identity to pass over the unencrypted channel if the chosen EAP + method supports passing a different tunnelled identity. + + ``ca-certificate`` (scalar) + : Path to a file with one or more trusted certificate authority (CA) + certificates. + + ``client-certificate`` (scalar) + : Path to a file containing the certificate to be used by the client + during authentication. + + ``client-key`` (scalar) + : Path to a file containing the private key corresponding to + ``client-certificate``. + + ``client-key-password`` (scalar) + : Password to use to decrypt the private key specified in + ``client-key`` if it is encrypted. + + ## Properties for device type ``ethernets:`` Ethernet device definitions do not support any specific properties beyond the common ones described above. @@ -395,9 +536,17 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. supported properties: ``password`` (scalar) - : Enable WPA2 authentication and set the passphrase for it. If not - given, the network is assumed to be open. Other authentication modes - are not currently supported. + : Enable WPA2 authentication and set the passphrase for it. If neither + this nor an ``auth`` block are given, the network is assumed to be + open. The setting + + password: "S3kr1t" + + is equivalent to + + auth: + key-management: psk + password: "S3kr1t" ``mode`` (scalar) : Possible access point modes are ``infrastructure`` (the default), @@ -425,15 +574,17 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. ``parameters`` (mapping) -: Customization parameters for special bridging options. Unless otherwise - specified, parameter values for time intervals should be expressed in - milliseconds, but can also be expressed in seconds using a time suffix - (such as "s" for seconds, "ms" for milliseconds). +: Customization parameters for special bridging options. Time intervals + may need to be expressed as a number of seconds or milliseconds: the + default value type is specified below. If necessary, time intervals can + be qualified using a time suffix (such as "s" for seconds, "ms" for + milliseconds) to allow for more control over its behavior. ``ageing-time`` (scalar) - : Set the period of time (in seconds) to keep a MAC address in the forwarding + : Set the period of time to keep a MAC address in the forwarding database after a packet is received. This maps to the AgeingTimeSec= - property when the networkd renderer is used. + property when the networkd renderer is used. If no time suffix is + specified, the value will be interpreted as seconds. ``priority`` (scalar) : Set the priority value for the bridge. This value should be a @@ -447,21 +598,25 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. designated port and root port selection algorithms. ``forward-delay`` (scalar) - : Specify the period of time (in seconds) the bridge will remain in Listening and + : Specify the period of time the bridge will remain in Listening and Learning states before getting to the Forwarding state. This field maps to the ForwardDelaySec= property for the networkd renderer. + If no time suffix is specified, the value will be interpreted as + seconds. ``hello-time`` (scalar) - : Specify the interval (in seconds) between two hello packets being sent out from + : Specify the interval between two hello packets being sent out from the root and designated bridges. Hello packets communicate information about the network topology. When the networkd renderer - is used, this maps to the HelloTimeSec= property. + is used, this maps to the HelloTimeSec= property. If no time suffix + is specified, the value will be interpreted as seconds. ``max-age`` (scalar) - : Set the maximum age (in seconds) of a hello packet. If the last hello packet is + : Set the maximum age of a hello packet. If the last hello packet is older than that value, the bridge will attempt to become the root bridge. This maps to the MaxAgeSec= property when the networkd - renderer is used. + renderer is used. If no time suffix is specified, the value will be + interpreted as seconds. ``path-cost`` (scalar) : Set the cost of a path on the bridge. Faster interfaces should have @@ -492,10 +647,11 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. ``parameters`` (mapping) -: Customization parameters for special bonding options. Unless otherwise - specified, parameter values for time intervals should be expressed in - milliseconds, but can also be expressed in seconds using a time suffix - (such as "s" for seconds, "ms" for milliseconds). +: Customization parameters for special bonding options. Time intervals + may need to be expressed as a number of seconds or milliseconds: the + default value type is specified below. If necessary, time intervals can + be qualified using a time suffix (such as "s" for seconds, "ms" for + milliseconds) to allow for more control over its behavior. ``mode`` (scalar) : Set the bonding mode used for the interfaces. The default is @@ -512,7 +668,8 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. : Specifies the interval for MII monitoring (verifying if an interface of the bond has carrier). The default is ``0``; which disables MII monitoring. This is equivalent to the MIIMonitorSec= field for the - networkd backend. + networkd backend. If no time suffix is specified, the value will be + interpreted as milliseconds. ``min-links`` (scalar) : The minimum number of links up in a bond to consider the bond @@ -539,6 +696,8 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. : Set the interval value for how frequently ARP link monitoring should happen. The default value is ``0``, which disables ARP monitoring. For the networkd backend, this maps to the ARPIntervalSec= property. + If no time suffix is specified, the value will be interpreted as + milliseconds. ``arp-ip-targets`` (sequence of scalars) : IPs of other hosts on the link which should be sent ARP requests in @@ -562,12 +721,14 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. ``up-delay`` (scalar) : Specify the delay before enabling a link once the link is physically up. The default value is ``0``. This maps to the UpDelaySec= property - for the networkd renderer. + for the networkd renderer. If no time suffix is specified, the value + will be interpreted as milliseconds. ``down-delay`` (scalar) : Specify the delay before disabling a link once the link has been lost. The default value is ``0``. This maps to the DownDelaySec= - property for the networkd renderer. + property for the networkd renderer. If no time suffix is specified, + the value will be interpreted as milliseconds. ``fail-over-mac-policy`` (scalar) : Set whether to set all slaves to the same MAC address when adding @@ -609,11 +770,12 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. are sent at 200ms intervals. ``learn-packet-interval`` (scalar) - : Specify the interval (seconds) between sending learning packets to + : Specify the interval between sending learning packets to each slave. The value range is between ``1`` and ``0x7fffffff``. The default value is ``1``. This option only affects ``balance-tlb`` and ``balance-alb`` modes. Using the networkd renderer, this field - maps to the LearnPacketIntervalSec= property. + maps to the LearnPacketIntervalSec= property. If no time suffix is + specified, the value will be interpreted as seconds. ``primary`` (scalar) : Specify a device to be used as a primary slave, or preferred device @@ -622,6 +784,71 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi. ``active-backup``, ``balance-alb``, and ``balance-tlb`` modes. +## Properties for device type ``tunnels:`` + +Tunnels allow traffic to pass as if it was between systems on the same local +network, although systems may be far from each other but reachable via the +Internet. They may be used to support IPv6 traffic on a network where the ISP +does not provide the service, or to extend and "connect" separate local +networks. Please see https://en.wikipedia.org/wiki/Tunneling_protocol for +more general information about tunnels. + +``mode`` (scalar) + +: Defines the tunnel mode. Valid options are ``sit``, ``gre``, ``ip6gre``, + ``ipip``, ``ipip6``, ``ip6ip6``, ``vti``, and ``vti6``. Additionally, + the ``networkd`` backend also supports ``gretap`` and ``ip6gretap`` modes. + In addition, the ``NetworkManager`` backend supports ``isatap`` tunnels. + +``local`` (scalar) + +: Defines the address of the local endpoint of the tunnel. + +``remote`` (scalar) + +: Defines the address of the remote endpoint of the tunnel. + +``key`` (scalar or mapping) + +: Define keys to use for the tunnel. The key can be a number or a dotted + quad (an IPv4 address). It is used for identification of IP transforms. + This is only required for ``vti`` and ``vti6`` when using the networkd + backend, and for ``gre`` or ``ip6gre`` tunnels when using the + NetworkManager backend. + + This field may be used as a scalar (meaning that a single key is + specified and to be used for both input and output key), or as a mapping, + where you can then further specify ``input`` and ``output``. + + ``input`` (scalar) + : The input key for the tunnel + + ``output`` (scalar) + : The output key for the tunnel + +Examples: + + tunnels: + tun0: + mode: gre + local: ... + remote: ... + keys: + input: 1234 + output: 5678 + + tunnels: + tun0: + mode: vti6 + local: ... + remote: ... + key: 59568549 + +``keys`` (scalar or mapping) + +: Alternate name for the ``key`` field. See above. + + ## Properties for device type ``vlans:`` ``id`` (scalar) @@ -657,9 +884,9 @@ DHCP: 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 +and multiple gateways with networkd, with equal route metric levels, and static DNS nameservers (Google DNS for this example): network: @@ -680,7 +907,7 @@ DNS nameservers (Google DNS for this example): metric: 100 - to: 0.0.0.0/0 via: 11.0.0.1 - metric: 100 + metric: 100 This is a complex example which shows most available features: @@ -720,6 +947,8 @@ This is a complex example which shows most available features: from: 192.168.14.3/24 table: 70 priority: 50 + # only networkd can render on-link routes and routing policies + renderer: networkd lom: match: driver: ixgbe diff --git a/examples/bridge.yaml b/examples/bridge.yaml index 89b9f00..dbfcae5 100644 --- a/examples/bridge.yaml +++ b/examples/bridge.yaml @@ -1,6 +1,9 @@ network: version: 2 renderer: networkd + ethernets: + enp3s0: + dhcp4: no bridges: br0: dhcp4: yes diff --git a/examples/ipv6_tunnel.yaml b/examples/ipv6_tunnel.yaml new file mode 100644 index 0000000..86bceec --- /dev/null +++ b/examples/ipv6_tunnel.yaml @@ -0,0 +1,16 @@ +network: + version: 2 + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" + gateway4: 1.1.1.254 + tunnels: + he-ipv6: + mode: sit + remote: 2.2.2.2 + local: 1.1.1.1 + addresses: + - "2001:dead:beef::2/64" + gateway6: "2001:dead:beef::1" diff --git a/examples/route_metric.yaml b/examples/route_metric.yaml new file mode 100644 index 0000000..20bf48c --- /dev/null +++ b/examples/route_metric.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + ethernets: + enred: + dhcp4: yes + dhcp4-overrides: + route-metric: 100 + engreen: + dhcp4: yes + dhcp4-overrides: + route-metric: 200 diff --git a/examples/wpa_enterprise.yaml b/examples/wpa_enterprise.yaml new file mode 100644 index 0000000..0602e21 --- /dev/null +++ b/examples/wpa_enterprise.yaml @@ -0,0 +1,26 @@ +network: + version: 2 + wifis: + wl0: + access-points: + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + university: + auth: + key-management: eap + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + open-network: + auth: + key-management: none + dhcp4: yes diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index 6038514..42fd0c6 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -111,6 +111,21 @@ class NetplanApply(utils.NetplanCommand): utils.systemctl_network_manager('start', sync=sync) @staticmethod + def is_composite_member(composites, phy): # pragma: nocover (covered in autopkgtest) + """ + Is this physical interface a member of a 'composite' virtual + interface? (bond, bridge) + """ + for composite in composites: + for id, settings in composite.items(): + members = settings.get('interfaces', []) + for iface in members: + if iface == phy: + return True + + return False + + @staticmethod def process_link_changes(interfaces, config_manager): # pragma: nocover (covered in autopkgtest) """ Go through the pending changes and pick what needs special @@ -120,6 +135,7 @@ class NetplanApply(utils.NetplanCommand): changes = {} phys = dict(config_manager.physical_interfaces) + composite_interfaces = [config_manager.bridges, config_manager.bonds] # TODO (cyphermox): factor out some of this matching code (and make it # pretty) in its own module. @@ -140,13 +156,22 @@ class NetplanApply(utils.NetplanCommand): driver = match.get('driver') mac = match.get('macaddress') if driver: - matches['by-driver'][driver] = phy + matches['by-driver'][driver] = newname if mac: - matches['by-mac'][mac] = phy + matches['by-mac'][mac] = newname # /sys/class/net/ens3/device -> ../../../virtio0 # /sys/class/net/ens3/device/driver -> ../../../../bus/virtio/drivers/virtio_net for interface in interfaces: + if interface not in phys: + # do not rename virtual devices + logging.debug('Skipping non-physical interface: %s', interface) + continue + if NetplanApply.is_composite_member(composite_interfaces, interface): + logging.debug('Skipping composite member %s', interface) + # do not rename members of virtual devices. MAC addresses + # may be the same for all interface members. + continue # try to get the device's driver for matching. devdir = os.path.join('/sys/class/net', interface) try: diff --git a/netplan/cli/commands/ip.py b/netplan/cli/commands/ip.py index db5a4b3..b7a7f29 100644 --- a/netplan/cli/commands/ip.py +++ b/netplan/cli/commands/ip.py @@ -142,7 +142,7 @@ class NetplanIpLeases(utils.NetplanCommand): logging.debug('command ip leases: running %s', argv) try: out = subprocess.check_output(argv, universal_newlines=True) - except CalledProcessError as e: # pragma: nocover (better be covered in autopkgtest) + except CalledProcessError: # pragma: nocover (better be covered in autopkgtest) sys.exit(1) mapping = {} mapping_s = out.split(',') diff --git a/rpm/netplan.spec b/rpm/netplan.spec index 3dbf7a2..5cbafa5 100644 --- a/rpm/netplan.spec +++ b/rpm/netplan.spec @@ -15,7 +15,7 @@ Name: netplan -Version: 0.34.1 +Version: 0.95 Release: 0%{?dist} Summary: Network configuration tool using YAML Group: System Environment/Base @@ -33,13 +33,19 @@ BuildRequires: pkgconfig(uuid) BuildRequires: %{_bindir}/pandoc BuildRequires: python%{python3_pkgversion}-devel # For tests +BuildRequires: %{_sbindir}/ip BuildRequires: python%{python3_pkgversion}-coverage -BuildRequires: python%{python3_pkgversion}-PyYAML +BuildRequires: python%{python3_pkgversion}-netifaces +BuildRequires: python%{python3_pkgversion}-nose BuildRequires: python%{python3_pkgversion}-pycodestyle BuildRequires: python%{python3_pkgversion}-pyflakes +BuildRequires: python%{python3_pkgversion}-PyYAML -# /usr/sbin/netplan is a Python 3 script that requires PyYAML +# /usr/sbin/netplan is a Python 3 script that requires netifaces and PyYAML +Requires: python%{python3_pkgversion}-netifaces Requires: python%{python3_pkgversion}-PyYAML +# 'ip' command is used in netplan apply subcommand +Requires: %{_sbindir}/ip # netplan supports either systemd or NetworkManager as backends to configure the network Requires: systemd @@ -59,7 +65,7 @@ Provides: %{ubuntu_name} = %{version}-%{release} Provides: %{ubuntu_name}%{?_isa} = %{version}-%{release} %description -netplan reads network configuration from /etc/nplan/*.yaml which are written by administrators, +netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. @@ -99,12 +105,19 @@ make check %{_unitdir}/%{name}*.service %{_systemdgeneratordir}/%{name} %{_mandir}/man5/%{name}.5* +%{_mandir}/man8/%{name}*.8* %dir %{_sysconfdir}/%{name} %{_prefix}/lib/%{name}/ %{_datadir}/bash-completion/completions/%{name} %changelog +* Fri Dec 13 2018 Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> - 0.95 +- Update to 0.95 + +* Sat Oct 13 2018 Neal Gompa <ngompa13@gmail.com> - 0.40.3-0 +- Rebase to 0.40.3 + * Tue Mar 13 2018 Neal Gompa <ngompa13@gmail.com> - 0.34-0.1 - Update to 0.34 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..4b2197f --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,42 @@ +name: netplan +version: git +summary: Backend-agnostic network configuration in YAML +description: | + Netplan is a utility for easily configuring networking on a linux system. + You simply create a YAML description of the required network interfaces and + what each should be configured to do. From this description Netplan will + generate all the necessary configuration for your chosen renderer tool. +grade: devel +confinement: classic + +apps: + netplan: + command: usr/sbin/netplan + environment: + PYTHONPATH: $PYTHONPATH:$SNAP/usr/lib/python3/dist-packages + +parts: + netplan: + source: https://github.com/CanonicalLtd/netplan.git + plugin: make + build-packages: + - bash-completion + - libglib2.0-dev + - libyaml-dev + - uuid-dev + - pandoc + - pkg-config + - python3 + - python3-coverage + - python3-yaml + - python3-netifaces + - python3-nose + - pyflakes3 + - pep8 + - systemd + stage-packages: + - iproute2 + - python3 + - python3-netifaces + - python3-yaml + - systemd diff --git a/src/generate.c b/src/generate.c index 0307352..0f86f25 100644 --- a/src/generate.c +++ b/src/generate.c @@ -51,7 +51,7 @@ reload_udevd(void) }; static void -nd_iterator(gpointer key, gpointer value, gpointer user_data) +nd_iterator_list(gpointer value, gpointer user_data) { if (write_networkd_conf((net_definition*) value, (const char*) user_data)) any_networkd = TRUE; @@ -250,15 +250,15 @@ int main(int argc, char** argv) /* Generate backend specific configuration files from merged data. */ if (netdefs) { g_debug("Generating output files.."); - g_hash_table_foreach(netdefs, nd_iterator, rootdir); + g_list_foreach (netdefs_ordered, nd_iterator_list, rootdir); write_nm_conf_finish(rootdir); - /* We may have written .rules & .link files, thus we must - * invalidate udevd cache of its config as by default it only - * invalidates cache at most every 3 seconds. Not sure if this - * should live in `generate' or `apply', but it is confusing - * when udevd ignores just-in-time created rules files. - */ - reload_udevd(); + /* We may have written .rules & .link files, thus we must + * invalidate udevd cache of its config as by default it only + * invalidates cache at most every 3 seconds. Not sure if this + * should live in `generate' or `apply', but it is confusing + * when udevd ignores just-in-time created rules files. + */ + reload_udevd(); } /* Disable /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf diff --git a/src/networkd.c b/src/networkd.c index 328dccf..0c447b0 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -54,6 +54,24 @@ append_match_section(net_definition* def, GString* s, gboolean match_rename) else if (def->match.original_name) g_string_append_printf(s, "Name=%s\n", def->match.original_name); } + + /* Workaround for bug LP: #1804861: something outputs netplan config + * that includes using the MAC of the first phy member of a bond as + * default value for the MAC of the bond device itself. This is + * evil, it's an optional field and networkd knows what to do if + * the MAC isn't specified; but work around this by adding an + * arbitrary additional match condition on Path= for the phys. + * This way, hopefully setting a MTU on the phy does not bleed over + * to bond/bridge and any further virtual devices (VLANs?) on top of + * it. + * Make sure to add the extra match only if we're matching by MAC + * already and dealing with a bond or bridge. + */ + if (def->bond || def->bridge) { + /* update if we support new device types */ + if (def->match.mac) + g_string_append(s, "Type=!vlan bond bridge\n"); + } } static void @@ -83,6 +101,27 @@ write_bridge_params(GString* s, net_definition* def) } static void +write_tunnel_params(GString* s, net_definition* def) +{ + GString *params = NULL; + + params = g_string_sized_new(200); + + g_string_printf(params, "Independent=true\n"); + if (def->tunnel.mode == TUNNEL_MODE_IPIP6 || def->tunnel.mode == TUNNEL_MODE_IP6IP6) + g_string_append_printf(params, "Mode=%s\n", tunnel_mode_to_string(def->tunnel.mode)); + g_string_append_printf(params, "Local=%s\n", def->tunnel.local_ip); + g_string_append_printf(params, "Remote=%s\n", def->tunnel.remote_ip); + if (def->tunnel.input_key) + g_string_append_printf(params, "InputKey=%s\n", def->tunnel.input_key); + if (def->tunnel.output_key) + g_string_append_printf(params, "OutputKey=%s\n", def->tunnel.output_key); + + g_string_append_printf(s, "\n[Tunnel]\n%s", params->str); + g_string_free(params, TRUE); +} + +static void write_link_file(net_definition* def, const char* rootdir, const char* path) { GString* s = NULL; @@ -240,6 +279,35 @@ write_netdev_file(net_definition* def, const char* rootdir, const char* path) g_string_append_printf(s, "Kind=vlan\n\n[VLAN]\nId=%u\n", def->vlan_id); break; + case ND_TUNNEL: + switch(def->tunnel.mode) { + case TUNNEL_MODE_GRE: + case TUNNEL_MODE_GRETAP: + case TUNNEL_MODE_IPIP: + case TUNNEL_MODE_IP6GRE: + case TUNNEL_MODE_IP6GRETAP: + case TUNNEL_MODE_SIT: + case TUNNEL_MODE_VTI: + case TUNNEL_MODE_VTI6: + g_string_append_printf(s, + "Kind=%s\n", + tunnel_mode_to_string(def->tunnel.mode)); + break; + + case TUNNEL_MODE_IP6IP6: + case TUNNEL_MODE_IPIP6: + g_string_append(s, "Kind=ip6tnl\n"); + break; + + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } + + write_tunnel_params(s, def); + break; + // LCOV_EXCL_START default: g_assert_not_reached(); @@ -297,6 +365,55 @@ write_ip_rule(ip_rule* r, GString* s) g_string_append_printf(s, "TypeOfService=%d\n", r->tos); } +#define DHCP_OVERRIDES_ERROR \ + "ERROR: %s: networkd requires that %s has the same value in both " \ + "dhcp4_overrides and dhcp6_overrides\n" + +static void +combine_dhcp_overrides(net_definition* def, dhcp_overrides* combined_dhcp_overrides) +{ + /* if only one of dhcp4 or dhcp6 is enabled, those overrides are used */ + if (def->dhcp4 && !def->dhcp6) { + *combined_dhcp_overrides = def->dhcp4_overrides; + } else if (!def->dhcp4 && def->dhcp6) { + *combined_dhcp_overrides = def->dhcp6_overrides; + } else { + /* networkd doesn't support separately configuring dhcp4 and dhcp6, so + * we enforce that they are the same. + */ + if (def->dhcp4_overrides.use_dns != def->dhcp6_overrides.use_dns) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-dns"); + exit(1); + } + if (def->dhcp4_overrides.use_ntp != def->dhcp6_overrides.use_ntp) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-ntp"); + exit(1); + } + if (def->dhcp4_overrides.send_hostname != def->dhcp6_overrides.send_hostname) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "send-hostname"); + exit(1); + } + if (def->dhcp4_overrides.use_hostname != def->dhcp6_overrides.use_hostname) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-hostname"); + exit(1); + } + if (def->dhcp4_overrides.use_mtu != def->dhcp6_overrides.use_mtu) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-mtu"); + exit(1); + } + if (g_strcmp0(def->dhcp4_overrides.hostname, def->dhcp6_overrides.hostname) != 0) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "hostname"); + exit(1); + } + if (def->dhcp4_overrides.metric != def->dhcp6_overrides.metric) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "route-metric"); + exit(1); + } + /* Just use dhcp4_overrides now, since we know they are the same. */ + *combined_dhcp_overrides = def->dhcp4_overrides; + } +} + static void write_network_file(net_definition* def, const char* rootdir, const char* path) { @@ -316,14 +433,14 @@ write_network_file(net_definition* def, const char* rootdir, const char* path) 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); - } - } + 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"); @@ -358,6 +475,8 @@ write_network_file(net_definition* def, const char* rootdir, const char* path) g_string_append_printf(s, "IPv6AcceptRA=yes\n"); else if (def->accept_ra == ACCEPT_RA_DISABLED) g_string_append_printf(s, "IPv6AcceptRA=no\n"); + if (def->ip6_privacy) + g_string_append(s, "IPv6PrivacyExtensions=yes\n"); if (def->gateway4) g_string_append_printf(s, "Gateway=%s\n", def->gateway4); if (def->gateway6) @@ -419,15 +538,45 @@ write_network_file(net_definition* def, const char* rootdir, const char* path) } if (def->dhcp4 || def->dhcp6) { - /* isc-dhcp dhclient compatible UseMTU, networkd default is to - * not accept MTU, which breaks clouds */ - g_string_append_printf(s, "\n[DHCP]\nUseMTU=true\n"); /* NetworkManager compatible route metrics */ - g_string_append_printf(s, "RouteMetric=%i\n", (def->type == ND_WIFI ? 600 : 100)); + g_string_append(s, "\n[DHCP]\n"); if (g_strcmp0(def->dhcp_identifier, "duid") != 0) g_string_append_printf(s, "ClientIdentifier=%s\n", def->dhcp_identifier); if (def->critical) g_string_append_printf(s, "CriticalConnection=true\n"); + + dhcp_overrides combined_dhcp_overrides; + combine_dhcp_overrides(def, &combined_dhcp_overrides); + + if (combined_dhcp_overrides.metric == METRIC_UNSPEC) { + g_string_append_printf(s, "RouteMetric=%i\n", (def->type == ND_WIFI ? 600 : 100)); + } else { + g_string_append_printf(s, "RouteMetric=%u\n", + combined_dhcp_overrides.metric); + } + + /* Only set MTU from DHCP if use-mtu dhcp-override is not false. */ + if (!combined_dhcp_overrides.use_mtu) { + /* isc-dhcp dhclient compatible UseMTU, networkd default is to + * not accept MTU, which breaks clouds */ + g_string_append_printf(s, "UseMTU=false\n"); + } else { + g_string_append_printf(s, "UseMTU=true\n"); + } + + /* Only write DHCP options that differ from the networkd default. */ + if (!combined_dhcp_overrides.use_routes) + g_string_append_printf(s, "UseRoutes=false\n"); + if (!combined_dhcp_overrides.use_dns) + g_string_append_printf(s, "UseDNS=false\n"); + if (!combined_dhcp_overrides.use_ntp) + g_string_append_printf(s, "UseNTP=false\n"); + if (!combined_dhcp_overrides.send_hostname) + g_string_append_printf(s, "SendHostname=false\n"); + if (!combined_dhcp_overrides.use_hostname) + g_string_append_printf(s, "UseHostname=false\n"); + if (combined_dhcp_overrides.hostname) + g_string_append_printf(s, "Hostname=%s\n", combined_dhcp_overrides.hostname); } /* these do not contain secrets and need to be readable by @@ -482,33 +631,110 @@ write_rules_file(net_definition* def, const char* rootdir) } static void +append_wpa_auth_conf(GString* s, const authentication_settings* auth) +{ + switch (auth->key_management) { + case KEY_MANAGEMENT_NONE: + g_string_append(s, " key_mgmt=NONE\n"); + break; + + case KEY_MANAGEMENT_WPA_PSK: + g_string_append(s, " key_mgmt=WPA-PSK\n"); + break; + + case KEY_MANAGEMENT_WPA_EAP: + g_string_append(s, " key_mgmt=WPA-EAP\n"); + break; + + case KEY_MANAGEMENT_8021X: + g_string_append(s, " key_mgmt=IEEE8021X\n"); + break; + } + + switch (auth->eap_method) { + case EAP_NONE: + break; + + case EAP_TLS: + g_string_append(s, " eap=TLS\n"); + break; + + case EAP_PEAP: + g_string_append(s, " eap=PEAP\n"); + break; + + case EAP_TTLS: + g_string_append(s, " eap=TTLS\n"); + break; + } + + if (auth->identity) { + g_string_append_printf(s, " identity=\"%s\"\n", auth->identity); + } + if (auth->anonymous_identity) { + g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity); + } + if (auth->password) { + if (auth->key_management == KEY_MANAGEMENT_WPA_PSK) { + g_string_append_printf(s, " psk=\"%s\"\n", auth->password); + } else { + g_string_append_printf(s, " password=\"%s\"\n", auth->password); + } + } + if (auth->ca_certificate) { + g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate); + } + if (auth->client_certificate) { + g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate); + } + if (auth->client_key) { + g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key); + } + if (auth->client_key_password) { + g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password); + } +} + +static void write_wpa_conf(net_definition* def, const char* rootdir) { GHashTableIter iter; - wifi_access_point* ap; GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n"); g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL); mode_t orig_umask; g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path); - g_hash_table_iter_init(&iter, def->access_points); - while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) { - g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid); - if (ap->password) - g_string_append_printf(s, " psk=\"%s\"\n", ap->password); - else - g_string_append(s, " key_mgmt=NONE\n"); - switch (ap->mode) { - case WIFI_MODE_INFRASTRUCTURE: - /* default in wpasupplicant */ - break; - case WIFI_MODE_ADHOC: - g_string_append(s, " mode=1\n"); - break; - case WIFI_MODE_AP: - g_fprintf(stderr, "ERROR: %s: networkd does not support wifi in access point mode\n", def->id); - exit(1); + if (def->type == ND_WIFI) { + wifi_access_point* ap; + g_hash_table_iter_init(&iter, def->access_points); + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) { + g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid); + switch (ap->mode) { + case WIFI_MODE_INFRASTRUCTURE: + /* default in wpasupplicant */ + break; + case WIFI_MODE_ADHOC: + g_string_append(s, " mode=1\n"); + break; + case WIFI_MODE_AP: + g_fprintf(stderr, "ERROR: %s: networkd does not support wifi in access point mode\n", def->id); + exit(1); + } + + /* wifi auth trumps netdef auth */ + if (ap->has_auth) { + append_wpa_auth_conf(s, &ap->auth); + } + else { + g_string_append(s, " key_mgmt=NONE\n"); + } + g_string_append(s, "}\n"); } + } + else { + /* wired 802.1x auth or similar */ + g_string_append(s, "network={\n"); + append_wpa_auth_conf(s, &def->auth); g_string_append(s, "}\n"); } @@ -540,9 +766,9 @@ write_networkd_conf(net_definition* def, const char* rootdir) return FALSE; } - if (def->type == ND_WIFI) { + if (def->type == ND_WIFI || def->has_auth) { g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/multi-user.target.wants/netplan-wpa@", def->id, ".service", NULL); - if (def->has_match) { + if (def->type == ND_WIFI && def->has_match) { g_fprintf(stderr, "ERROR: %s: networkd backend does not support wifi with match:, only by interface name\n", def->id); exit(1); } @@ -86,6 +86,8 @@ type_str(netdef_type type) return "bond"; case ND_VLAN: return "vlan"; + case ND_TUNNEL: + return "ip-tunnel"; // LCOV_EXCL_START default: g_assert_not_reached(); @@ -213,7 +215,7 @@ write_bond_parameters(const net_definition* def, GString *s) 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 */ + * 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) @@ -260,6 +262,94 @@ write_bridge_params(const net_definition* def, GString *s) } static void +write_tunnel_params(const net_definition* def, GString *s) +{ + g_string_append(s, "\n[ip-tunnel]\n"); + + g_string_append_printf(s, "mode=%d\n", def->tunnel.mode); + g_string_append_printf(s, "local=%s\n", def->tunnel.local_ip); + g_string_append_printf(s, "remote=%s\n", def->tunnel.remote_ip); + + if (def->tunnel.input_key) + g_string_append_printf(s, "input-key=%s\n", def->tunnel.input_key); + if (def->tunnel.output_key) + g_string_append_printf(s, "output-key=%s\n", def->tunnel.output_key); +} + +static void +write_dot1x_auth_parameters(const authentication_settings* auth, GString *s) +{ + if (auth->eap_method == EAP_NONE) { + return; + } + + g_string_append_printf(s, "\n[802-1x]\n"); + + switch (auth->eap_method) { + case EAP_NONE: break; // LCOV_EXCL_LINE + case EAP_TLS: + g_string_append(s, "eap=tls\n"); + break; + case EAP_PEAP: + g_string_append(s, "eap=peap\n"); + break; + case EAP_TTLS: + g_string_append(s, "eap=ttls\n"); + break; + } + + if (auth->identity) { + g_string_append_printf(s, "identity=%s\n", auth->identity); + } + if (auth->anonymous_identity) { + g_string_append_printf(s, "anonymous-identity=%s\n", auth->anonymous_identity); + } + if (auth->password && auth->key_management != KEY_MANAGEMENT_WPA_PSK) { + g_string_append_printf(s, "password=%s\n", auth->password); + } + if (auth->ca_certificate) { + g_string_append_printf(s, "ca-cert=%s\n", auth->ca_certificate); + } + if (auth->client_certificate) { + g_string_append_printf(s, "client-cert=%s\n", auth->client_certificate); + } + if (auth->client_key) { + g_string_append_printf(s, "private-key=%s\n", auth->client_key); + } + if (auth->client_key_password) { + g_string_append_printf(s, "private-key-password=%s\n", auth->client_key_password); + } +} + +static void +write_wifi_auth_parameters(const authentication_settings* auth, GString *s) +{ + if (auth->key_management == KEY_MANAGEMENT_NONE) { + return; + } + + g_string_append(s, "\n[wifi-security]\n"); + + switch (auth->key_management) { + case KEY_MANAGEMENT_NONE: break; // LCOV_EXCL_LINE + case KEY_MANAGEMENT_WPA_PSK: + g_string_append(s, "key-mgmt=wpa-psk\n"); + if (auth->password) { + g_string_append_printf(s, "psk=%s\n", auth->password); + } + break; + case KEY_MANAGEMENT_WPA_EAP: + g_string_append(s, "key-mgmt=wpa-eap\n"); + break; + case KEY_MANAGEMENT_8021X: + g_string_append(s, "key-mgmt=ieee8021x\n"); + break; + } + + write_dot1x_auth_parameters(auth, s); +} + +static void maybe_generate_uuid(net_definition* def) { if (uuid_is_null(def->uuid)) @@ -406,6 +496,9 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_ if (def->type == ND_BOND) write_bond_parameters(def, s); + if (def->type == ND_TUNNEL) + write_tunnel_params(def, s); + g_string_append(s, "\n[ipv4]\n"); if (ap && ap->mode == WIFI_MODE_AP) @@ -415,6 +508,9 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_ else if (def->ip4_addresses) /* This requires adding at least one address (done below) */ g_string_append(s, "method=manual\n"); + else if (def->type == ND_TUNNEL) + /* sit tunnels will not start in link-local apparently */ + g_string_append(s, "method=disabled\n"); else /* Without any address, this is the only available mode */ g_string_append(s, "method=link-local\n"); @@ -437,12 +533,22 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_ write_routes(def, s, AF_INET); } + if (!def->dhcp4_overrides.use_routes) { + g_string_append(s, "ignore-auto-routes=true\n"); + g_string_append(s, "never-default=true\n"); + } + + if (def->dhcp4 && def->dhcp4_overrides.metric != METRIC_UNSPEC) + g_string_append_printf(s, "route-metric=%u\n", def->dhcp4_overrides.metric); + if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers) { g_string_append(s, "\n[ipv6]\n"); g_string_append(s, def->dhcp6 ? "method=auto\n" : "method=manual\n"); if (def->ip6_addresses) for (unsigned i = 0; i < def->ip6_addresses->len; ++i) g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip6_addresses, char*, i)); + if (def->ip6_privacy) + g_string_append(s, "ip6-privacy=2\n"); if (def->gateway6) g_string_append_printf(s, "gateway=%s\n", def->gateway6); if (def->ip6_nameservers) { @@ -457,6 +563,14 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_ /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */ write_routes(def, s, AF_INET6); + + if (!def->dhcp6_overrides.use_routes) { + g_string_append(s, "ignore-auto-routes=true\n"); + g_string_append(s, "never-default=true\n"); + } + + if (def->dhcp6_overrides.metric != METRIC_UNSPEC) + g_string_append_printf(s, "route-metric=%u\n", def->dhcp6_overrides.metric); } else { g_string_append(s, "\n[ipv6]\nmethod=ignore\n"); @@ -469,10 +583,14 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, NULL); g_string_append_printf(s, "\n[wifi]\nssid=%s\nmode=%s\n", ap->ssid, wifi_mode_str(ap->mode)); - if (ap->password) - g_string_append_printf(s, "\n[wifi-security]\nkey-mgmt=wpa-psk\npsk=%s\n", ap->password); + if (ap->has_auth) { + write_wifi_auth_parameters(&ap->auth, s); + } } else { conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, NULL); + if (def->has_auth) { + write_dot1x_auth_parameters(&def->auth, s); + } } /* NM connection files might contain secrets, and NM insists on tight permissions */ diff --git a/src/parse.c b/src/parse.c index f9d72c6..369de40 100644 --- a/src/parse.c +++ b/src/parse.c @@ -22,6 +22,7 @@ #include <glib.h> #include <glib/gstdio.h> +#include <gio/gio.h> #include <yaml.h> @@ -31,6 +32,7 @@ #define netdef_offset(field) GUINT_TO_POINTER(offsetof(net_definition, field)) #define route_offset(field) GUINT_TO_POINTER(offsetof(ip_route, field)) #define ip_rule_offset(field) GUINT_TO_POINTER(offsetof(ip_rule, field)) +#define auth_offset(field) GUINT_TO_POINTER(offsetof(authentication_settings, field)) /* file that is currently being processed, for useful error messages */ const char* current_file; @@ -40,6 +42,9 @@ net_definition* cur_netdef; /* wifi AP that is currently being processed */ wifi_access_point* cur_access_point; +/* authentication options that are currently being processed */ +authentication_settings* cur_auth; + ip_route* cur_route; ip_rule* cur_ip_rule; @@ -47,6 +52,10 @@ netdef_backend backend_global, backend_cur_type; /* Global ID → net_definition* map for all parsed config files */ GHashTable* netdefs; + +/* Contains the same objects as 'netdefs' but ordered by dependency */ +GList* netdefs_ordered; + /* Set of IDs in currently parsed YAML file, for being able to detect * "duplicate ID within one file" vs. allowing a drop-in to override/amend an * existing definition */ @@ -63,6 +72,80 @@ int missing_ids_found; * Loading and error handling ****************************************************/ +static void +write_error_marker(GString *message, int column) +{ + int i; + + for (i = 0; (column > 0 && i < column); i++) + g_string_append_printf(message, " "); + + g_string_append_printf(message, "^"); +} + +static char * +get_syntax_error_context(const int line_num, const int column, GError **error) +{ + GString *message = NULL; + GFile *cur_file = g_file_new_for_path(current_file); + GFileInputStream *file_stream; + GDataInputStream *stream; + gsize len; + gchar* line = NULL; + + message = g_string_sized_new(200); + file_stream = g_file_read(cur_file, NULL, error); + stream = g_data_input_stream_new (G_INPUT_STREAM(file_stream)); + g_object_unref(file_stream); + + for (int i = 0; i < line_num + 1; i++) { + g_free(line); + line = g_data_input_stream_read_line(stream, &len, NULL, error); + } + g_string_append_printf(message, "%s\n", line); + + write_error_marker(message, column); + + g_object_unref(stream); + g_object_unref(cur_file); + + return g_string_free(message, FALSE); +} + +static char * +get_parser_error_context(yaml_parser_t *parser, GError **error) +{ + GString *message = NULL; + unsigned char* line = parser->buffer.pointer; + unsigned char* current = line; + + message = g_string_sized_new(200); + + while (current > parser->buffer.start) { + current--; + if (*current == '\n') { + line = current + 1; + break; + } + } + if (current <= parser->buffer.start) + line = parser->buffer.start; + current = line + 1; + while (current <= parser->buffer.last) { + if (*current == '\n') { + *current = '\0'; + break; + } + current++; + } + + g_string_append_printf(message, "%s\n", line); + + write_error_marker(message, parser->problem_mark.column); + + return g_string_free(message, FALSE); +} + /** * Load YAML file name into a yaml_document_t. * @@ -86,10 +169,40 @@ load_yaml(const char* yaml, yaml_document_t* doc, GError** error) yaml_parser_initialize(&parser); yaml_parser_set_input_file(&parser, fyaml); if (!yaml_parser_load(&parser, doc)) { - g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, - "Invalid YAML at %s line %zu column %zu: %s", - yaml, parser.problem_mark.line, parser.problem_mark.column, parser.problem); + char *error_context = get_parser_error_context(&parser, error); + if ((char)*parser.buffer.pointer == '\t') + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: tabs are not allowed for indent:\n%s", + yaml, + parser.problem_mark.line + 1, + parser.problem_mark.column + 1, + error_context); + else if (((char)*parser.buffer.pointer == ' ' || (char)*parser.buffer.pointer == '\0') + && !parser.token_available) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: aliases are not supported:\n%s", + yaml, + parser.problem_mark.line + 1, + parser.problem_mark.column + 1, + error_context); + else if (parser.state == YAML_PARSE_BLOCK_MAPPING_KEY_STATE) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: inconsistent indentation:\n%s", + yaml, + parser.problem_mark.line + 1, + parser.problem_mark.column + 1, + error_context); + else { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: %s:\n%s", + yaml, + parser.problem_mark.line + 1, + parser.problem_mark.column + 1, + parser.problem, + error_context); + } ret = FALSE; + g_free(error_context); } fclose(fyaml); @@ -103,18 +216,26 @@ static gboolean yaml_error(const yaml_node_t* node, GError** error, const char* msg, ...) { va_list argp; - gchar* s; + char* s; + char* error_context = NULL; va_start(argp, msg); g_vasprintf(&s, msg, argp); + error_context = get_syntax_error_context(node->start_mark.line, node->start_mark.column, error); g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, - "Error in network definition %s line %zu column %zu: %s", - current_file, node->start_mark.line, node->start_mark.column, s); + "%s:%zu:%zu: Error in network definition: %s\n%s", + current_file, + node->start_mark.line + 1, + node->start_mark.column + 1, + s, + error_context); g_free(s); va_end(argp); return FALSE; } +#define YAML_VARIABLE_NODE YAML_NO_NODE + /** * Raise a GError about a type mismatch and return FALSE. */ @@ -125,6 +246,10 @@ assert_type_fn(yaml_node_t* node, yaml_node_type_t expected_type, GError** error return TRUE; switch (expected_type) { + case YAML_VARIABLE_NODE: + /* Special case, defer sanity checking to the next handlers */ + return TRUE; + break; case YAML_SCALAR_NODE: yaml_error(node, error, "expected scalar"); break; @@ -132,7 +257,7 @@ assert_type_fn(yaml_node_t* node, yaml_node_type_t expected_type, GError** error yaml_error(node, error, "expected sequence"); break; case YAML_MAPPING_NODE: - yaml_error(node, error, "expected mapping"); + yaml_error(node, error, "expected mapping (check indentation)"); break; // LCOV_EXCL_START @@ -249,7 +374,7 @@ process_mapping(yaml_document_t* doc, yaml_node_t* node, const mapping_entry_han assert_type(key, YAML_SCALAR_NODE); h = get_handler(handlers, scalar(key)); if (!h) - return yaml_error(node, error, "unknown key %s", scalar(key)); + return yaml_error(key, error, "unknown key '%s'", scalar(key)); assert_type(value, h->type); if (h->map_handlers) { g_assert(h->handler == NULL); @@ -360,7 +485,7 @@ handle_netdef_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GE g_ascii_strcasecmp(scalar(node), "n") == 0) v = FALSE; else - return yaml_error(node, error, "invalid boolean value %s", scalar(node)); + return yaml_error(node, error, "invalid boolean value '%s'", scalar(node)); *((gboolean*) ((void*) cur_netdef + offset)) = v; return TRUE; @@ -379,12 +504,101 @@ handle_netdef_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, G v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid unsigned int value %s", scalar(node)); + return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(node)); *((guint*) ((void*) cur_netdef + offset)) = (guint) v; return TRUE; } +static gboolean +is_ip4_address(const char* address) +{ + struct in_addr a4; + int ret; + + ret = inet_pton(AF_INET, address, &a4); + g_assert(ret >= 0); + if (ret > 0) + return TRUE; + + return FALSE; +} + +static gboolean +is_ip6_address(const char* address) +{ + struct in6_addr a6; + int ret; + + ret = inet_pton(AF_INET6, address, &a6); + g_assert(ret >= 0); + if (ret > 0) + return TRUE; + + return FALSE; +} + +static gboolean +handle_netdef_ip4(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_netdef + offset); + g_autofree char* addr = NULL; + char* prefix_len; + + /* these addresses can't have /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + + /* FIXME: stop excluding this from coverage; refactor address handling instead */ + // LCOV_EXCL_START + if (prefix_len) + return yaml_error(node, error, + "invalid address: a single IPv4 address (without /prefixlength) is required"); + + /* is it an IPv4 address? */ + if (!is_ip4_address(addr)) + return yaml_error(node, error, + "invalid IPv4 address: %s", scalar(node)); + // LCOV_EXCL_STOP + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +static gboolean +handle_netdef_ip6(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_netdef + offset); + g_autofree char* addr = NULL; + char* prefix_len; + + /* these addresses can't have /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + + /* FIXME: stop excluding this from coverage; refactor address handling instead */ + // LCOV_EXCL_START + if (prefix_len) + return yaml_error(node, error, + "invalid address: a single IPv6 address (without /prefixlength) is required"); + + /* is it an IPv6 address? */ + if (!is_ip6_address(addr)) + return yaml_error(node, error, + "invalid IPv6 address: %s", scalar(node)); + // LCOV_EXCL_STOP + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + + /**************************************************** * Grammar and handlers for network config "match" entry ****************************************************/ @@ -397,6 +611,66 @@ const mapping_entry_handler match_handlers[] = { }; /**************************************************** + * Grammar and handlers for network config "auth" entry + ****************************************************/ + +static gboolean +handle_auth_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_auth); + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_auth + offset); + g_free(*dest); + *dest = g_strdup(scalar(node)); + return TRUE; +} + +static gboolean +handle_auth_key_management(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_auth); + if (strcmp(scalar(node), "none") == 0) + cur_auth->key_management = KEY_MANAGEMENT_NONE; + else if (strcmp(scalar(node), "psk") == 0) + cur_auth->key_management = KEY_MANAGEMENT_WPA_PSK; + else if (strcmp(scalar(node), "eap") == 0) + cur_auth->key_management = KEY_MANAGEMENT_WPA_EAP; + else if (strcmp(scalar(node), "802.1x") == 0) + cur_auth->key_management = KEY_MANAGEMENT_8021X; + else + return yaml_error(node, error, "unknown key management type '%s'", scalar(node)); + return TRUE; +} + +static gboolean +handle_auth_method(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_auth); + if (strcmp(scalar(node), "tls") == 0) + cur_auth->eap_method = EAP_TLS; + else if (strcmp(scalar(node), "peap") == 0) + cur_auth->eap_method = EAP_PEAP; + else if (strcmp(scalar(node), "ttls") == 0) + cur_auth->eap_method = EAP_TTLS; + else + return yaml_error(node, error, "unknown EAP method '%s'", scalar(node)); + return TRUE; +} + +const mapping_entry_handler auth_handlers[] = { + {"key-management", YAML_SCALAR_NODE, handle_auth_key_management}, + {"method", YAML_SCALAR_NODE, handle_auth_method}, + {"identity", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(identity)}, + {"anonymous-identity", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(anonymous_identity)}, + {"password", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(password)}, + {"ca-certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(ca_certificate)}, + {"client-certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_certificate)}, + {"client-key", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key)}, + {"client-key-password", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key_password)}, + {NULL} +}; + +/**************************************************** * Grammar and handlers for network device definition ****************************************************/ @@ -415,11 +689,30 @@ static gboolean handle_access_point_password(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { g_assert(cur_access_point); - cur_access_point->password = g_strdup(scalar(node)); + /* shortcut for WPA-PSK */ + cur_access_point->has_auth = TRUE; + cur_access_point->auth.key_management = KEY_MANAGEMENT_WPA_PSK; + g_free(cur_access_point->auth.password); + cur_access_point->auth.password = g_strdup(scalar(node)); return TRUE; } static gboolean +handle_access_point_auth(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret; + + g_assert(cur_access_point); + cur_access_point->has_auth = TRUE; + + cur_auth = &cur_access_point->auth; + ret = process_mapping(doc, node, auth_handlers, error); + cur_auth = NULL; + + return ret; +} + +static gboolean handle_access_point_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { g_assert(cur_access_point); @@ -437,6 +730,7 @@ handle_access_point_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, const mapping_entry_handler wifi_access_point_handlers[] = { {"mode", YAML_SCALAR_NODE, handle_access_point_mode}, {"password", YAML_SCALAR_NODE, handle_access_point_password}, + {"auth", YAML_MAPPING_NODE, handle_access_point_auth}, {NULL} }; @@ -475,7 +769,7 @@ handle_accept_ra(yaml_document_t* doc, yaml_node_t* node, const void* data, GErr g_ascii_strcasecmp(scalar(node), "n") == 0) cur_netdef->accept_ra = ACCEPT_RA_DISABLED; else - return yaml_error(node, error, "invalid boolean value %s", scalar(node)); + return yaml_error(node, error, "invalid boolean value '%s'", scalar(node)); return TRUE; } @@ -488,12 +782,23 @@ handle_match(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** er } static gboolean +handle_auth(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret; + + cur_netdef->has_auth = TRUE; + + cur_auth = &cur_netdef->auth; + ret = process_mapping(doc, node, auth_handlers, error); + cur_auth = NULL; + + return ret; +} + +static gboolean handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { - struct in_addr a4; - struct in6_addr a6; - int ret; g_autofree char* addr = NULL; char* prefix_len; guint64 prefix_len_num; @@ -510,9 +815,7 @@ handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError* prefix_len_num = g_ascii_strtoull(prefix_len, NULL, 10); /* is it an IPv4 address? */ - ret = inet_pton(AF_INET, addr, &a4); - g_assert(ret >= 0); - if (ret > 0) { + if (is_ip4_address(addr)) { if (prefix_len_num == 0 || prefix_len_num > 32) return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry)); @@ -524,9 +827,7 @@ handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError* } /* is it an IPv6 address? */ - ret = inet_pton(AF_INET6, addr, &a6); - g_assert(ret >= 0); - if (ret > 0) { + if (is_ip6_address(addr)) { if (prefix_len_num == 0 || prefix_len_num > 128) return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry)); if (!cur_netdef->ip6_addresses) @@ -545,10 +846,7 @@ handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError* static gboolean handle_gateway4(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { - struct in_addr a4; - int ret = inet_pton(AF_INET, scalar(node), &a4); - g_assert(ret >= 0); - if (ret == 0) + if (!is_ip4_address(scalar(node))) return yaml_error(node, error, "invalid IPv4 address '%s'", scalar(node)); cur_netdef->gateway4 = g_strdup(scalar(node)); return TRUE; @@ -557,10 +855,7 @@ handle_gateway4(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** static gboolean handle_gateway6(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { - struct in6_addr a6; - int ret = inet_pton(AF_INET6, scalar(node), &a6); - g_assert(ret >= 0); - if (ret == 0) + if (!is_ip6_address(scalar(node))) return yaml_error(node, error, "invalid IPv6 address '%s'", scalar(node)); cur_netdef->gateway6 = g_strdup(scalar(node)); return TRUE; @@ -592,11 +887,11 @@ handle_wifi_access_points(yaml_document_t* doc, yaml_node_t* node, const void* d ret = yaml_error(key, error, "%s: Duplicate access point SSID '%s'", cur_netdef->id, cur_access_point->ssid); cur_access_point = NULL; return ret; - } + } if (!process_mapping(doc, value, wifi_access_point_handlers, error)) { - cur_access_point = NULL; - return FALSE; + cur_access_point = NULL; + return FALSE; } cur_access_point = NULL; @@ -623,10 +918,10 @@ handle_bridge_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* da add_missing_node(entry); } else { if (component->bridge && g_strcmp0(component->bridge, cur_netdef->id) != 0) - return yaml_error(node, error, "%s: interface %s is already assigned to bridge %s", + return yaml_error(node, error, "%s: interface '%s' is already assigned to bridge %s", cur_netdef->id, scalar(entry), component->bridge); if (component->bond) - return yaml_error(node, error, "%s: interface %s is already assigned to bond %s", + return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s", cur_netdef->id, scalar(entry), component->bond); component->bridge = g_strdup(cur_netdef->id); } @@ -653,10 +948,10 @@ handle_bond_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* data add_missing_node(entry); } else { if (component->bridge) - return yaml_error(node, error, "%s: interface %s is already assigned to bridge %s", + return yaml_error(node, error, "%s: interface '%s' is already assigned to bridge %s", cur_netdef->id, scalar(entry), component->bridge); if (component->bond && g_strcmp0(component->bond, cur_netdef->id) != 0) - return yaml_error(node, error, "%s: interface %s is already assigned to bond %s", + return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s", cur_netdef->id, scalar(entry), component->bond); component->bond = g_strdup(cur_netdef->id); } @@ -684,16 +979,11 @@ static gboolean handle_nameservers_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { - struct in_addr a4; - struct in6_addr a6; - int ret; yaml_node_t *entry = yaml_document_get_node(doc, *i); assert_type(entry, YAML_SCALAR_NODE); /* is it an IPv4 address? */ - ret = inet_pton(AF_INET, scalar(entry), &a4); - g_assert(ret >= 0); - if (ret > 0) { + if (is_ip4_address(scalar(entry))) { if (!cur_netdef->ip4_nameservers) cur_netdef->ip4_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); char* s = g_strdup(scalar(entry)); @@ -702,9 +992,7 @@ handle_nameservers_addresses(yaml_document_t* doc, yaml_node_t* node, const void } /* is it an IPv6 address? */ - ret = inet_pton(AF_INET6, scalar(entry), &a6); - g_assert(ret >= 0); - if (ret > 0) { + if (is_ip6_address(scalar(entry))) { if (!cur_netdef->ip6_nameservers) cur_netdef->ip6_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); char* s = g_strdup(scalar(entry)); @@ -734,7 +1022,7 @@ handle_link_local(yaml_document_t* doc, yaml_node_t* node, const void* _, GError 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)); + return yaml_error(node, error, "invalid value for link-local: '%s'", scalar(entry)); } cur_netdef->linklocal.ipv4 = ipv4; @@ -758,18 +1046,18 @@ handle_optional_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _ 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)); - } + 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; } @@ -777,25 +1065,18 @@ handle_optional_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _ static int get_ip_family(const char* address) { - struct in_addr a4; - struct in6_addr a6; g_autofree char *ip_str; char *prefix_len; - int ret = -1; ip_str = g_strdup(address); prefix_len = strrchr(ip_str, '/'); if (prefix_len) *prefix_len = '\0'; - ret = inet_pton(AF_INET, ip_str, &a4); - g_assert(ret >= 0); - if (ret > 0) + if (is_ip4_address(ip_str)) return AF_INET; - ret = inet_pton(AF_INET6, ip_str, &a6); - g_assert(ret >= 0); - if (ret > 0) + if (is_ip6_address(ip_str)) return AF_INET6; return -1; @@ -831,7 +1112,7 @@ handle_routes_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GE g_ascii_strcasecmp(scalar(node), "n") == 0) v = FALSE; else - return yaml_error(node, error, "invalid boolean value %s", scalar(node)); + return yaml_error(node, error, "invalid boolean value '%s'", scalar(node)); *((gboolean*) ((void*) cur_route + offset)) = v; return TRUE; @@ -877,7 +1158,7 @@ handle_routes_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GErr g_free(*dest); if (family < 0) - return yaml_error(node, error, "invalid IP family %d", family); + return yaml_error(node, error, "invalid IP family '%d'", family); if (!check_and_set_family(family, &cur_route->family)) return yaml_error(node, error, "IP family mismatch in route to %s", scalar(node)); @@ -896,7 +1177,7 @@ handle_ip_rule_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GEr g_free(*dest); if (family < 0) - return yaml_error(node, error, "invalid IP family %d", family); + return yaml_error(node, error, "invalid IP family '%d'", family); if (!check_and_set_family(family, &cur_ip_rule->family)) return yaml_error(node, error, "IP family mismatch in route to %s", scalar(node)); @@ -914,7 +1195,7 @@ handle_ip_rule_prio(yaml_document_t* doc, yaml_node_t* node, const void* data, G v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid priority value %s", scalar(node)); + return yaml_error(node, error, "invalid priority value '%s'", scalar(node)); cur_ip_rule->priority = (guint) v; return TRUE; @@ -942,7 +1223,7 @@ handle_routes_table(yaml_document_t* doc, yaml_node_t* node, const void* data, G v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid routing table %s", scalar(node)); + return yaml_error(node, error, "invalid routing table '%s'", scalar(node)); cur_route->table = (guint) v; return TRUE; @@ -956,7 +1237,7 @@ handle_ip_rule_table(yaml_document_t* doc, yaml_node_t* node, const void* data, v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid routing table %s", scalar(node)); + return yaml_error(node, error, "invalid routing table '%s'", scalar(node)); cur_ip_rule->table = (guint) v; return TRUE; @@ -970,7 +1251,7 @@ handle_ip_rule_fwmark(yaml_document_t* doc, yaml_node_t* node, const void* data, v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid fwmark value %s", scalar(node)); + return yaml_error(node, error, "invalid fwmark value '%s'", scalar(node)); cur_ip_rule->fwmark = (guint) v; return TRUE; @@ -984,7 +1265,7 @@ handle_routes_metric(yaml_document_t* doc, yaml_node_t* node, const void* _, GEr v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid unsigned int value %s", scalar(node)); + return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(node)); cur_route->metric = (guint) v; return TRUE; @@ -1015,12 +1296,12 @@ handle_bridge_path_cost(yaml_document_t* doc, yaml_node_t* node, const void* dat } else { ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); if (*ref_ptr) - return yaml_error(node, error, "%s: interface %s already has a path cost of %u", + return yaml_error(node, error, "%s: interface '%s' already has a path cost of %u", cur_netdef->id, scalar(key), *ref_ptr); v = g_ascii_strtoull(scalar(value), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) - return yaml_error(node, error, "invalid unsigned int value %s", scalar(value)); + return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(value)); g_debug("%s: adding path '%s' of cost: %d", cur_netdef->id, scalar(key), v); @@ -1051,7 +1332,7 @@ handle_bridge_port_priority(yaml_document_t* doc, yaml_node_t* node, const void* } else { ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); if (*ref_ptr) - return yaml_error(node, error, "%s: interface %s already has a port priority of %u", + return yaml_error(node, error, "%s: interface '%s' already has a port priority of %u", cur_netdef->id, scalar(key), *ref_ptr); v = g_ascii_strtoull(scalar(value), &endptr, 10); @@ -1112,7 +1393,7 @@ handle_routes(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** e 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 */ + cur_route->metric = METRIC_UNSPEC; /* 0 is a valid metric */ if (process_mapping(doc, entry, routes_handlers, error)) { if (!cur_netdef->routes) { @@ -1191,8 +1472,6 @@ static gboolean handle_arp_ip_targets(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { - struct in_addr a4; - int ret; g_autofree char* addr = NULL; yaml_node_t *entry = yaml_document_get_node(doc, *i); assert_type(entry, YAML_SCALAR_NODE); @@ -1200,9 +1479,7 @@ handle_arp_ip_targets(yaml_document_t* doc, yaml_node_t* node, const void* _, GE addr = g_strdup(scalar(entry)); /* is it an IPv4 address? */ - ret = inet_pton(AF_INET, addr, &a4); - g_assert(ret >= 0); - if (ret > 0) { + if (is_ip4_address(addr)) { if (!cur_netdef->bond_params.arp_ip_targets) cur_netdef->bond_params.arp_ip_targets = g_array_new(FALSE, FALSE, sizeof(char*)); char* s = g_strdup(scalar(entry)); @@ -1287,6 +1564,108 @@ handle_dhcp_identifier(yaml_document_t* doc, yaml_node_t* node, const void* data } /**************************************************** + * Grammar and handlers for tunnels + ****************************************************/ + +const char* +tunnel_mode_to_string(tunnel_mode mode) +{ + return tunnel_mode_table[mode]; +} + +static gboolean +handle_tunnel_addr(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_autofree char* addr = NULL; + char* prefix_len; + + /* split off /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + if (prefix_len) + return yaml_error(node, error, "address '%s' should not include /prefixlength", scalar(node)); + + /* is it an IPv4 address? */ + if (is_ip4_address(addr)) + return handle_netdef_ip4(doc, node, data, error); + + /* is it an IPv6 address? */ + if (is_ip6_address(addr)) + return handle_netdef_ip6(doc, node, data, error); + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(node)); +} + +static gboolean +handle_tunnel_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + const char *key = scalar(node); + tunnel_mode i; + + // Skip over unknown (0) tunnel mode. + for (i = 1; i < _TUNNEL_MODE_MAX; ++i) { + if (g_strcmp0(tunnel_mode_table[i], key) == 0) { + cur_netdef->tunnel.mode = i; + return TRUE; + } + } + + return yaml_error(node, error, "%s: tunnel mode '%s' is not supported", cur_netdef->id, key); +} + +static gboolean +handle_tunnel_key(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + /* Tunnel key should be a number or dotted quad. */ + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_netdef + offset); + guint64 v; + gchar* endptr; + + v = g_ascii_strtoull(scalar(node), &endptr, 10); + if (*endptr != '\0' || v > G_MAXUINT) { + /* Not a simple uint, try for a dotted quad */ + if (!is_ip4_address(scalar(node))) + return yaml_error(node, error, "invalid tunnel key '%s'", scalar(node)); + } + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +const mapping_entry_handler tunnel_keys_handlers[] = { + {"input", YAML_SCALAR_NODE, handle_tunnel_key, NULL, netdef_offset(tunnel.input_key)}, + {"output", YAML_SCALAR_NODE, handle_tunnel_key, NULL, netdef_offset(tunnel.output_key)}, + {NULL} +}; + +static gboolean +handle_tunnel_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret = FALSE; + + /* We overload the key 'key' for tunnels; such that it can either be a + * single scalar with the same key to use for both input and output keys, + * or a mapping where one can specify each. + */ + if (node->type == YAML_SCALAR_NODE) { + ret = handle_tunnel_key(doc, node, netdef_offset(tunnel.input_key), error); + if (ret) + ret = handle_tunnel_key(doc, node, netdef_offset(tunnel.output_key), error); + } + else if (node->type == YAML_MAPPING_NODE) { + ret = process_mapping(doc, node, tunnel_keys_handlers, error); + } + else { + return yaml_error(node, error, "invalid type for 'keys': must be a scalar or mapping"); + } + + return ret; +} + +/**************************************************** * Grammar and handlers for network devices ****************************************************/ @@ -1296,42 +1675,68 @@ const mapping_entry_handler nameservers_handlers[] = { {NULL} }; +/* Handlers for DHCP overrides. */ +#define COMMON_DHCP_OVERRIDES_HANDLERS(overrides) \ + {"hostname", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(overrides.hostname)}, \ + {"route-metric", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(overrides.metric)}, \ + {"send-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.send_hostname)}, \ + {"use-dns", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_dns)}, \ + {"use-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_hostname)}, \ + {"use-mtu", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_mtu)}, \ + {"use-ntp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_ntp)}, \ + {"use-routes", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_routes)} + +const mapping_entry_handler dhcp4_overrides_handlers[] = { + COMMON_DHCP_OVERRIDES_HANDLERS(dhcp4_overrides), + {NULL}, +}; + +const mapping_entry_handler dhcp6_overrides_handlers[] = { + COMMON_DHCP_OVERRIDES_HANDLERS(dhcp6_overrides), + {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}, \ +#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}, \ + {"dhcp4-overrides", YAML_MAPPING_NODE, NULL, dhcp4_overrides_handlers}, \ + {"dhcp6-overrides", YAML_MAPPING_NODE, NULL, dhcp6_overrides_handlers}, \ + {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, \ + {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, \ + {"ipv6-privacy", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(ip6_privacy)}, \ + {"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)}, \ + {"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[] = { COMMON_LINK_HANDLERS, PHYSICAL_LINK_HANDLERS, - {NULL}, + {"auth", YAML_MAPPING_NODE, handle_auth}, + {NULL} }; const mapping_entry_handler wifi_def_handlers[] = { COMMON_LINK_HANDLERS, PHYSICAL_LINK_HANDLERS, {"access-points", YAML_MAPPING_NODE, handle_wifi_access_points}, + {"auth", YAML_MAPPING_NODE, handle_auth}, {NULL} }; @@ -1356,6 +1761,20 @@ const mapping_entry_handler vlan_def_handlers[] = { {NULL} }; +const mapping_entry_handler tunnel_def_handlers[] = { + COMMON_LINK_HANDLERS, + {"mode", YAML_SCALAR_NODE, handle_tunnel_mode}, + {"local", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.local_ip)}, + {"remote", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.remote_ip)}, + + /* Handle key/keys for clarity in config: this can be either a scalar or + * mapping of multiple keys (input and output) + */ + {"key", YAML_NO_NODE, handle_tunnel_key_mapping}, + {"keys", YAML_NO_NODE, handle_tunnel_key_mapping}, + {NULL} +}; + /**************************************************** * Grammar and handlers for network node ****************************************************/ @@ -1375,9 +1794,103 @@ handle_network_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _, } static gboolean +validate_tunnel(net_definition* nd, yaml_node_t* node, GError** error) +{ + if (nd->tunnel.mode == TUNNEL_MODE_UNKNOWN) + return yaml_error(node, error, "%s: missing 'mode' property for tunnel", nd->id); + + /* Backend-specific validation rules */ + switch (nd->backend) { + case BACKEND_NETWORKD: + switch (nd->tunnel.mode) { + case TUNNEL_MODE_VTI: + case TUNNEL_MODE_VTI6: + break; + + /* TODO: Remove this exception and fix ISATAP handling with the + * networkd backend. + * systemd-networkd has grown ISATAP support in 918049a. + */ + case TUNNEL_MODE_ISATAP: + return yaml_error(node, error, + "%s: %s tunnel mode is not supported by networkd", + nd->id, + g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1)); + break; + + default: + if (nd->tunnel.input_key) + return yaml_error(node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); + if (nd->tunnel.output_key) + return yaml_error(node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); + break; + } + break; + + case BACKEND_NM: + switch (nd->tunnel.mode) { + case TUNNEL_MODE_GRE: + case TUNNEL_MODE_IP6GRE: + break; + + case TUNNEL_MODE_GRETAP: + case TUNNEL_MODE_IP6GRETAP: + return yaml_error(node, error, + "%s: %s tunnel mode is not supported by NetworkManager", + nd->id, + g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1)); + break; + + default: + if (nd->tunnel.input_key) + return yaml_error(node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); + if (nd->tunnel.output_key) + return yaml_error(node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); + break; + } + break; + + // LCOV_EXCL_START + default: + break; + // LCOV_EXCL_STOP + } + + /* Validate local/remote IPs */ + if (!nd->tunnel.local_ip) + return yaml_error(node, error, "%s: missing 'local' property for tunnel", nd->id); + if (!nd->tunnel.remote_ip) + return yaml_error(node, error, "%s: missing 'remote' property for tunnel", nd->id); + + switch(nd->tunnel.mode) { + case TUNNEL_MODE_IPIP6: + case TUNNEL_MODE_IP6IP6: + case TUNNEL_MODE_IP6GRE: + case TUNNEL_MODE_IP6GRETAP: + case TUNNEL_MODE_VTI6: + if (!is_ip6_address(nd->tunnel.local_ip)) + return yaml_error(node, error, "%s: 'local' must be a valid IPv6 address for this tunnel type", nd->id); + if (!is_ip6_address(nd->tunnel.remote_ip)) + return yaml_error(node, error, "%s: 'remote' must be a valid IPv6 address for this tunnel type", nd->id); + break; + + default: + if (!is_ip4_address(nd->tunnel.local_ip)) + return yaml_error(node, error, "%s: 'local' must be a valid IPv4 address for this tunnel type", nd->id); + if (!is_ip4_address(nd->tunnel.remote_ip)) + return yaml_error(node, error, "%s: 'remote' must be a valid IPv4 address for this tunnel type", nd->id); + break; + } + + return TRUE; +} + +static gboolean validate_netdef(net_definition* nd, yaml_node_t* node, GError** error) { int missing_id_count = g_hash_table_size(missing_id); + gboolean valid = FALSE; + g_assert(nd->type != ND_NONE); /* Skip all validation if we're missing some definition IDs (devices). @@ -1388,22 +1901,44 @@ validate_netdef(net_definition* nd, yaml_node_t* node, GError** error) /* set-name: requires match: */ if (nd->set_name && !nd->has_match) - return yaml_error(node, error, "%s: set-name: requires match: properties", nd->id); + return yaml_error(node, error, "%s: 'set-name:' requires 'match:' properties", nd->id); if (nd->type == ND_WIFI && nd->access_points == NULL) return yaml_error(node, error, "%s: No access points defined", nd->id); if (nd->type == ND_VLAN) { if (!nd->vlan_link) - return yaml_error(node, error, "%s: missing link property", nd->id); + return yaml_error(node, error, "%s: missing 'link' property", nd->id); nd->vlan_link->has_vlans = TRUE; if (nd->vlan_id == G_MAXUINT) - return yaml_error(node, error, "%s: missing id property", nd->id); + return yaml_error(node, error, "%s: missing 'id' property", nd->id); if (nd->vlan_id > 4094) - return yaml_error(node, error, "%s: invalid id %u (allowed values are 0 to 4094)", nd->id, nd->vlan_id); + return yaml_error(node, error, "%s: invalid id '%u' (allowed values are 0 to 4094)", nd->id, nd->vlan_id); } - return TRUE; + if (nd->type == ND_TUNNEL) { + valid = validate_tunnel(nd, node, error); + if (!valid) + goto validation_error; + } + + valid = TRUE; + +validation_error: + return valid; +} + +static void +initialize_dhcp_overrides(dhcp_overrides* overrides) +{ + overrides->use_dns = TRUE; + overrides->use_ntp = TRUE; + overrides->send_hostname = TRUE; + overrides->use_hostname = TRUE; + overrides->use_mtu = TRUE; + overrides->use_routes = TRUE; + overrides->hostname = NULL; + overrides->metric = METRIC_UNSPEC; } /** @@ -1450,13 +1985,23 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G /* create new network definition */ cur_netdef = g_new0(net_definition, 1); cur_netdef->type = GPOINTER_TO_UINT(data); - cur_netdef->backend = backend_cur_type ?: BACKEND_NONE; + cur_netdef->backend = backend_cur_type ?: get_default_backend_for_type(cur_netdef->type); cur_netdef->id = g_strdup(scalar(key)); + + /* Set some default values */ cur_netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */ + cur_netdef->tunnel.mode = TUNNEL_MODE_UNKNOWN; 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); + netdefs_ordered = g_list_append(netdefs_ordered, cur_netdef); + + /* DHCP override defaults */ + initialize_dhcp_overrides(&cur_netdef->dhcp4_overrides); + initialize_dhcp_overrides(&cur_netdef->dhcp6_overrides); + + g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef); } // XXX: breaks multi-pass parsing. @@ -1465,11 +2010,12 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G /* and fill it with definitions */ switch (cur_netdef->type) { - case ND_ETHERNET: handlers = ethernet_def_handlers; break; - case ND_WIFI: handlers = wifi_def_handlers; break; - case ND_BRIDGE: handlers = bridge_def_handlers; break; case ND_BOND: handlers = bond_def_handlers; break; + case ND_BRIDGE: handlers = bridge_def_handlers; break; + case ND_ETHERNET: handlers = ethernet_def_handlers; break; + case ND_TUNNEL: handlers = tunnel_def_handlers; break; case ND_VLAN: handlers = vlan_def_handlers; break; + case ND_WIFI: handlers = wifi_def_handlers; break; default: g_assert_not_reached(); // LCOV_EXCL_LINE } if (!process_mapping(doc, value, handlers, error)) @@ -1489,13 +2035,14 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G } const mapping_entry_handler network_handlers[] = { - {"version", YAML_SCALAR_NODE, handle_network_version}, - {"renderer", YAML_SCALAR_NODE, handle_network_renderer}, - {"ethernets", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_ETHERNET)}, - {"wifis", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_WIFI)}, - {"bridges", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_BRIDGE)}, {"bonds", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_BOND)}, + {"bridges", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_BRIDGE)}, + {"ethernets", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_ETHERNET)}, + {"renderer", YAML_SCALAR_NODE, handle_network_renderer}, + {"tunnels", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_TUNNEL)}, + {"version", YAML_SCALAR_NODE, handle_network_version}, {"vlans", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_VLAN)}, + {"wifis", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(ND_WIFI)}, {NULL} }; @@ -1550,7 +2097,7 @@ process_document(yaml_document_t* doc, GError** error) g_hash_table_iter_next (&iter, &key, &value); missing = (missing_node*) value; - return yaml_error(missing->node, error, "%s: interface %s is not defined", + return yaml_error(missing->node, error, "%s: interface '%s' is not defined", missing->netdef_id, key); } @@ -1595,10 +2142,13 @@ static void finish_iterator(gpointer key, gpointer value, gpointer user_data) { net_definition* nd = value; + /* Take more steps to make sure we always have a backend set for netdefs */ + // LCOV_EXCL_START if (nd->backend == BACKEND_NONE) { nd->backend = get_default_backend_for_type(nd->type); g_debug("%s: setting default backend to %i", nd->id, nd->backend); } + // LCOV_EXCL_STOP } /** diff --git a/src/parse.h b/src/parse.h index 001a75a..a9b2d12 100644 --- a/src/parse.h +++ b/src/parse.h @@ -34,6 +34,7 @@ typedef enum { ND_BRIDGE = ND_VIRTUAL, ND_BOND, ND_VLAN, + ND_TUNNEL, } netdef_type; typedef enum { @@ -63,6 +64,46 @@ typedef enum { OPTIONAL_STATIC = 1<<4, } optional_addr; +/* Tunnel mode enum; sync with NetworkManager's DBUS API */ +/* TODO: figure out whether networkd's GRETAP and NM's ISATAP + * are the same thing. + */ +typedef enum { + TUNNEL_MODE_UNKNOWN = 0, + TUNNEL_MODE_IPIP = 1, + TUNNEL_MODE_GRE = 2, + TUNNEL_MODE_SIT = 3, + TUNNEL_MODE_ISATAP = 4, // NM only. + TUNNEL_MODE_VTI = 5, + TUNNEL_MODE_IP6IP6 = 6, + TUNNEL_MODE_IPIP6 = 7, + TUNNEL_MODE_IP6GRE = 8, + TUNNEL_MODE_VTI6 = 9, + + /* systemd-only, apparently? */ + TUNNEL_MODE_GRETAP = 101, + TUNNEL_MODE_IP6GRETAP = 102, + + _TUNNEL_MODE_MAX, +} tunnel_mode; + +static const char* const +tunnel_mode_table[_TUNNEL_MODE_MAX] = { + [TUNNEL_MODE_UNKNOWN] = "unknown", + [TUNNEL_MODE_IPIP] = "ipip", + [TUNNEL_MODE_GRE] = "gre", + [TUNNEL_MODE_SIT] = "sit", + [TUNNEL_MODE_ISATAP] = "isatap", + [TUNNEL_MODE_VTI] = "vti", + [TUNNEL_MODE_IP6IP6] = "ip6ip6", + [TUNNEL_MODE_IPIP6] = "ipip6", + [TUNNEL_MODE_IP6GRE] = "ip6gre", + [TUNNEL_MODE_VTI6] = "vti6", + + [TUNNEL_MODE_GRETAP] = "gretap", + [TUNNEL_MODE_IP6GRETAP] = "ip6gretap", +}; + struct optional_address_option { char* name; optional_addr flag; @@ -70,11 +111,49 @@ struct optional_address_option { extern struct optional_address_option optional_address_options[]; +typedef enum { + KEY_MANAGEMENT_NONE, + KEY_MANAGEMENT_WPA_PSK, + KEY_MANAGEMENT_WPA_EAP, + KEY_MANAGEMENT_8021X, +} auth_key_management_type; + +typedef enum { + EAP_NONE, + EAP_TLS, + EAP_PEAP, + EAP_TTLS, +} auth_eap_method; + typedef struct missing_node { char* netdef_id; const yaml_node_t* node; } missing_node; +typedef struct authentication_settings { + auth_key_management_type key_management; + auth_eap_method eap_method; + char* identity; + char* anonymous_identity; + char* password; + char* ca_certificate; + char* client_certificate; + char* client_key; + char* client_key_password; +} authentication_settings; + +/* Fields below are valid for dhcp4 and dhcp6 unless otherwise noted. */ +typedef struct dhcp_overrides { + gboolean use_dns; + gboolean use_ntp; + gboolean send_hostname; + gboolean use_hostname; + gboolean use_mtu; + gboolean use_routes; + char* hostname; + guint metric; +} dhcp_overrides; + /** * Represent a configuration stanza */ @@ -94,9 +173,12 @@ typedef struct net_definition { gboolean dhcp4; gboolean dhcp6; char* dhcp_identifier; + dhcp_overrides dhcp4_overrides; + dhcp_overrides dhcp6_overrides; ra_mode accept_ra; GArray* ip4_addresses; GArray* ip6_addresses; + gboolean ip6_privacy; char* gateway4; char* gateway6; GArray* ip4_nameservers; @@ -173,6 +255,16 @@ typedef struct net_definition { } bridge_params; gboolean custom_bridging; + struct { + tunnel_mode mode; + char *local_ip; + char *remote_ip; + char *input_key; + char *output_key; + } tunnel; + + authentication_settings auth; + gboolean has_auth; } net_definition; typedef enum { @@ -184,7 +276,9 @@ typedef enum { typedef struct { wifi_mode mode; char* ssid; - char* password; + + authentication_settings auth; + gboolean has_auth; } wifi_access_point; #define METRIC_UNSPEC G_MAXUINT @@ -227,6 +321,7 @@ typedef struct { /* Written/updated by parse_yaml(): char* id → net_definition */ extern GHashTable* netdefs; +extern GList* netdefs_ordered; /**************************************************** * Functions @@ -235,3 +330,4 @@ extern GHashTable* netdefs; gboolean parse_yaml(const char* filename, GError** error); gboolean finish_parse(GError** error); netdef_backend get_global_backend(); +const char* tunnel_mode_to_string(tunnel_mode mode); diff --git a/tests/generate.py b/tests/generate.py deleted file mode 100755 index 72e05fe..0000000 --- a/tests/generate.py +++ /dev/null @@ -1,5217 +0,0 @@ -#!/usr/bin/python3 -# Blackbox tests of netplan generate that verify that the generated -# configuration files look as expected. These are run during "make check" and -# don't touch the system configuration at all. -# -# Copyright (C) 2016 Canonical, Ltd. -# Author: Martin Pitt <martin.pitt@ubuntu.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import random -import re -import sys -import stat -import string -import tempfile -import textwrap -import subprocess -import unittest - -exe_generate = os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'generate') - -# make sure we fail on criticals -os.environ['G_DEBUG'] = 'fatal-criticals' - -# common patterns for expected output -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' - - -class TestBase(unittest.TestCase): - def setUp(self): - self.workdir = tempfile.TemporaryDirectory() - self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') - self.nm_enable_all_conf = os.path.join( - self.workdir.name, 'run', 'NetworkManager', 'conf.d', '10-globally-managed-devices.conf') - - def generate(self, yaml, expect_fail=False, extra_args=[], confs=None): - '''Call generate with given YAML string as configuration - - Return stderr output. - ''' - conf = os.path.join(self.confdir, 'a.yaml') - os.makedirs(os.path.dirname(conf), exist_ok=True) - if yaml is not None: - with open(conf, 'w') as f: - f.write(yaml) - if confs: - for f, contents in confs.items(): - with open(os.path.join(self.confdir, f + '.yaml'), 'w') as f: - f.write(contents) - - argv = [exe_generate, '--root-dir', self.workdir.name] + extra_args - if 'TEST_SHELL' in os.environ: - print('Test is about to run:\n%s' % ' '.join(argv)) - subprocess.call(['bash', '-i'], cwd=self.workdir.name) - - p = subprocess.Popen(argv, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) - (out, err) = p.communicate() - if expect_fail: - self.assertGreater(p.returncode, 0) - else: - self.assertEqual(p.returncode, 0, err) - 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: - self.assertFalse(os.path.exists(networkd_dir)) - return - - self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) - self.assertEqual(set(os.listdir(networkd_dir)), - {'10-netplan-' + f for f in file_contents_map}) - for fname, contents in file_contents_map.items(): - with open(os.path.join(networkd_dir, '10-netplan-' + fname)) as f: - self.assertEqual(f.read(), contents) - - def assert_networkd_udev(self, file_contents_map): - udev_dir = os.path.join(self.workdir.name, 'run', 'udev', 'rules.d') - if not file_contents_map: - # it can either not exist, or can only contain 90-netplan.rules - self.assertTrue((not os.path.exists(udev_dir)) or - (os.listdir(udev_dir) == ['90-netplan.rules'])) - return - - self.assertEqual(set(os.listdir(udev_dir)) - set(['90-netplan.rules']), - {'99-netplan-' + f for f in file_contents_map}) - for fname, contents in file_contents_map.items(): - 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') - if conf: - with open(conf_path) as f: - self.assertEqual(f.read(), conf) - else: - if os.path.exists(conf_path): - with open(conf_path) as f: - self.fail('unexpected %s:\n%s' % (conf_path, f.read())) - - # check connections - con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') - if connections_map: - self.assertEqual(set(os.listdir(con_dir)), - set(['netplan-' + n for n in connections_map])) - for fname, contents in connections_map.items(): - with open(os.path.join(con_dir, 'netplan-' + fname)) as f: - self.assertEqual(f.read(), contents) - # NM connection files might contain secrets - self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) - else: - if os.path.exists(con_dir): - self.assertEqual(os.listdir(con_dir), []) - - def assert_nm_udev(self, contents): - rule_path = os.path.join(self.workdir.name, 'run/udev/rules.d/90-netplan.rules') - if contents is None: - self.assertFalse(os.path.exists(rule_path)) - return - with open(rule_path) as f: - self.assertEqual(f.read(), contents) - - -class TestConfigArgs(TestBase): - '''Config file argument handling''' - - def test_no_files(self): - subprocess.check_call([exe_generate, '--root-dir', self.workdir.name]) - self.assertEqual(os.listdir(self.workdir.name), []) - self.assert_nm_udev(None) - - def test_no_configs(self): - self.generate('network:\n version: 2') - # should not write any files - self.assertEqual(os.listdir(self.workdir.name), ['etc']) - self.assert_nm_udev(None) - - def test_empty_config(self): - self.generate('') - # should not write any files - self.assertEqual(os.listdir(self.workdir.name), ['etc']) - self.assert_nm_udev(None) - - def test_file_args(self): - conf = os.path.join(self.workdir.name, 'config') - with open(conf, 'w') as f: - f.write('network: {}') - # when specifying custom files, it should ignore the global config - self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp4: true''', extra_args=[conf]) - self.assertEqual(set(os.listdir(self.workdir.name)), {'config', 'etc'}) - - def test_file_args_notfound(self): - err = self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp4: true''', expect_fail=True, extra_args=['/non/existing/config']) - self.assertEqual(err, 'Cannot open /non/existing/config: No such file or directory\n') - self.assertEqual(os.listdir(self.workdir.name), ['etc']) - - def test_help(self): - conf = os.path.join(self.workdir.name, 'etc', 'netplan', 'a.yaml') - os.makedirs(os.path.dirname(conf)) - with open(conf, 'w') as f: - f.write('''network: - version: 2 - ethernets: - eth0: - dhcp4: true''') - - p = subprocess.Popen([exe_generate, '--root-dir', self.workdir.name, '--help'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) - (out, err) = p.communicate() - self.assertEqual(err, '') - self.assertEqual(p.returncode, 0) - self.assertIn('Usage:', out) - self.assertEqual(os.listdir(self.workdir.name), ['etc']) - - def test_unknown_cli_args(self): - p = subprocess.Popen([exe_generate, '--foo'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) - (out, err) = p.communicate() - self.assertIn('nknown option --foo', err) - self.assertNotEqual(p.returncode, 0) - - def test_output_mkdir_error(self): - conf = os.path.join(self.workdir.name, 'config') - with open(conf, 'w') as f: - f.write('''network: - version: 2 - ethernets: - eth0: - dhcp4: true''') - err = self.generate('', extra_args=['--root-dir', '/proc/foo', conf], expect_fail=True) - self.assertIn('cannot create directory /proc/foo/run/systemd/network', err) - - def test_systemd_generator(self): - conf = os.path.join(self.confdir, 'a.yaml') - os.makedirs(os.path.dirname(conf)) - with open(conf, 'w') as f: - f.write('''network: - version: 2 - ethernets: - eth0: - dhcp4: true''') - outdir = os.path.join(self.workdir.name, 'out') - os.mkdir(outdir) - - generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') - os.makedirs(os.path.dirname(generator)) - os.symlink(exe_generate, generator) - - subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) - self.assertEqual(set(os.listdir(outdir)), - {'netplan.stamp', 'multi-user.target.wants', 'network-online.target.wants'}) - n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') - self.assertTrue(os.path.exists(n)) - os.unlink(n) - - # should auto-enable networkd and -wait-online - self.assertTrue(os.path.islink(os.path.join( - outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) - self.assertTrue(os.path.islink(os.path.join( - outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) - - # should be a no-op the second time while the stamp exists - out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], - stderr=subprocess.STDOUT) - self.assertFalse(os.path.exists(n)) - self.assertIn(b'netplan generate already ran', out) - - # after removing the stamp it generates again, and not trip over the - # existing enablement symlink - os.unlink(os.path.join(outdir, 'netplan.stamp')) - subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) - self.assertTrue(os.path.exists(n)) - - def test_systemd_generator_noconf(self): - outdir = os.path.join(self.workdir.name, 'out') - os.mkdir(outdir) - - generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') - os.makedirs(os.path.dirname(generator)) - os.symlink(exe_generate, generator) - - subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) - # no enablement symlink here - self.assertEqual(os.listdir(outdir), ['netplan.stamp']) - - def test_systemd_generator_badcall(self): - outdir = os.path.join(self.workdir.name, 'out') - os.mkdir(outdir) - - generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') - os.makedirs(os.path.dirname(generator)) - os.symlink(exe_generate, generator) - - try: - subprocess.check_output([generator, '--root-dir', self.workdir.name], - stderr=subprocess.STDOUT) - self.fail("direct systemd generator call is expected to fail, but succeeded.") - except subprocess.CalledProcessError as e: - self.assertEqual(e.returncode, 1) - self.assertIn(b'can not be called directly', e.output) - - -class TestNetworkd(TestBase): - '''networkd output''' - - def test_eth_optional(self): - self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp6: true - optional: true''') - self.assert_networkd({'eth0.network': '''[Match] -Name=eth0 - -[Link] -RequiredForOnline=no - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - 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 - ethernets: - eth0: - wakeonlan: true - dhcp4: n''') - - self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) - self.assert_networkd_udev(None) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:eth0,''') - self.assert_nm_udev(None) - # should not allow NM to manage everything - self.assertFalse(os.path.exists(self.nm_enable_all_conf)) - - def test_eth_mtu(self): - self.generate('''network: - version: 2 - ethernets: - eth1: - mtu: 1280 - dhcp4: n''') - - self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) - self.assert_networkd_udev(None) - - def test_mtu_all(self): - self.generate(textwrap.dedent(""" - network: - version: 2 - ethernets: - eth1: - mtu: 1280 - dhcp4: n - bonds: - bond0: - interfaces: - - eth1 - mtu: 9000 - vlans: - bond0.108: - link: bond0 - 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] -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]\nLinkLocalAddressing=no\nBond=bond0\n' - }) - self.assert_networkd_udev(None) - - def test_eth_match_by_driver_rename(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - driver: ixgbe - set-name: lom1''') - - self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) - self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) - # NM cannot match by driver, so blacklisting needs to happen via udev - self.assert_nm(None, None) - self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') - - def test_eth_match_by_mac_rename(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - macaddress: 11:22:33:44:55:66 - set-name: lom1''') - - self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) - self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=mac:11:22:33:44:55:66,''') - self.assert_nm_udev(None) - - def test_eth_implicit_name_match_dhcp4(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: y''') - - self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) - self.assert_networkd_udev(None) - - def test_eth_match_dhcp4(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - driver: ixgbe - dhcp4: true''') - - self.assert_networkd({'def1.network': '''[Match] -Driver=ixgbe - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - self.assert_networkd_udev(None) - self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') - - def test_eth_match_name(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - name: green - dhcp4: true''') - - self.assert_networkd({'def1.network': ND_DHCP4 % 'green'}) - self.assert_networkd_udev(None) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:green,''') - self.assert_nm_udev(None) - - def test_eth_set_mac(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - name: green - macaddress: 00:01:02:03:04:05 - dhcp4: true''') - - self.assert_networkd({'def1.network': ND_DHCP4 % 'green', - 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' - }) - self.assert_networkd_udev(None) - - def test_eth_match_name_rename(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - name: green - set-name: blue - dhcp4: true''') - - # the .network needs to match on the renamed name - self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n', - 'def1.network': ND_DHCP4 % 'blue'}) - - # The udev rules engine does support renaming by name - self.assert_networkd_udev(None) - - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:blue,''') - - def test_eth_match_all_names(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: {name: "*"} - dhcp4: true''') - - self.assert_networkd({'def1.network': ND_DHCP4 % '*'}) - self.assert_networkd_udev(None) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:*,''') - self.assert_nm_udev(None) - - def test_eth_match_all(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: {} - dhcp4: true''') - - 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] -# devices managed by networkd -unmanaged-devices+=type:ethernet,''') - self.assert_nm_udev(None) - - def test_match_multiple(self): - self.generate('''network: - version: 2 - ethernets: - def1: - match: - name: en1s* - macaddress: 00:11:22:33:44:55 - dhcp4: on''') - self.assert_networkd({'def1.network': '''[Match] -MACAddress=00:11:22:33:44:55 -Name=en1s* - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=mac:00:11:22:33:44:55,''') - - def test_eth_global_renderer(self): - self.generate('''network: - version: 2 - renderer: networkd - ethernets: - eth0: - dhcp4: true''') - - self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:eth0,''') - self.assert_nm_udev(None) - # should not allow NM to manage everything - self.assertFalse(os.path.exists(self.nm_enable_all_conf)) - - def test_eth_type_renderer(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - renderer: networkd - eth0: - dhcp4: true''') - - self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:eth0,''') - # should allow NM to manage everything else - self.assertTrue(os.path.exists(self.nm_enable_all_conf)) - self.assert_nm_udev(None) - - def test_bridge_set_mac(self): - self.generate('''network: - version: 2 - bridges: - br0: - macaddress: 00:01:02:03:04:05 - dhcp4: true''') - - 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): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - renderer: NetworkManager - eth0: - renderer: networkd - dhcp4: true''') - - self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) - self.assert_networkd_udev(None) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:eth0,''') - self.assert_nm_udev(None) - - def test_eth_dhcp6(self): - self.generate('''network: - version: 2 - ethernets: - eth0: {dhcp6: true}''') - self.assert_networkd({'eth0.network': ND_DHCP6 % 'eth0'}) - - def test_eth_dhcp6_no_accept_ra(self): - self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp6: true - accept-ra: n''') - self.assert_networkd({'eth0.network': '''[Match] -Name=eth0 - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 -IPv6AcceptRA=no - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - - def test_bridge_dhcp6_no_accept_ra(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: no - dhcp6: no - accept-ra: no - bridges: - br0: - interfaces: [engreen] - dhcp6: true - accept-ra: no''') - self.assert_networkd({'br0.network': '''[Match] -Name=br0 - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 -IPv6AcceptRA=no -ConfigureWithoutCarrier=yes - -[DHCP] -UseMTU=true -RouteMetric=100 -''', - 'br0.netdev': '''[NetDev] -Name=br0 -Kind=bridge -''', - 'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=no -IPv6AcceptRA=no -Bridge=br0 -'''}) - - def test_bond_dhcp6_no_accept_ra(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp6: no - accept-ra: no - bonds: - bond0: - interfaces: [engreen] - dhcp6: true - accept-ra: yes''') - self.assert_networkd({'bond0.network': '''[Match] -Name=bond0 - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 -IPv6AcceptRA=yes -ConfigureWithoutCarrier=yes - -[DHCP] -UseMTU=true -RouteMetric=100 -''', - 'bond0.netdev': '''[NetDev] -Name=bond0 -Kind=bond -''', - 'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=no -IPv6AcceptRA=no -Bond=bond0 -'''}) - - def test_eth_dhcp6_accept_ra(self): - self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp6: true - accept-ra: yes''') - self.assert_networkd({'eth0.network': '''[Match] -Name=eth0 - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 -IPv6AcceptRA=yes - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - - def test_eth_dhcp6_accept_ra_unset(self): - self.generate('''network: - version: 2 - ethernets: - eth0: - dhcp6: true''') - self.assert_networkd({'eth0.network': '''[Match] -Name=eth0 - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - - def test_eth_dhcp4_and_6(self): - self.generate('''network: - version: 2 - ethernets: - eth0: {dhcp4: true, dhcp6: true}''') - self.assert_networkd({'eth0.network': ND_DHCPYES % 'eth0'}) - - def test_eth_manual_addresses(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 -Address=2001:FFfe::1/64 -'''}) - - def test_eth_manual_addresses_dhcp(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: yes - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 -Address=2001:FFfe::1/64 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - - def test_dhcp_critical_true(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: yes - critical: yes -''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -CriticalConnection=true -'''}) - - def test_dhcp_identifier_mac(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: yes - dhcp-identifier: mac -''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -ClientIdentifier=mac -'''}) - - def test_dhcp_identifier_duid(self): - # This option should be silently ignored, since it's the default - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: yes - dhcp-identifier: duid -''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - - def test_route_v4_single(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -Metric=100 -'''}) - - def test_route_v4_multiple(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 8.8.0.0/16 - via: 192.168.1.1 - - to: 10.10.10.8 - via: 192.168.1.2 - metric: 5000 - - to: 11.11.11.0/24 - via: 192.168.1.3 - metric: 9999 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=8.8.0.0/16 -Gateway=192.168.1.1 - -[Route] -Destination=10.10.10.8 -Gateway=192.168.1.2 -Metric=5000 - -[Route] -Destination=11.11.11.0/24 -Gateway=192.168.1.3 -Metric=9999 -'''}) - - def test_route_v4_onlink(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - on-link: true - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -GatewayOnlink=true -Metric=100 -'''}) - - def test_route_v4_onlink_no(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - on-link: n - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -Metric=100 -'''}) - - def test_route_v4_scope(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - scope: link - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Scope=link -Metric=100 -'''}) - - def test_route_v4_scope_redefine(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - scope: host - via: 192.168.14.20 - scope: link - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -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 -'''}) - - def test_route_v4_type_blackhole(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - type: blackhole - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -Type=blackhole -Metric=100 -'''}) - - def test_route_v4_type_redefine(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - type: prohibit - via: 192.168.14.20 - type: unicast - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -Metric=100 -'''}) - - def test_route_v4_table(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - table: 201 - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -Metric=100 -Table=201 -'''}) - - def test_route_v4_from(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - from: 192.168.14.2 - metric: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=10.10.10.0/24 -Gateway=192.168.14.20 -PreferredSource=192.168.14.2 -Metric=100 -'''}) - - def test_route_v6_single(self): - self.generate('''network: - version: 2 - ethernets: - enblue: - addresses: ["192.168.1.3/24"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1''') - - self.assert_networkd({'enblue.network': '''[Match] -Name=enblue - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.1.3/24 - -[Route] -Destination=2001:dead:beef::2/64 -Gateway=2001:beef:beef::1 -'''}) - - def test_route_v6_type(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1 - type: prohibit''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=2001:dead:beef::2/64 -Gateway=2001:beef:beef::1 -Type=prohibit -'''}) - - def test_route_v6_scope_host(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1 - scope: host''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[Route] -Destination=2001:dead:beef::2/64 -Gateway=2001:beef:beef::1 -Scope=host -'''}) - - def test_route_v6_multiple(self): - self.generate('''network: - version: 2 - ethernets: - enblue: - addresses: ["192.168.1.3/24"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1 - - to: 2001:f00f:f00f::fe/64 - via: 2001:beef:feed::1 - metric: 1024''') - - self.assert_networkd({'enblue.network': '''[Match] -Name=enblue - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.1.3/24 - -[Route] -Destination=2001:dead:beef::2/64 -Gateway=2001:beef:beef::1 - -[Route] -Destination=2001:f00f:f00f::fe/64 -Gateway=2001:beef:feed::1 -Metric=1024 -'''}) - - def test_wifi(self): - self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - "Joe's Home": - password: "s3kr1t" - workplace: - password: "c0mpany" - peer2peer: - mode: adhoc - dhcp4: yes''') - - self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:wl0,''') - self.assert_nm_udev(None) - - # generates wpa config and enables wpasupplicant unit - with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: - self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant - -network={ - ssid="Joe's Home" - psk="s3kr1t" -} -network={ - ssid="workplace" - psk="c0mpany" -} -network={ - ssid="peer2peer" - key_mgmt=NONE - mode=1 -} -''') - self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) - self.assertTrue(os.path.islink(os.path.join( - self.workdir.name, 'run/systemd/system/multi-user.target.wants/netplan-wpa@wl0.service'))) - - def test_wifi_route(self): - self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - workplace: - password: "c0mpany" - dhcp4: yes - routes: - - to: 10.10.10.0/24 - via: 8.8.8.8''') - - self.assert_networkd({'wl0.network': '''[Match] -Name=wl0 - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 - -[Route] -Destination=10.10.10.0/24 -Gateway=8.8.8.8 - -[DHCP] -UseMTU=true -RouteMetric=600 -'''}) - - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:wl0,''') - self.assert_nm_udev(None) - - def test_wifi_match(self): - err = self.generate('''network: - version: 2 - wifis: - somewifi: - match: - driver: foo - access-points: - workplace: - password: "c0mpany" - dhcp4: yes''', expect_fail=True) - self.assertIn('networkd backend does not support wifi with match:', err) - - def test_wifi_ap(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - workplace: - password: "c0mpany" - mode: ap - dhcp4: yes''', expect_fail=True) - self.assertIn('networkd does not support wifi in access point mode', err) - - def test_bridge_empty(self): - self.generate('''network: - version: 2 - bridges: - br0: - dhcp4: true''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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,''') - self.assert_nm_udev(None) - - def test_bridge_type_renderer(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - bridges: - renderer: networkd - br0: - dhcp4: true''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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,''') - self.assert_nm_udev(None) - - def test_bridge_def_renderer(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - bridges: - renderer: NetworkManager - br0: - renderer: networkd - addresses: [1.2.3.4/12] - dhcp4: true''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - 'br0.network': '''[Match] -Name=br0 - -[Network] -DHCP=ipv4 -LinkLocalAddressing=ipv6 -Address=1.2.3.4/12 -ConfigureWithoutCarrier=yes - -[DHCP] -UseMTU=true -RouteMetric=100 -'''}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:br0,''') - self.assert_nm_udev(None) - - def test_bridge_forward_declaration(self): - self.generate('''network: - version: 2 - bridges: - br0: - interfaces: [eno1, switchports] - dhcp4: true - ethernets: - eno1: {} - switchports: - match: - driver: yayroute -''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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]\nLinkLocalAddressing=no\nBridge=br0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) - - def test_eth_bridge_nm_blacklist(self): - self.generate('''network: - renderer: networkd - ethernets: - eth42: - dhcp4: yes - ethbr: - match: {name: eth43} - bridges: - mybr: - interfaces: [ethbr] - dhcp4: yes''') - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:eth42,interface-name:eth43,interface-name:mybr,''') - - def test_bridge_components(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bridges: - br0: - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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]\nLinkLocalAddressing=no\nBridge=br0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) - - def test_bridge_params(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bridges: - br0: - interfaces: [eno1, switchports] - parameters: - ageing-time: 50 - forward-delay: 12 - hello-time: 6 - max-age: 24 - priority: 1000 - stp: true - path-cost: - eno1: 70 - port-priority: - eno1: 14 - dhcp4: true''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' - '[Bridge]\nAgeingTimeSec=50\n' - 'Priority=1000\n' - 'ForwardDelaySec=12\n' - 'HelloTimeSec=6\n' - 'MaxAgeSec=24\n' - 'STP=true\n', - '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]\nLinkLocalAddressing=no\nBridge=br0\n\n' - '[Bridge]\nCost=70\nPriority=14\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) - - def test_bond_empty(self): - self.generate('''network: - version: 2 - bonds: - bn0: - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', - '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,''') - self.assert_nm_udev(None) - - def test_bond_components(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bonds: - bn0: - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\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]\nLinkLocalAddressing=no\nBond=bn0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) - - def test_bond_empty_parameters(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bonds: - bn0: - parameters: {} - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\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]\nLinkLocalAddressing=no\nBond=bn0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) - - def test_bond_with_parameters(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bonds: - bn0: - parameters: - mode: 802.1ad - lacp-rate: 10 - mii-monitor-interval: 10 - min-links: 10 - up-delay: 20 - down-delay: 30 - all-slaves-active: true - transmit-hash-policy: none - ad-select: none - arp-interval: 15 - arp-validate: all - arp-all-targets: all - fail-over-mac-policy: none - gratuitious-arp: 10 - packets-per-slave: 10 - primary-reselect-policy: none - resend-igmp: 10 - learn-packet-interval: 10 - arp-ip-targets: - - 10.10.10.10 - - 20.20.20.20 - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' - '[Bond]\n' - 'Mode=802.1ad\n' - 'LACPTransmitRate=10\n' - 'MIIMonitorSec=10ms\n' - 'MinLinks=10\n' - 'TransmitHashPolicy=none\n' - 'AdSelect=none\n' - 'AllSlavesActive=1\n' - 'ARPIntervalSec=15ms\n' - 'ARPIPTargets=10.10.10.10,20.20.20.20\n' - 'ARPValidate=all\n' - 'ARPAllTargets=all\n' - 'UpDelaySec=20ms\n' - 'DownDelaySec=30ms\n' - 'FailOverMACPolicy=none\n' - 'GratuitousARP=10\n' - 'PacketsPerSlave=10\n' - 'PrimaryReselectPolicy=none\n' - 'ResendIGMP=10\n' - 'LearnPacketIntervalSec=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]\nLinkLocalAddressing=no\nBond=bn0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) - - def test_bond_with_parameters_all_suffix(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bonds: - bn0: - parameters: - mode: 802.1ad - mii-monitor-interval: 10ms - up-delay: 20ms - down-delay: 30s - arp-interval: 15m - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' - '[Bond]\n' - 'Mode=802.1ad\n' - 'MIIMonitorSec=10ms\n' - 'ARPIntervalSec=15m\n' - 'UpDelaySec=20ms\n' - 'DownDelaySec=30s\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]\nLinkLocalAddressing=no\nBond=bn0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) - - def test_bond_primary_slave(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute - bonds: - bn0: - parameters: - mode: active-backup - primary: eno1 - interfaces: [eno1, switchports] - dhcp4: true''') - - self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' - '[Bond]\n' - 'Mode=active-backup\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]\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]\nLinkLocalAddressing=no\nBond=bn0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) - - def test_gateway(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] - gateway4: 192.168.14.1 - gateway6: 2001:FFfe::2''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 -Address=2001:FFfe::1/64 -Gateway=192.168.14.1 -Gateway=2001:FFfe::2 -'''}) - - def test_nameserver(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - nameservers: - addresses: [1.2.3.4, "1234::FFFF"] - enblue: - addresses: ["192.168.1.3/24"] - nameservers: - search: [lab, kitchen] - addresses: [8.8.8.8]''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 -DNS=1.2.3.4 -DNS=1234::FFFF -''', - 'enblue.network': '''[Match] -Name=enblue - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.1.3/24 -DNS=8.8.8.8 -Domains=lab kitchen -'''}) - - def test_vlan(self): - self.generate('''network: - version: 2 - ethernets: - en1: {} - vlans: - enblue: - id: 1 - link: en1 - addresses: [1.2.3.4/24] - enred: - id: 3 - link: en1 - macaddress: aa:bb:cc:dd:ee:11 - engreen: {id: 2, link: en1, dhcp6: true}''') - - 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,''') - self.assert_nm_udev(None) - - def test_ip_rule_table(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routing-policy: - - to: 10.10.10.0/24 - table: 100 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[RoutingPolicyRule] -To=10.10.10.0/24 -Table=100 -'''}) - - def test_ip_rule_priority(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routing-policy: - - to: 10.10.10.0/24 - priority: 99 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[RoutingPolicyRule] -To=10.10.10.0/24 -Priority=99 -'''}) - - def test_ip_rule_fwmark(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routing-policy: - - from: 10.10.10.0/24 - mark: 50 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[RoutingPolicyRule] -From=10.10.10.0/24 -FirewallMark=50 -'''}) - - def test_ip_rule_tos(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routing-policy: - - to: 10.10.10.0/24 - type-of-service: 250 - ''') - - self.assert_networkd({'engreen.network': '''[Match] -Name=engreen - -[Network] -LinkLocalAddressing=ipv6 -Address=192.168.14.2/24 - -[RoutingPolicyRule] -To=10.10.10.0/24 -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): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth0: - wakeonlan: true''') - - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=1 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -'''}) - # should allow NM to manage everything else - self.assertTrue(os.path.exists(self.nm_enable_all_conf)) - self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) - self.assert_nm_udev(None) - - def test_eth_mtu(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth1: - mtu: 1280 - dhcp4: n''') - - self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) - self.assert_nm({'eth1': '''[connection] -id=netplan-eth1 -type=ethernet -interface-name=eth1 - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -mtu=1280 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -'''}) - - def test_mtu_all(self): - self.generate(textwrap.dedent(""" - network: - version: 2 - renderer: NetworkManager - ethernets: - eth1: - mtu: 1280 - dhcp4: n - bonds: - bond0: - interfaces: - - eth1 - mtu: 9000 - vlans: - bond0.108: - link: bond0 - id: 108""")) - self.assert_nm({ - 'bond0.108': '''[connection] -id=netplan-bond0.108 -type=vlan -interface-name=bond0.108 - -[vlan] -id=108 -parent=bond0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'bond0': '''[connection] -id=netplan-bond0 -type=bond -interface-name=bond0 - -[802-3-ethernet] -mtu=9000 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'eth1': '''[connection] -id=netplan-eth1 -type=ethernet -interface-name=eth1 -slave-type=bond -master=bond0 - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -mtu=1280 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - }) - - def test_eth_set_mac(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth0: - macaddress: 00:01:02:03:04:05 - dhcp4: true''') - - self.assert_networkd({'eth0.link': '''[Match] -OriginalName=eth0 - -[Link] -WakeOnLan=off -MACAddress=00:01:02:03:04:05 -'''}) - - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -cloned-mac-address=00:01:02:03:04:05 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - - def test_eth_match_by_driver(self): - err = self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - driver: ixgbe''', expect_fail=True) - self.assertIn('NetworkManager definitions do not support matching by driver', err) - - def test_eth_match_by_driver_rename(self): - # in this case udev will rename the device so that NM can use the name - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - driver: ixgbe - set-name: lom1''') - - self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) - self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet -interface-name=lom1 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -'''}) - self.assert_nm_udev(None) - - def test_eth_match_by_mac_rename(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - macaddress: 11:22:33:44:55:66 - set-name: lom1''') - - self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) - self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet -interface-name=lom1 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -'''}) - self.assert_nm_udev(None) - - def test_eth_implicit_name_match_dhcp4(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - dhcp4: true''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - - def test_eth_match_mac_dhcp4(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - macaddress: 11:22:33:44:55:66 - dhcp4: true''') - - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -mac-address=11:22:33:44:55:66 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - - def test_eth_match_name(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - name: green - dhcp4: true''') - - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet -interface-name=green - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_match_name_rename(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - name: green - set-name: blue - dhcp4: true''') - - # The udev rules engine does support renaming by name - self.assert_networkd_udev(None) - - # NM needs to match on the renamed name - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet -interface-name=blue - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - # ... while udev renames it - self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n'}) - self.assert_nm_udev(None) - - def test_eth_match_name_glob(self): - err = self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: {name: "en*"} - dhcp4: true''', expect_fail=True) - self.assertIn('def1: NetworkManager definitions do not support name globbing', err) - - self.assert_nm({}) - self.assert_networkd({}) - - def test_eth_match_all(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: {} - dhcp4: true''') - - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - - def test_match_multiple(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - def1: - match: - name: engreen - macaddress: 00:11:22:33:44:55 - dhcp4: yes''') - self.assert_nm({'def1': '''[connection] -id=netplan-def1 -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -mac-address=00:11:22:33:44:55 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_global_renderer(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth0: - dhcp4: true''') - - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_type_renderer(self): - self.generate('''network: - version: 2 - renderer: networkd - ethernets: - renderer: NetworkManager - eth0: - dhcp4: true''') - - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_def_renderer(self): - self.generate('''network: - version: 2 - renderer: networkd - ethernets: - renderer: networkd - eth0: - renderer: NetworkManager''') - - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_global_renderer_only(self): - self.generate(None, confs={'01-default-nm.yaml': 'network: {version: 2, renderer: NetworkManager}'}) - # should allow NM to manage everything else - self.assertTrue(os.path.exists(self.nm_enable_all_conf)) - # but not configure anything else - self.assert_nm(None, None) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_dhcp6(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth0: {dhcp6: true}''') - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=auto -'''}) - - def test_eth_dhcp4_and_6(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eth0: {dhcp4: true, dhcp6: true}''') - self.assert_nm({'eth0': '''[connection] -id=netplan-eth0 -type=ethernet -interface-name=eth0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=auto -'''}) - - def test_eth_manual_addresses(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: - - 192.168.14.2/24 - - 172.16.0.4/16 - - 2001:FFfe::1/64''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -address2=172.16.0.4/16 - -[ipv6] -method=manual -address1=2001:FFfe::1/64 -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_eth_manual_addresses_dhcp(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - dhcp4: yes - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto -address1=192.168.14.2/24 - -[ipv6] -method=manual -address1=2001:FFfe::1/64 -'''}) - - def test_route_v4_single(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 10.10.10.0/24 - via: 192.168.14.20 - metric: 100 - ''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -route1=10.10.10.0/24,192.168.14.20,100 - -[ipv6] -method=ignore -'''}) - - def test_route_v4_multiple(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - routes: - - to: 8.8.0.0/16 - via: 192.168.1.1 - metric: 5000 - - to: 10.10.10.8 - via: 192.168.1.2 - - to: 11.11.11.0/24 - via: 192.168.1.3 - metric: 9999 - ''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -route1=8.8.0.0/16,192.168.1.1,5000 -route2=10.10.10.8,192.168.1.2 -route3=11.11.11.0/24,192.168.1.3,9999 - -[ipv6] -method=ignore -'''}) - - def test_route_v6_single(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - enblue: - addresses: ["2001:f00f:f00f::2/64"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1''') - - self.assert_nm({'enblue': '''[connection] -id=netplan-enblue -type=ethernet -interface-name=enblue - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=manual -address1=2001:f00f:f00f::2/64 -route1=2001:dead:beef::2/64,2001:beef:beef::1 -'''}) - - def test_route_v6_multiple(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - enblue: - addresses: ["2001:f00f:f00f::2/64"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1 - - to: 2001:dead:feed::2/64 - via: 2001:beef:beef::2 - metric: 1000''') - - self.assert_nm({'enblue': '''[connection] -id=netplan-enblue -type=ethernet -interface-name=enblue - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=manual -address1=2001:f00f:f00f::2/64 -route1=2001:dead:beef::2/64,2001:beef:beef::1 -route2=2001:dead:feed::2/64,2001:beef:beef::2,1000 -'''}) - - def test_routes_mixed(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: ["192.168.14.2/24", "2001:f00f::2/128"] - routes: - - to: 2001:dead:beef::2/64 - via: 2001:beef:beef::1 - metric: 997 - - to: 8.8.0.0/16 - via: 192.168.1.1 - metric: 5000 - - to: 10.10.10.8 - via: 192.168.1.2 - - to: 11.11.11.0/24 - via: 192.168.1.3 - metric: 9999 - - to: 2001:f00f:f00f::fe/64 - via: 2001:beef:feed::1 - ''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -route1=8.8.0.0/16,192.168.1.1,5000 -route2=10.10.10.8,192.168.1.2 -route3=11.11.11.0/24,192.168.1.3,9999 - -[ipv6] -method=manual -address1=2001:f00f::2/128 -route1=2001:dead:beef::2/64,2001:beef:beef::1,997 -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 - renderer: NetworkManager - wifis: - wl0: - access-points: - "Joe's Home": - password: "s3kr1t" - workplace: - password: "c0mpany" - dhcp4: yes''') - - self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] -id=netplan-wl0-Joe's Home -type=wifi -interface-name=wl0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore - -[wifi] -ssid=Joe's Home -mode=infrastructure - -[wifi-security] -key-mgmt=wpa-psk -psk=s3kr1t -''', - 'wl0-workplace': '''[connection] -id=netplan-wl0-workplace -type=wifi -interface-name=wl0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=auto - -[ipv6] -method=ignore - -[wifi] -ssid=workplace -mode=infrastructure - -[wifi-security] -key-mgmt=wpa-psk -psk=c0mpany -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_wifi_match_mac(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - wifis: - all: - match: - macaddress: 11:22:33:44:55:66 - access-points: - workplace: {}''') - - self.assert_nm({'all-workplace': '''[connection] -id=netplan-all-workplace -type=wifi - -[ethernet] -wake-on-lan=0 - -[802-11-wireless] -mac-address=11:22:33:44:55:66 - -[ipv4] -method=link-local - -[ipv6] -method=ignore - -[wifi] -ssid=workplace -mode=infrastructure -'''}) - - def test_wifi_match_all(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - wifis: - all: - match: {} - access-points: - workplace: {mode: infrastructure}''') - - self.assert_nm({'all-workplace': '''[connection] -id=netplan-all-workplace -type=wifi - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore - -[wifi] -ssid=workplace -mode=infrastructure -'''}) - - def test_wifi_ap(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - wifis: - wl0: - access-points: - homenet: - mode: ap - password: s3cret''') - - self.assert_nm({'wl0-homenet': '''[connection] -id=netplan-wl0-homenet -type=wifi -interface-name=wl0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=shared - -[ipv6] -method=ignore - -[wifi] -ssid=homenet -mode=ap - -[wifi-security] -key-mgmt=wpa-psk -psk=s3cret -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_wifi_adhoc(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - wifis: - wl0: - access-points: - homenet: - mode: adhoc''') - - self.assert_nm({'wl0-homenet': '''[connection] -id=netplan-wl0-homenet -type=wifi -interface-name=wl0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore - -[wifi] -ssid=homenet -mode=adhoc -'''}) - - def test_bridge_empty(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - bridges: - br0: - dhcp4: true''') - - self.assert_nm({'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bridge_type_renderer(self): - self.generate('''network: - version: 2 - renderer: networkd - bridges: - renderer: NetworkManager - br0: - dhcp4: true''') - - self.assert_nm({'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bridge_set_mac(self): - self.generate('''network: - version: 2 - bridges: - renderer: NetworkManager - br0: - macaddress: 00:01:02:03:04:05 - dhcp4: true''') - - self.assert_nm({'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[802-3-ethernet] -cloned-mac-address=00:01:02:03:04:05 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - - def test_bridge_def_renderer(self): - self.generate('''network: - version: 2 - renderer: networkd - bridges: - renderer: networkd - br0: - renderer: NetworkManager - addresses: [1.2.3.4/12] - dhcp4: true''') - - self.assert_nm({'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[ipv4] -method=auto -address1=1.2.3.4/12 - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bridge_forward_declaration(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - bridges: - br0: - interfaces: [eno1, switchport] - dhcp4: true - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 -''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bridge -master=br0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bridge -master=br0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bridge_components(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bridges: - br0: - interfaces: [eno1, switchport] - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bridge -master=br0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bridge -master=br0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bridge_params(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bridges: - br0: - interfaces: [eno1, switchport] - parameters: - ageing-time: 50 - priority: 1000 - forward-delay: 12 - hello-time: 6 - max-age: 24 - path-cost: - eno1: 70 - port-priority: - eno1: 61 - stp: true - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bridge -master=br0 - -[bridge-port] -path-cost=70 -priority=61 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bridge -master=br0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'br0': '''[connection] -id=netplan-br0 -type=bridge -interface-name=br0 - -[bridge] -ageing-time=50 -priority=1000 -forward-delay=12 -hello-time=6 -max-age=24 -stp=true - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bond_empty(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - bonds: - bn0: - dhcp4: true''') - - self.assert_nm({'bn0': '''[connection] -id=netplan-bn0 -type=bond -interface-name=bn0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - - def test_bond_components(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bonds: - bn0: - interfaces: [eno1, switchport] - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'bn0': '''[connection] -id=netplan-bn0 -type=bond -interface-name=bn0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bond_empty_params(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bonds: - bn0: - interfaces: [eno1, switchport] - parameters: {} - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'bn0': '''[connection] -id=netplan-bn0 -type=bond -interface-name=bn0 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bond_with_params(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bonds: - bn0: - interfaces: [eno1, switchport] - parameters: - mode: 802.1ad - lacp-rate: 10 - mii-monitor-interval: 10 - min-links: 10 - up-delay: 10 - down-delay: 10 - all-slaves-active: true - transmit-hash-policy: none - ad-select: none - arp-interval: 10 - arp-validate: all - arp-all-targets: all - arp-ip-targets: - - 10.10.10.10 - - 20.20.20.20 - fail-over-mac-policy: none - gratuitious-arp: 10 - packets-per-slave: 10 - primary-reselect-policy: none - resend-igmp: 10 - learn-packet-interval: 10 - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'bn0': '''[connection] -id=netplan-bn0 -type=bond -interface-name=bn0 - -[bond] -mode=802.1ad -lacp_rate=10 -miimon=10 -min_links=10 -xmit_hash_policy=none -ad_select=none -all_slaves_active=1 -arp_interval=10 -arp_ip_target=10.10.10.10,20.20.20.20 -arp_validate=all -arp_all_targets=all -updelay=10 -downdelay=10 -fail_over_mac=none -num_grat_arp=10 -num_unsol_na=10 -packets_per_slave=10 -primary_reselect=none -resend_igmp=10 -lp_interval=10 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_bond_primary_slave(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - eno1: {} - switchport: - match: - name: enp2s1 - bonds: - bn0: - interfaces: [eno1, switchport] - parameters: - mode: active-backup - primary: eno1 - dhcp4: true''') - - self.assert_nm({'eno1': '''[connection] -id=netplan-eno1 -type=ethernet -interface-name=eno1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'switchport': '''[connection] -id=netplan-switchport -type=ethernet -interface-name=enp2s1 -slave-type=bond -master=bn0 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'bn0': '''[connection] -id=netplan-bn0 -type=bond -interface-name=bn0 - -[bond] -mode=active-backup -primary=eno1 - -[ipv4] -method=auto - -[ipv6] -method=ignore -'''}) - self.assert_networkd({}) - self.assert_nm_udev(None) - - def test_gateway(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] - gateway4: 192.168.14.1 - gateway6: 2001:FFfe::2''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -gateway=192.168.14.1 - -[ipv6] -method=manual -address1=2001:FFfe::1/64 -gateway=2001:FFfe::2 -'''}) - - def test_nameserver(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - addresses: ["192.168.14.2/24"] - nameservers: - addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"] - search: [lab, kitchen] - enblue: - addresses: ["192.168.1.3/24"] - nameservers: - addresses: [8.8.8.8]''') - - self.assert_nm({'engreen': '''[connection] -id=netplan-engreen -type=ethernet -interface-name=engreen - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.14.2/24 -dns=1.2.3.4;2.3.4.5; -dns-search=lab;kitchen; - -[ipv6] -method=manual -dns=1234::FFFF; -dns-search=lab;kitchen; -''', - 'enblue': '''[connection] -id=netplan-enblue -type=ethernet -interface-name=enblue - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=manual -address1=192.168.1.3/24 -dns=8.8.8.8; - -[ipv6] -method=ignore -'''}) - - def test_vlan(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - en1: {} - vlans: - enblue: - id: 1 - link: en1 - addresses: [1.2.3.4/24] - engreen: {id: 2, link: en1, dhcp6: true}''') - - self.assert_networkd({}) - self.assert_nm({'en1': '''[connection] -id=netplan-en1 -type=ethernet -interface-name=en1 - -[ethernet] -wake-on-lan=0 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''', - 'enblue': '''[connection] -id=netplan-enblue -type=vlan -interface-name=enblue - -[vlan] -id=1 -parent=en1 - -[ipv4] -method=manual -address1=1.2.3.4/24 - -[ipv6] -method=ignore -''', - 'engreen': '''[connection] -id=netplan-engreen -type=vlan -interface-name=engreen - -[vlan] -id=2 -parent=en1 - -[ipv4] -method=link-local - -[ipv6] -method=auto -'''}) - self.assert_nm_udev(None) - - def test_vlan_parent_match(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - en-v: - match: {macaddress: "11:22:33:44:55:66"} - vlans: - engreen: {id: 2, link: en-v, dhcp4: true}''') - - self.assert_networkd({}) - - # get assigned UUID from en-v connection - with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-en-v')) as f: - m = re.search('uuid=([0-9a-fA-F-]{36})\n', f.read()) - self.assertTrue(m) - uuid = m.group(1) - self.assertNotEquals(uuid, "00000000-0000-0000-0000-000000000000") - - self.assert_nm({'en-v': '''[connection] -id=netplan-en-v -type=ethernet -uuid=%s - -[ethernet] -wake-on-lan=0 - -[802-3-ethernet] -mac-address=11:22:33:44:55:66 - -[ipv4] -method=link-local - -[ipv6] -method=ignore -''' % uuid, - 'engreen': '''[connection] -id=netplan-engreen -type=vlan -interface-name=engreen - -[vlan] -id=2 -parent=%s - -[ipv4] -method=auto - -[ipv6] -method=ignore -''' % uuid}) - self.assert_nm_udev(None) - - -class TestConfigErrors(TestBase): - def test_malformed_yaml(self): - err = self.generate('network:\n version: 2\n foo: *', expect_fail=True) - self.assertIn('Invalid YAML', err) - self.assertIn('/a.yaml line 2 column 1: did not find expected key', err) - - def test_yaml_expected_scalar(self): - err = self.generate('network:\n version: {}', expect_fail=True) - self.assertIn('expected scalar', err) - - def test_yaml_expected_sequence(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - interfaces: {}''', expect_fail=True) - self.assertIn('expected sequence', err) - - def test_yaml_expected_mapping(self): - err = self.generate('network:\n version', expect_fail=True) - self.assertIn('/a.yaml line 1 column 2: expected mapping', err) - - def test_invalid_bool(self): - err = self.generate('''network: - version: 2 - ethernets: - id0: - wakeonlan: wut -''', expect_fail=True) - self.assertIn('invalid boolean value wut', err) - - def test_invalid_version(self): - err = self.generate('network:\n version: 1', expect_fail=True) - self.assertIn('/a.yaml line 1 column 11: Only version 2 is supported', err) - - def test_id_redef_type_mismatch(self): - err = self.generate('''network: - version: 2 - ethernets: - id0: - wakeonlan: true''', - confs={'redef': '''network: - version: 2 - bridges: - id0: - wakeonlan: true'''}, expect_fail=True) - self.assertIn("redef.yaml line 3 column 4: Updated definition 'id0' changes device type", err) - - def test_set_name_without_match(self): - err = self.generate('''network: - version: 2 - ethernets: - def1: - set-name: lom1 -''', expect_fail=True) - self.assertIn('/a.yaml line 4 column 6: def1: set-name: requires match: properties', err) - - def test_virtual_set_name(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - set_name: br1''', expect_fail=True) - self.assertIn('/a.yaml line 4 column 6: unknown key set_name\n', err) - - def test_virtual_match(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - match: - driver: foo''', expect_fail=True) - self.assertIn('/a.yaml line 4 column 6: unknown key match\n', err) - - def test_virtual_wol(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - wakeonlan: true''', expect_fail=True) - self.assertIn('/a.yaml line 4 column 6: unknown key wakeonlan\n', err) - - def test_bridge_unknown_iface(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - interfaces: ['foo']''', expect_fail=True) - self.assertIn('/a.yaml line 4 column 19: br0: interface foo is not defined\n', err) - - def test_bridge_multiple_assignments(self): - err = self.generate('''network: - version: 2 - ethernets: - eno1: {} - bridges: - br0: - interfaces: [eno1] - br1: - interfaces: [eno1]''', expect_fail=True) - self.assertIn('br1: interface eno1 is already assigned to bridge br0\n', err) - - def test_unknown_global_renderer(self): - err = self.generate('''network: - version: 2 - renderer: bogus -''', expect_fail=True) - self.assertIn("unknown renderer 'bogus'", err) - - def test_unknown_type_renderer(self): - err = self.generate('''network: - version: 2 - ethernets: - renderer: bogus -''', expect_fail=True) - self.assertIn("unknown renderer 'bogus'", err) - - def test_invalid_id(self): - err = self.generate('''network: - version: 2 - ethernets: - "eth 0": - dhcp4: true''', expect_fail=True) - self.assertIn("Invalid name 'eth 0'", err) - - def test_invalid_name_match(self): - err = self.generate('''network: - version: 2 - ethernets: - def1: - match: - name: | - fo o - bar - dhcp4: true''', expect_fail=True) - self.assertIn("Invalid name 'fo o\nbar\n'", err) - - def test_invalid_mac_match(self): - err = self.generate('''network: - version: 2 - ethernets: - def1: - match: - macaddress: 00:11:ZZ - dhcp4: true''', expect_fail=True) - self.assertIn("Invalid MAC address '00:11:ZZ', must be XX:XX:XX:XX:XX:XX", err) - - def test_glob_in_id(self): - err = self.generate('''network: - version: 2 - ethernets: - en*: - dhcp4: true''', expect_fail=True) - self.assertIn("Definition ID 'en*' must not use globbing", err) - - def test_wifi_duplicate_ssid(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - workplace: - password: "s3kr1t" - workplace: - password: "c0mpany" - dhcp4: yes''', expect_fail=True) - self.assertIn("wl0: Duplicate access point SSID 'workplace'", err) - - def test_wifi_no_ap(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - dhcp4: yes''', expect_fail=True) - self.assertIn('wl0: No access points defined', err) - - def test_wifi_empty_ap(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - access-points: {} - dhcp4: yes''', expect_fail=True) - self.assertIn('wl0: No access points defined', err) - - def test_wifi_ap_unknown_key(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - workplace: - something: false - dhcp4: yes''', expect_fail=True) - self.assertIn('/etc/netplan/a.yaml line 6 column 10: unknown key something', err) - - def test_wifi_ap_unknown_mode(self): - err = self.generate('''network: - version: 2 - wifis: - wl0: - access-points: - workplace: - mode: bogus''', expect_fail=True) - self.assertIn("unknown wifi mode 'bogus'", err) - - def test_invalid_ipv4_address(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 192.168.14/24 - - 2001:FFfe::1/64''', expect_fail=True) - - self.assertIn("malformed address '192.168.14/24', must be X.X.X.X/NN", err) - - def test_missing_ipv4_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 192.168.14.1''', expect_fail=True) - - self.assertIn("address '192.168.14.1' is missing /prefixlength", err) - - def test_empty_ipv4_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 192.168.14.1/''', expect_fail=True) - - self.assertIn("invalid prefix length in address '192.168.14.1/'", err) - - def test_invalid_ipv4_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 192.168.14.1/33''', expect_fail=True) - - self.assertIn("invalid prefix length in address '192.168.14.1/33'", err) - - def test_invalid_ipv6_address(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 2001:G::1/64''', expect_fail=True) - - self.assertIn("malformed address '2001:G::1/64', must be X.X.X.X/NN", err) - - def test_missing_ipv6_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 2001::1''', expect_fail=True) - self.assertIn("address '2001::1' is missing /prefixlength", err) - - def test_invalid_ipv6_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 2001::1/129''', expect_fail=True) - self.assertIn("invalid prefix length in address '2001::1/129'", err) - - def test_empty_ipv6_prefixlen(self): - err = self.generate('''network: - version: 2 - ethernets: - engreen: - addresses: - - 2001::1/''', expect_fail=True) - self.assertIn("invalid prefix length in address '2001::1/'", err) - - def test_invalid_gateway4(self): - for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: - err = self.generate('''network: - version: 2 - ethernets: - engreen: - gateway4: %s''' % a, expect_fail=True) - self.assertIn("invalid IPv4 address '%s'" % a, err) - - def test_invalid_gateway6(self): - for a in ['1234', '1:::c', '1234::1/50']: - err = self.generate('''network: - version: 2 - ethernets: - engreen: - gateway6: %s''' % a, expect_fail=True) - self.assertIn("invalid IPv6 address '%s'" % a, err) - - def test_invalid_nameserver_ipv4(self): - for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: - err = self.generate('''network: - version: 2 - ethernets: - engreen: - nameservers: - addresses: [%s]''' % a, expect_fail=True) - self.assertIn("malformed address '%s'" % a, err) - - def test_invalid_nameserver_ipv6(self): - for a in ['1234', '1:::c', '1234::1/50']: - err = self.generate('''network: - version: 2 - ethernets: - engreen: - nameservers: - addresses: ["%s"]''' % a, expect_fail=True) - self.assertIn("malformed address '%s'" % a, err) - - def test_vlan_missing_id(self): - err = self.generate('''network: - version: 2 - ethernets: {en1: {}} - vlans: - ena: {link: en1}''', expect_fail=True) - self.assertIn('missing id property', err) - - def test_vlan_invalid_id(self): - err = self.generate('''network: - version: 2 - ethernets: {en1: {}} - vlans: - ena: {id: a, link: en1}''', expect_fail=True) - self.assertIn('invalid unsigned int value a', err) - - err = self.generate('''network: - version: 2 - ethernets: {en1: {}} - vlans: - ena: {id: 4095, link: en1}''', expect_fail=True) - self.assertIn('invalid id 4095', err) - - def test_vlan_missing_link(self): - err = self.generate('''network: - version: 2 - vlans: - ena: {id: 1}''', expect_fail=True) - self.assertIn('ena: missing link property', err) - - def test_vlan_unknown_link(self): - err = self.generate('''network: - version: 2 - vlans: - ena: {id: 1, link: en1}''', expect_fail=True) - self.assertIn('ena: interface en1 is not defined\n', err) - - def test_device_bad_route_to(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - to: badlocation - via: 192.168.14.20 - metric: 100 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_bad_route_via(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - to: 10.10.0.0/16 - via: badgateway - metric: 100 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_bad_route_metric(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - to: 10.10.0.0/16 - via: 10.1.1.1 - metric: -1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_family_mismatch_ipv6_to(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - to: 2001:dead:beef::0/16 - via: 10.1.1.1 - metric: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_family_mismatch_ipv4_to(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - to: 10.10.10.0/24 - metric: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_missing_to(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - metric: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_missing_via(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - to: 2001:dead:beef::2 - metric: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_type_missing_to(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - type: prohibit - metric: 1 - addresses: - - 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 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - to: 2000:cafe:cafe::1/24 - on-link: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_invalid_table(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - to: 2000:cafe:cafe::1/24 - table: -1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_invalid_type(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - to: 2000:cafe:cafe::1/24 - type: thisisinvalidtype - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_route_invalid_scope(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routes: - - via: 2001:dead:beef::2 - to: 2000:cafe:cafe::1/24 - scope: linky - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_bridge_invalid_dev_for_path_cost(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - path-cost: - eth0: 50 - dhcp4: true''', expect_fail=True) - - def test_bridge_path_cost_already_defined(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - path-cost: - eno1: 50 - eno1: 40 - dhcp4: true''', expect_fail=True) - - def test_bridge_invalid_path_cost(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - path-cost: - eno1: aa - dhcp4: true''', expect_fail=True) - - def test_bridge_invalid_dev_for_port_prio(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - port-priority: - eth0: 50 - dhcp4: true''', expect_fail=True) - - def test_bridge_port_prio_already_defined(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - port-priority: - eno1: 50 - eno1: 40 - dhcp4: true''', expect_fail=True) - - def test_bridge_invalid_port_prio(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bridges: - br0: - interfaces: [eno1] - parameters: - port-priority: - eno1: 257 - dhcp4: true''', expect_fail=True) - - def test_bond_invalid_arp_target(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bonds: - bond0: - interfaces: [eno1] - parameters: - arp-ip-targets: - - 2001:dead:beef::1 - dhcp4: true''', expect_fail=True) - - def test_bond_invalid_primary_slave(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - bonds: - bond0: - interfaces: [eno1] - parameters: - primary: wigglewiggle - dhcp4: true''', expect_fail=True) - - def test_bond_duplicate_primary_slave(self): - self.generate('''network: - version: 2 - ethernets: - eno1: - match: - name: eth0 - eno2: - match: - name: eth1 - bonds: - bond0: - interfaces: [eno1, eno2] - parameters: - primary: eno1 - primary: eno2 - dhcp4: true''', expect_fail=True) - - def test_bond_multiple_assignments(self): - err = self.generate('''network: - version: 2 - ethernets: - eno1: {} - bonds: - bond0: - interfaces: [eno1] - bond1: - interfaces: [eno1]''', expect_fail=True) - self.assertIn('bond1: interface eno1 is already assigned to bond bond0\n', err) - - def test_bond_bridge_cross_assignments1(self): - err = self.generate('''network: - version: 2 - ethernets: - eno1: {} - bonds: - bond0: - interfaces: [eno1] - bridges: - br1: - interfaces: [eno1]''', expect_fail=True) - self.assertIn('br1: interface eno1 is already assigned to bond bond0\n', err) - - def test_bond_bridge_cross_assignments2(self): - err = self.generate('''network: - version: 2 - ethernets: - eno1: {} - bridges: - br0: - interfaces: [eno1] - bonds: - bond1: - interfaces: [eno1]''', expect_fail=True) - self.assertIn('bond1: interface eno1 is already assigned to bridge br0\n', err) - - def test_bond_bridge_nested_assignments(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - bonds: - bond0: - interfaces: [eno1] - bridges: - br1: - interfaces: [bond0]''') - - def test_device_ip_rule_mismatched_addresses(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - from: 10.10.10.0/24 - to: 2000:dead:beef::3/64 - table: 50 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_missing_address(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - table: 50 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_invalid_tos(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - from: 10.10.10.0/24 - type-of-service: 256 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_invalid_prio(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - from: 10.10.10.0/24 - priority: -1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_invalid_table(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - from: 10.10.10.0/24 - table: -1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_invalid_fwmark(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - from: 10.10.10.0/24 - mark: -1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_device_ip_rule_invalid_address(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - routing-policy: - - to: 10.10.10.0/24 - from: someinvalidaddress - mark: 1 - addresses: - - 192.168.14.2/24 - - 2001:FFfe::1/64''', expect_fail=True) - - def test_invalid_dhcp_identifier(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: yes - dhcp-identifier: invalid''', expect_fail=True) - - def test_invalid_accept_ra(self): - err = self.generate('''network: - version: 2 - bridges: - br0: - 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): - - def test_fwdecl_bridge_on_bond(self): - self.generate('''network: - version: 2 - bridges: - br0: - interfaces: ['bond0'] - dhcp4: true - bonds: - bond0: - interfaces: ['eth0', 'eth1'] - ethernets: - eth0: - match: - macaddress: 00:01:02:03:04:05 - set-name: eth0 - eth1: - match: - macaddress: 02:01:02:03:04:05 - set-name: eth1 -''') - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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] -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]\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]\nLinkLocalAddressing=no\nBond=bond0\n'}) - - def test_fwdecl_feature_blend(self): - self.generate('''network: - version: 2 - vlans: - vlan1: - link: 'br0' - id: 1 - dhcp4: true - bridges: - br0: - interfaces: ['bond0', 'eth2'] - parameters: - path-cost: - eth2: 1000 - bond0: 8888 - bonds: - bond0: - interfaces: ['eth0', 'br1'] - ethernets: - eth0: - match: - macaddress: 00:01:02:03:04:05 - set-name: eth0 - bridges: - br1: - interfaces: ['eth1'] - ethernets: - eth1: - match: - macaddress: 02:01:02:03:04:05 - set-name: eth1 - eth2: - match: - name: eth2 -''') - - self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n' - '[VLAN]\nId=1\n', - '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] -Name=br0 - -[Network] -LinkLocalAddressing=ipv6 -ConfigureWithoutCarrier=yes -VLAN=vlan1 -''', - 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', - 'bond0.network': '''[Match] -Name=bond0 - -[Network] -LinkLocalAddressing=no -ConfigureWithoutCarrier=yes -Bridge=br0 - -[Bridge] -Cost=8888 -''', - 'eth2.network': '[Match]\nName=eth2\n\n' - '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' - '[Bridge]\nCost=1000\n', - 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\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]\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]\nLinkLocalAddressing=no\nBridge=br1\n'}) - - -class TestMerging(TestBase): - '''multiple *.yaml merging''' - - def test_global_backend(self): - self.generate('''network: - version: 2 - renderer: NetworkManager - ethernets: - engreen: - dhcp4: y''', - confs={'backend': 'network:\n renderer: networkd'}) - - self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:engreen,''') - self.assert_nm_udev(None) - - def test_add_def(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - dhcp4: true''', - confs={'blue': '''network: - version: 2 - ethernets: - enblue: - dhcp4: true'''}) - - self.assert_networkd({'enblue.network': ND_DHCP4 % 'enblue', - 'engreen.network': ND_DHCP4 % 'engreen'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:enblue,interface-name:engreen,''') - self.assert_nm_udev(None) - - def test_change_def(self): - self.generate('''network: - version: 2 - ethernets: - engreen: - wakeonlan: true - dhcp4: false''', - confs={'green-dhcp': '''network: - version: 2 - ethernets: - engreen: - dhcp4: true'''}) - - self.assert_networkd({'engreen.link': '[Match]\nOriginalName=engreen\n\n[Link]\nWakeOnLan=magic\n', - 'engreen.network': ND_DHCP4 % 'engreen'}) - - def test_cleanup_old_config(self): - self.generate('''network: - version: 2 - ethernets: - engreen: {dhcp4: true} - enyellow: {renderer: NetworkManager}''', - confs={'blue': '''network: - version: 2 - ethernets: - enblue: - dhcp4: true'''}) - - os.unlink(os.path.join(self.confdir, 'blue.yaml')) - self.generate('''network: - version: 2 - ethernets: - engreen: {dhcp4: true}''') - - self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) - self.assert_nm(None, '''[keyfile] -# devices managed by networkd -unmanaged-devices+=interface-name:engreen,''') - self.assert_nm_udev(None) - - def test_ref(self): - self.generate('''network: - version: 2 - ethernets: - eno1: {} - switchports: - match: - driver: yayroute''', - confs={'bridges': '''network: - version: 2 - bridges: - br0: - interfaces: [eno1, switchports] - dhcp4: true'''}) - - self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', - '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]\nLinkLocalAddressing=no\nBridge=br0\n', - 'switchports.network': '[Match]\nDriver=yayroute\n\n' - '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) - - def test_def_in_run(self): - rundir = os.path.join(self.workdir.name, 'run', 'netplan') - os.makedirs(rundir) - # override b.yaml definition for enred - with open(os.path.join(rundir, 'b.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {enred: {dhcp4: true}}''') - - # append new definition for enblue - with open(os.path.join(rundir, 'c.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {enblue: {dhcp4: true}}''') - - self.generate('''network: - version: 2 - ethernets: - engreen: {dhcp4: true}''', confs={'b': '''network: - version: 2 - ethernets: {enred: {wakeonlan: true}}'''}) - - # b.yaml in /run/ should completely shadow b.yaml in /etc, thus no enred.link - self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', - 'enred.network': ND_DHCP4 % 'enred', - 'enblue.network': ND_DHCP4 % 'enblue'}) - - def test_def_in_lib(self): - libdir = os.path.join(self.workdir.name, 'lib', 'netplan') - rundir = os.path.join(self.workdir.name, 'run', 'netplan') - os.makedirs(libdir) - os.makedirs(rundir) - # b.yaml is in /etc/netplan too which should have precedence - with open(os.path.join(libdir, 'b.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {notme: {dhcp4: true}}''') - - # /run should trump /lib too - with open(os.path.join(libdir, 'c.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {alsonot: {dhcp4: true}}''') - with open(os.path.join(rundir, 'c.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {enyellow: {dhcp4: true}}''') - - # this should be considered - with open(os.path.join(libdir, 'd.yaml'), 'w') as f: - f.write('''network: - version: 2 - ethernets: {enblue: {dhcp4: true}}''') - - self.generate('''network: - version: 2 - ethernets: - engreen: {dhcp4: true}''', confs={'b': '''network: - version: 2 - ethernets: {enred: {wakeonlan: true}}'''}) - - self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', - 'enred.link': '[Match]\nOriginalName=enred\n\n[Link]\nWakeOnLan=magic\n', - 'enyellow.network': ND_DHCP4 % 'enyellow', - 'enblue.network': ND_DHCP4 % 'enblue'}) - - -unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/generator/__init__.py b/tests/generator/__init__.py new file mode 100644 index 0000000..81eadaa --- /dev/null +++ b/tests/generator/__init__.py @@ -0,0 +1,17 @@ +# +# __init__ for generator tests. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/tests/generator/base.py b/tests/generator/base.py new file mode 100644 index 0000000..9852156 --- /dev/null +++ b/tests/generator/base.py @@ -0,0 +1,169 @@ +# +# Blackbox tests of netplan generate that verify that the generated +# configuration files look as expected. These are run during "make check" and +# don't touch the system configuration at all. +# +# Copyright (C) 2016 Canonical, Ltd. +# Author: Martin Pitt <martin.pitt@ubuntu.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import random +import re +import sys +import stat +import string +import tempfile +import subprocess +import unittest + +exe_generate = os.path.join(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))), 'generate') + +# make sure we fail on criticals +os.environ['G_DEBUG'] = 'fatal-criticals' + +# common patterns for expected output +ND_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' +ND_DHCP4_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\n' +ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=600\nUseMTU=true\n' +ND_DHCP6 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' +ND_DHCP6_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\n' +ND_DHCPYES = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' +ND_DHCPYES_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\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' + + +class TestBase(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') + self.nm_enable_all_conf = os.path.join( + self.workdir.name, 'run', 'NetworkManager', 'conf.d', '10-globally-managed-devices.conf') + self.maxDiff = None + + def generate(self, yaml, expect_fail=False, extra_args=[], confs=None): + '''Call generate with given YAML string as configuration + + Return stderr output. + ''' + conf = os.path.join(self.confdir, 'a.yaml') + os.makedirs(os.path.dirname(conf), exist_ok=True) + if yaml is not None: + with open(conf, 'w') as f: + f.write(yaml) + if confs: + for f, contents in confs.items(): + with open(os.path.join(self.confdir, f + '.yaml'), 'w') as f: + f.write(contents) + + argv = [exe_generate, '--root-dir', self.workdir.name] + extra_args + if 'TEST_SHELL' in os.environ: # pragma nocover + print('Test is about to run:\n%s' % ' '.join(argv)) + subprocess.call(['bash', '-i'], cwd=self.workdir.name) + + p = subprocess.Popen(argv, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + (out, err) = p.communicate() + if expect_fail: + self.assertGreater(p.returncode, 0) + else: + self.assertEqual(p.returncode, 0, err) + 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: + self.assertFalse(os.path.exists(networkd_dir)) + return + + self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) + self.assertEqual(set(os.listdir(networkd_dir)), + {'10-netplan-' + f for f in file_contents_map}) + for fname, contents in file_contents_map.items(): + with open(os.path.join(networkd_dir, '10-netplan-' + fname)) as f: + self.assertEqual(f.read(), contents) + + def assert_networkd_udev(self, file_contents_map): + udev_dir = os.path.join(self.workdir.name, 'run', 'udev', 'rules.d') + if not file_contents_map: + # it can either not exist, or can only contain 90-netplan.rules + self.assertTrue((not os.path.exists(udev_dir)) or + (os.listdir(udev_dir) == ['90-netplan.rules'])) + return + + self.assertEqual(set(os.listdir(udev_dir)) - set(['90-netplan.rules']), + {'99-netplan-' + f for f in file_contents_map}) + for fname, contents in file_contents_map.items(): + 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 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 assert_nm(self, connections_map=None, conf=None): + # check config + conf_path = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'conf.d', 'netplan.conf') + if conf: + with open(conf_path) as f: + self.assertEqual(f.read(), conf) + else: + if os.path.exists(conf_path): + with open(conf_path) as f: # pragma: nocover + self.fail('unexpected %s:\n%s' % (conf_path, f.read())) + + # check connections + con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') + if connections_map: + self.assertEqual(set(os.listdir(con_dir)), + set(['netplan-' + n for n in connections_map])) + for fname, contents in connections_map.items(): + with open(os.path.join(con_dir, 'netplan-' + fname)) as f: + self.assertEqual(f.read(), contents) + # NM connection files might contain secrets + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + else: + if os.path.exists(con_dir): + self.assertEqual(os.listdir(con_dir), []) + + def assert_nm_udev(self, contents): + rule_path = os.path.join(self.workdir.name, 'run/udev/rules.d/90-netplan.rules') + if contents is None: + self.assertFalse(os.path.exists(rule_path)) + return + with open(rule_path) as f: + self.assertEqual(f.read(), contents) diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py new file mode 100644 index 0000000..2e7e1be --- /dev/null +++ b/tests/generator/test_args.py @@ -0,0 +1,172 @@ +# +# Command-line arguments handling tests for generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import subprocess + +from .base import TestBase, exe_generate + + +class TestConfigArgs(TestBase): + '''Config file argument handling''' + + def test_no_files(self): + subprocess.check_call([exe_generate, '--root-dir', self.workdir.name]) + self.assertEqual(os.listdir(self.workdir.name), []) + self.assert_nm_udev(None) + + def test_no_configs(self): + self.generate('network:\n version: 2') + # should not write any files + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + self.assert_nm_udev(None) + + def test_empty_config(self): + self.generate('') + # should not write any files + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + self.assert_nm_udev(None) + + def test_file_args(self): + conf = os.path.join(self.workdir.name, 'config') + with open(conf, 'w') as f: + f.write('network: {}') + # when specifying custom files, it should ignore the global config + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''', extra_args=[conf]) + self.assertEqual(set(os.listdir(self.workdir.name)), {'config', 'etc'}) + + def test_file_args_notfound(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''', expect_fail=True, extra_args=['/non/existing/config']) + self.assertEqual(err, 'Cannot open /non/existing/config: No such file or directory\n') + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + + def test_help(self): + conf = os.path.join(self.workdir.name, 'etc', 'netplan', 'a.yaml') + os.makedirs(os.path.dirname(conf)) + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + + p = subprocess.Popen([exe_generate, '--root-dir', self.workdir.name, '--help'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + (out, err) = p.communicate() + self.assertEqual(err, '') + self.assertEqual(p.returncode, 0) + self.assertIn('Usage:', out) + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + + def test_unknown_cli_args(self): + p = subprocess.Popen([exe_generate, '--foo'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + (out, err) = p.communicate() + self.assertIn('nknown option --foo', err) + self.assertNotEqual(p.returncode, 0) + + def test_output_mkdir_error(self): + conf = os.path.join(self.workdir.name, 'config') + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + err = self.generate('', extra_args=['--root-dir', '/proc/foo', conf], expect_fail=True) + self.assertIn('cannot create directory /proc/foo/run/systemd/network', err) + + def test_systemd_generator(self): + conf = os.path.join(self.confdir, 'a.yaml') + os.makedirs(os.path.dirname(conf)) + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + self.assertEqual(set(os.listdir(outdir)), + {'netplan.stamp', 'multi-user.target.wants', 'network-online.target.wants'}) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) + + # should auto-enable networkd and -wait-online + self.assertTrue(os.path.islink(os.path.join( + outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) + self.assertTrue(os.path.islink(os.path.join( + outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) + + # should be a no-op the second time while the stamp exists + out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], + stderr=subprocess.STDOUT) + self.assertFalse(os.path.exists(n)) + self.assertIn(b'netplan generate already ran', out) + + # after removing the stamp it generates again, and not trip over the + # existing enablement symlink + os.unlink(os.path.join(outdir, 'netplan.stamp')) + subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + self.assertTrue(os.path.exists(n)) + + def test_systemd_generator_noconf(self): + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + # no enablement symlink here + self.assertEqual(os.listdir(outdir), ['netplan.stamp']) + + def test_systemd_generator_badcall(self): + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + try: + subprocess.check_output([generator, '--root-dir', self.workdir.name], + stderr=subprocess.STDOUT) + self.fail("direct systemd generator call is expected to fail, but succeeded.") # pragma: nocover + except subprocess.CalledProcessError as e: + self.assertEqual(e.returncode, 1) + self.assertIn(b'can not be called directly', e.output) diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py new file mode 100644 index 0000000..ae66dd5 --- /dev/null +++ b/tests/generator/test_auth.py @@ -0,0 +1,456 @@ +# +# Tests for network authentication config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import stat +import sys + +from .base import TestBase, ND_DHCP4, ND_WIFI_DHCP4 + + +class TestNetworkd(TestBase): + + def test_auth_wifi_detailed(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s3kr1t" + "Luke's Home": + auth: + key-management: psk + password: "4lsos3kr1t" + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + workplace2: + auth: + key-management: eap + method: peap + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + ca-certificate: /etc/ssl/work2-cacrt.pem + customernet: + auth: + key-management: eap + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + opennet: + auth: + key-management: none + peer2peer: + mode: adhoc + auth: {} + dhcp4: yes + ''') + + self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant + +network={ + ssid="Joe's Home" + key_mgmt=WPA-PSK + psk="s3kr1t" +} +network={ + ssid="Luke's Home" + key_mgmt=WPA-PSK + psk="4lsos3kr1t" +} +network={ + ssid="opennet" + key_mgmt=NONE +} +network={ + ssid="workplace" + key_mgmt=WPA-EAP + eap=TTLS + identity="joe@internal.example.com" + anonymous_identity="@internal.example.com" + password="v3ryS3kr1t" +} +network={ + ssid="customernet" + key_mgmt=WPA-EAP + eap=TLS + identity="cert-joe@cust.example.com" + anonymous_identity="@cust.example.com" + ca_cert="/etc/ssl/cust-cacrt.pem" + client_cert="/etc/ssl/cust-crt.pem" + private_key="/etc/ssl/cust-key.pem" + private_key_passwd="d3cryptPr1v4t3K3y" +} +network={ + ssid="workplace2" + key_mgmt=WPA-EAP + eap=PEAP + identity="joe@internal.example.com" + password="v3ryS3kr1t" + ca_cert="/etc/ssl/work2-cacrt.pem" +} +network={ + ssid="peer2peer" + mode=1 + key_mgmt=NONE +} +''') + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/multi-user.target.wants/netplan-wpa@wl0.service'))) + + def test_auth_wired(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + dhcp4: yes + ''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-eth0.conf')) as f: + self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant + +network={ + key_mgmt=IEEE8021X + eap=TLS + identity="cert-joe@cust.example.com" + anonymous_identity="@cust.example.com" + ca_cert="/etc/ssl/cust-cacrt.pem" + client_cert="/etc/ssl/cust-crt.pem" + private_key="/etc/ssl/cust-key.pem" + private_key_passwd="d3cryptPr1v4t3K3y" +} +''') + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/multi-user.target.wants/netplan-wpa@eth0.service'))) + + +class TestNetworkManager(TestBase): + + def test_auth_wifi_detailed(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + "Joe's Home": + password: "s3kr1t" + "Luke's Home": + auth: + key-management: psk + password: "4lsos3kr1t" + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + workplace2: + auth: + key-management: eap + method: peap + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + ca-certificate: /etc/ssl/work2-cacrt.pem + customernet: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + opennet: + auth: + key-management: none + peer2peer: + mode: adhoc + auth: {} + dhcp4: yes + ''') + + self.assert_networkd({}) + self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] +id=netplan-wl0-Joe's Home +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Joe's Home +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=s3kr1t +''', + 'wl0-Luke%27s%20Home': '''[connection] +id=netplan-wl0-Luke's Home +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Luke's Home +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=4lsos3kr1t +''', + 'wl0-workplace': '''[connection] +id=netplan-wl0-workplace +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap=ttls +identity=joe@internal.example.com +anonymous-identity=@internal.example.com +password=v3ryS3kr1t +''', + 'wl0-workplace2': '''[connection] +id=netplan-wl0-workplace2 +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace2 +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap=peap +identity=joe@internal.example.com +password=v3ryS3kr1t +ca-cert=/etc/ssl/work2-cacrt.pem +''', + 'wl0-customernet': '''[connection] +id=netplan-wl0-customernet +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=customernet +mode=infrastructure + +[wifi-security] +key-mgmt=ieee8021x + +[802-1x] +eap=tls +identity=cert-joe@cust.example.com +anonymous-identity=@cust.example.com +ca-cert=/etc/ssl/cust-cacrt.pem +client-cert=/etc/ssl/cust-crt.pem +private-key=/etc/ssl/cust-key.pem +private-key-password=d3cryptPr1v4t3K3y +''', + 'wl0-opennet': '''[connection] +id=netplan-wl0-opennet +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=opennet +mode=infrastructure +''', + 'wl0-peer2peer': '''[connection] +id=netplan-wl0-peer2peer +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=peer2peer +mode=adhoc +'''}) + self.assert_nm_udev(None) + + + def test_auth_wired(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + dhcp4: yes + ''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[802-1x] +eap=tls +identity=cert-joe@cust.example.com +anonymous-identity=@cust.example.com +ca-cert=/etc/ssl/cust-cacrt.pem +client-cert=/etc/ssl/cust-crt.pem +private-key=/etc/ssl/cust-key.pem +private-key-password=d3cryptPr1v4t3K3y +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestConfigErrors(TestBase): + + def test_auth_invalid_key_mgmt(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + key-management: bogus''', expect_fail=True) + self.assertIn("unknown key management type 'bogus'", err) + + def test_auth_invalid_eap_method(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + method: bogus''', expect_fail=True) + self.assertIn("unknown EAP method 'bogus'", err) diff --git a/tests/generator/test_bonds.py b/tests/generator/test_bonds.py new file mode 100644 index 0000000..2df3664 --- /dev/null +++ b/tests/generator/test_bonds.py @@ -0,0 +1,756 @@ +# +# Tests for bond devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_bond_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: no + accept-ra: no + bonds: + bond0: + interfaces: [engreen] + dhcp6: true + accept-ra: yes''') + self.assert_networkd({'bond0.network': '''[Match] +Name=bond0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=yes +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'bond0.netdev': '''[NetDev] +Name=bond0 +Kind=bond +''', + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +IPv6AcceptRA=no +Bond=bond0 +'''}) + + def test_bond_empty(self): + self.generate('''network: + version: 2 + bonds: + bn0: + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:bn0,''') + self.assert_nm_udev(None) + + def test_bond_components(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_empty_parameters(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: {} + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_with_parameters(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: 802.1ad + lacp-rate: 10 + mii-monitor-interval: 10 + min-links: 10 + up-delay: 20 + down-delay: 30 + all-slaves-active: true + transmit-hash-policy: none + ad-select: none + arp-interval: 15 + arp-validate: all + arp-all-targets: all + fail-over-mac-policy: none + gratuitious-arp: 10 + packets-per-slave: 10 + primary-reselect-policy: none + resend-igmp: 10 + learn-packet-interval: 10 + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=802.1ad\n' + 'LACPTransmitRate=10\n' + 'MIIMonitorSec=10ms\n' + 'MinLinks=10\n' + 'TransmitHashPolicy=none\n' + 'AdSelect=none\n' + 'AllSlavesActive=1\n' + 'ARPIntervalSec=15ms\n' + 'ARPIPTargets=10.10.10.10,20.20.20.20\n' + 'ARPValidate=all\n' + 'ARPAllTargets=all\n' + 'UpDelaySec=20ms\n' + 'DownDelaySec=30ms\n' + 'FailOverMACPolicy=none\n' + 'GratuitousARP=10\n' + 'PacketsPerSlave=10\n' + 'PrimaryReselectPolicy=none\n' + 'ResendIGMP=10\n' + 'LearnPacketIntervalSec=10\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_with_parameters_all_suffix(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: 802.1ad + mii-monitor-interval: 10ms + up-delay: 20ms + down-delay: 30s + arp-interval: 15m + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=802.1ad\n' + 'MIIMonitorSec=10ms\n' + 'ARPIntervalSec=15m\n' + 'UpDelaySec=20ms\n' + 'DownDelaySec=30s\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: active-backup + primary: eno1 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=active-backup\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + '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] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + +class TestNetworkManager(TestBase): + + def test_bond_empty(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bonds: + bn0: + dhcp4: true''') + + self.assert_nm({'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_bond_components(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_empty_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: {} + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_with_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: + mode: 802.1ad + lacp-rate: 10 + mii-monitor-interval: 10 + min-links: 10 + up-delay: 10 + down-delay: 10 + all-slaves-active: true + transmit-hash-policy: none + ad-select: none + arp-interval: 10 + arp-validate: all + arp-all-targets: all + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + fail-over-mac-policy: none + gratuitious-arp: 10 + packets-per-slave: 10 + primary-reselect-policy: none + resend-igmp: 10 + learn-packet-interval: 10 + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[bond] +mode=802.1ad +lacp_rate=10 +miimon=10 +min_links=10 +xmit_hash_policy=none +ad_select=none +all_slaves_active=1 +arp_interval=10 +arp_ip_target=10.10.10.10,20.20.20.20 +arp_validate=all +arp_all_targets=all +updelay=10 +downdelay=10 +fail_over_mac=none +num_grat_arp=10 +num_unsol_na=10 +packets_per_slave=10 +primary_reselect=none +resend_igmp=10 +lp_interval=10 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_primary_slave(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: + mode: active-backup + primary: eno1 + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[bond] +mode=active-backup +primary=eno1 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestConfigErrors(TestBase): + + def test_bond_invalid_arp_target(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bonds: + bond0: + interfaces: [eno1] + parameters: + arp-ip-targets: + - 2001:dead:beef::1 + dhcp4: true''', expect_fail=True) + + def test_bond_invalid_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bonds: + bond0: + interfaces: [eno1] + parameters: + primary: wigglewiggle + dhcp4: true''', expect_fail=True) + + def test_bond_duplicate_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + eno2: + match: + name: eth1 + bonds: + bond0: + interfaces: [eno1, eno2] + parameters: + primary: eno1 + primary: eno2 + dhcp4: true''', expect_fail=True) + + def test_bond_multiple_assignments(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bond1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("bond1: interface 'eno1' is already assigned to bond bond0", err) + + def test_bond_bridge_cross_assignments1(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bridges: + br1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("br1: interface 'eno1' is already assigned to bond bond0", err) + + def test_bond_bridge_cross_assignments2(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + bonds: + bond1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("bond1: interface 'eno1' is already assigned to bridge br0", err) + + def test_bond_bridge_nested_assignments(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bridges: + br1: + interfaces: [bond0]''') diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py new file mode 100644 index 0000000..0369202 --- /dev/null +++ b/tests/generator/test_bridges.py @@ -0,0 +1,714 @@ +# +# Tests for bridge devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_bridge_set_mac(self): + self.generate('''network: + version: 2 + bridges: + br0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd({'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '[NetDev]\nName=br0\nMACAddress=00:01:02:03:04:05\nKind=bridge\n'}) + + def test_bridge_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: no + dhcp6: no + accept-ra: no + bridges: + br0: + interfaces: [engreen] + dhcp6: true + accept-ra: no''') + self.assert_networkd({'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=no +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '''[NetDev] +Name=br0 +Kind=bridge +''', + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +IPv6AcceptRA=no +Bridge=br0 +'''}) + + def test_bridge_empty(self): + self.generate('''network: + version: 2 + bridges: + br0: + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_type_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + renderer: networkd + br0: + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_def_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + renderer: NetworkManager + br0: + renderer: networkd + addresses: [1.2.3.4/12] + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +Address=1.2.3.4/12 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_forward_declaration(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true + ethernets: + eno1: {} + switchports: + match: + driver: yayroute +''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + def test_eth_bridge_nm_blacklist(self): + self.generate('''network: + renderer: networkd + ethernets: + eth42: + dhcp4: yes + ethbr: + match: {name: eth43} + bridges: + mybr: + interfaces: [ethbr] + dhcp4: yes''') + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth42,interface-name:eth43,interface-name:mybr,''') + + def test_bridge_components(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + def test_bridge_params(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bridges: + br0: + interfaces: [eno1, switchports] + parameters: + ageing-time: 50 + forward-delay: 12 + hello-time: 6 + max-age: 24 + priority: 1000 + stp: true + path-cost: + eno1: 70 + port-priority: + eno1: 14 + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' + '[Bridge]\nAgeingTimeSec=50\n' + 'Priority=1000\n' + 'ForwardDelaySec=12\n' + 'HelloTimeSec=6\n' + 'MaxAgeSec=24\n' + 'STP=true\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' + '[Bridge]\nCost=70\nPriority=14\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + +class TestNetworkManager(TestBase): + + def test_bridge_empty(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + br0: + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_type_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + bridges: + renderer: NetworkManager + br0: + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_set_mac(self): + self.generate('''network: + version: 2 + bridges: + renderer: NetworkManager + br0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[802-3-ethernet] +cloned-mac-address=00:01:02:03:04:05 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_bridge_def_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + bridges: + renderer: networkd + br0: + renderer: NetworkManager + addresses: [1.2.3.4/12] + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto +address1=1.2.3.4/12 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_forward_declaration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + br0: + interfaces: [eno1, switchport] + dhcp4: true + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 +''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_components(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bridges: + br0: + interfaces: [eno1, switchport] + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bridges: + br0: + interfaces: [eno1, switchport] + parameters: + ageing-time: 50 + priority: 1000 + forward-delay: 12 + hello-time: 6 + max-age: 24 + path-cost: + eno1: 70 + port-priority: + eno1: 61 + stp: true + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[bridge-port] +path-cost=70 +priority=61 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[bridge] +ageing-time=50 +priority=1000 +forward-delay=12 +hello-time=6 +max-age=24 +stp=true + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestConfigErrors(TestBase): + + def test_bridge_unknown_iface(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + interfaces: ['foo']''', expect_fail=True) + self.assertIn("br0: interface 'foo' is not defined", err) + + def test_bridge_multiple_assignments(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + br1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("br1: interface 'eno1' is already assigned to bridge br0", err) + + def test_bridge_invalid_dev_for_path_cost(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eth0: 50 + dhcp4: true''', expect_fail=True) + + def test_bridge_path_cost_already_defined(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eno1: 50 + eno1: 40 + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_path_cost(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eno1: aa + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_dev_for_port_prio(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eth0: 50 + dhcp4: true''', expect_fail=True) + + def test_bridge_port_prio_already_defined(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eno1: 50 + eno1: 40 + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_port_prio(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eno1: 257 + dhcp4: true''', expect_fail=True) + diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py new file mode 100644 index 0000000..538d6ce --- /dev/null +++ b/tests/generator/test_common.py @@ -0,0 +1,1244 @@ +# +# Common tests for netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import textwrap +import sys + +from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES + + +class TestNetworkd(TestBase): + '''networkd output''' + + def test_optional(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + optional: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Link] +RequiredForOnline=no + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + 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 test_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_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_optional_addresses_invalid(self): + eth_name = self.eth_name() + err = self.generate(self.config_with_optional_addresses(eth_name, '["invalid"]'), expect_fail=True) + self.assertIn('invalid value for optional-addresses', err) + + def test_mtu_all(self): + self.generate(textwrap.dedent(""" + network: + version: 2 + ethernets: + eth1: + mtu: 1280 + dhcp4: n + bonds: + bond0: + interfaces: + - eth1 + mtu: 9000 + vlans: + bond0.108: + link: bond0 + 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] +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]\nLinkLocalAddressing=no\nBond=bond0\n' + }) + self.assert_networkd_udev(None) + + def test_eth_global_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + eth0: + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + # should not allow NM to manage everything + self.assertFalse(os.path.exists(self.nm_enable_all_conf)) + + def test_eth_type_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + renderer: networkd + eth0: + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + self.assert_nm_udev(None) + + def test_eth_def_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + renderer: NetworkManager + eth0: + renderer: networkd + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + + def test_eth_dhcp6(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {dhcp6: true}''') + self.assert_networkd({'eth0.network': ND_DHCP6 % 'eth0'}) + + def test_eth_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + accept-ra: n''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=no + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp6_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + accept-ra: yes''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp6_accept_ra_unset(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp4_and_6(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {dhcp4: true, dhcp6: true}''') + self.assert_networkd({'eth0.network': ND_DHCPYES % 'eth0'}) + + def test_eth_manual_addresses(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 +'''}) + + def test_eth_manual_addresses_dhcp(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_dhcp_critical_true(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + critical: yes +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +CriticalConnection=true +RouteMetric=100 +UseMTU=true +'''}) + + def test_dhcp_identifier_mac(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: mac +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +ClientIdentifier=mac +RouteMetric=100 +UseMTU=true +'''}) + + def test_dhcp_identifier_duid(self): + # This option should be silently ignored, since it's the default + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: duid +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_ipv6_privacy(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + ipv6-privacy: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6PrivacyExtensions=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_gateway(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] + gateway4: 192.168.14.1 + gateway6: 2001:FFfe::2''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 +Gateway=192.168.14.1 +Gateway=2001:FFfe::2 +'''}) + + def test_nameserver(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, "1234::FFFF"] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + search: [lab, kitchen] + addresses: [8.8.8.8]''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +DNS=1.2.3.4 +DNS=1234::FFFF +''', + 'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 +DNS=8.8.8.8 +Domains=lab kitchen +'''}) + + 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] +RouteMetric=100 +UseMTU=true +'''}) + + 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] +RouteMetric=100 +UseMTU=true +'''}) + + 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] +RouteMetric=100 +UseMTU=true +'''}) + + 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] +RouteMetric=100 +UseMTU=true +'''}) + + +class TestNetworkManager(TestBase): + + def test_mtu_all(self): + self.generate(textwrap.dedent(""" + network: + version: 2 + renderer: NetworkManager + ethernets: + eth1: + mtu: 1280 + dhcp4: n + bonds: + bond0: + interfaces: + - eth1 + mtu: 9000 + vlans: + bond0.108: + link: bond0 + id: 108""")) + self.assert_nm({ + 'bond0.108': '''[connection] +id=netplan-bond0.108 +type=vlan +interface-name=bond0.108 + +[vlan] +id=108 +parent=bond0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bond0': '''[connection] +id=netplan-bond0 +type=bond +interface-name=bond0 + +[802-3-ethernet] +mtu=9000 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'eth1': '''[connection] +id=netplan-eth1 +type=ethernet +interface-name=eth1 +slave-type=bond +master=bond0 + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +mtu=1280 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + }) + + def test_eth_global_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + dhcp4: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_type_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + renderer: NetworkManager + eth0: + dhcp4: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_def_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + renderer: networkd + eth0: + renderer: NetworkManager''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_global_renderer_only(self): + self.generate(None, confs={'01-default-nm.yaml': 'network: {version: 2, renderer: NetworkManager}'}) + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + # but not configure anything else + self.assert_nm(None, None) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_dhcp6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp6: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +'''}) + + def test_eth_dhcp4_and_6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp4: true, dhcp6: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +'''}) + + def test_eth_manual_addresses(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: + - 192.168.14.2/24 + - 172.16.0.4/16 + - 2001:FFfe::1/64''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +address2=172.16.0.4/16 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_manual_addresses_dhcp(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +address1=192.168.14.2/24 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +'''}) + + def test_eth_ipv6_privacy(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp6: true, ipv6-privacy: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +ip6-privacy=2 +'''}) + + def test_gateway(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] + gateway4: 192.168.14.1 + gateway6: 2001:FFfe::2''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +gateway=192.168.14.1 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +gateway=2001:FFfe::2 +'''}) + + def test_nameserver(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"] + search: [lab, kitchen] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + addresses: [8.8.8.8]''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +dns=1.2.3.4;2.3.4.5; +dns-search=lab;kitchen; + +[ipv6] +method=manual +dns=1234::FFFF; +dns-search=lab;kitchen; +''', + 'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.1.3/24 +dns=8.8.8.8; + +[ipv6] +method=ignore +'''}) + + +class TestForwardDeclaration(TestBase): + + def test_fwdecl_bridge_on_bond(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: ['bond0'] + dhcp4: true + bonds: + bond0: + interfaces: ['eth0', 'eth1'] + ethernets: + eth0: + match: + macaddress: 00:01:02:03:04:05 + set-name: eth0 + eth1: + match: + macaddress: 02:01:02:03:04:05 + set-name: eth1 +''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 +''', + 'eth0.link': '''[Match] +MACAddress=00:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth0 +WakeOnLan=off +''', + 'eth0.network': '''[Match] +MACAddress=00:01:02:03:04:05 +Name=eth0 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +''', + 'eth1.link': '''[Match] +MACAddress=02:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth1 +WakeOnLan=off +''', + 'eth1.network': '''[Match] +MACAddress=02:01:02:03:04:05 +Name=eth1 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +'''}) + + def test_fwdecl_feature_blend(self): + self.generate('''network: + version: 2 + vlans: + vlan1: + link: 'br0' + id: 1 + dhcp4: true + bridges: + br0: + interfaces: ['bond0', 'eth2'] + parameters: + path-cost: + eth2: 1000 + bond0: 8888 + bonds: + bond0: + interfaces: ['eth0', 'br1'] + ethernets: + eth0: + match: + macaddress: 00:01:02:03:04:05 + set-name: eth0 + bridges: + br1: + interfaces: ['eth1'] + ethernets: + eth1: + match: + macaddress: 02:01:02:03:04:05 + set-name: eth1 + eth2: + match: + name: eth2 +''') + + self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n' + '[VLAN]\nId=1\n', + 'vlan1.network': '''[Match] +Name=vlan1 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' + '[Bridge]\nSTP=true\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +VLAN=vlan1 +''', + 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 + +[Bridge] +Cost=8888 +''', + 'eth2.network': '[Match]\nName=eth2\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' + '[Bridge]\nCost=1000\n', + 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n', + 'br1.network': '''[Match] +Name=br1 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bond=bond0 +''', + 'eth0.link': '''[Match] +MACAddress=00:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth0 +WakeOnLan=off +''', + 'eth0.network': '''[Match] +MACAddress=00:01:02:03:04:05 +Name=eth0 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +''', + 'eth1.link': '''[Match] +MACAddress=02:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth1 +WakeOnLan=off +''', + 'eth1.network': '''[Match] +MACAddress=02:01:02:03:04:05 +Name=eth1 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bridge=br1 +'''}) + + +class TestMerging(TestBase): + '''multiple *.yaml merging''' + + def test_global_backend(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: y''', + confs={'backend': 'network:\n renderer: NetworkManager'}) + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_add_def(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true''', + confs={'blue': '''network: + version: 2 + ethernets: + enblue: + dhcp4: true'''}) + + self.assert_networkd({'enblue.network': ND_DHCP4 % 'enblue', + 'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:enblue,interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_change_def(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + wakeonlan: true + dhcp4: false''', + confs={'green-dhcp': '''network: + version: 2 + ethernets: + engreen: + dhcp4: true'''}) + + self.assert_networkd({'engreen.link': '[Match]\nOriginalName=engreen\n\n[Link]\nWakeOnLan=magic\n', + 'engreen.network': ND_DHCP4 % 'engreen'}) + + def test_cleanup_old_config(self): + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true} + enyellow: {renderer: NetworkManager}''', + confs={'blue': '''network: + version: 2 + ethernets: + enblue: + dhcp4: true'''}) + + os.unlink(os.path.join(self.confdir, 'blue.yaml')) + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''') + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_ref(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute''', + confs={'bridges': '''network: + version: 2 + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true'''}) + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + def test_def_in_run(self): + rundir = os.path.join(self.workdir.name, 'run', 'netplan') + os.makedirs(rundir) + # override b.yaml definition for enred + with open(os.path.join(rundir, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enred: {dhcp4: true}}''') + + # append new definition for enblue + with open(os.path.join(rundir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enblue: {dhcp4: true}}''') + + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''', confs={'b': '''network: + version: 2 + ethernets: {enred: {wakeonlan: true}}'''}) + + # b.yaml in /run/ should completely shadow b.yaml in /etc, thus no enred.link + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', + 'enred.network': ND_DHCP4 % 'enred', + 'enblue.network': ND_DHCP4 % 'enblue'}) + + def test_def_in_lib(self): + libdir = os.path.join(self.workdir.name, 'lib', 'netplan') + rundir = os.path.join(self.workdir.name, 'run', 'netplan') + os.makedirs(libdir) + os.makedirs(rundir) + # b.yaml is in /etc/netplan too which should have precedence + with open(os.path.join(libdir, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {notme: {dhcp4: true}}''') + + # /run should trump /lib too + with open(os.path.join(libdir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {alsonot: {dhcp4: true}}''') + with open(os.path.join(rundir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enyellow: {dhcp4: true}}''') + + # this should be considered + with open(os.path.join(libdir, 'd.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enblue: {dhcp4: true}}''') + + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''', confs={'b': '''network: + version: 2 + ethernets: {enred: {wakeonlan: true}}'''}) + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', + 'enred.link': '[Match]\nOriginalName=enred\n\n[Link]\nWakeOnLan=magic\n', + 'enyellow.network': ND_DHCP4 % 'enyellow', + 'enblue.network': ND_DHCP4 % 'enblue'}) + diff --git a/tests/generator/test_dhcp_overrides.py b/tests/generator/test_dhcp_overrides.py new file mode 100644 index 0000000..f00c8ea --- /dev/null +++ b/tests/generator/test_dhcp_overrides.py @@ -0,0 +1,423 @@ +# +# Tests for DHCP override handling in netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import (TestBase, ND_DHCP4, ND_DHCP4_NOMTU, ND_DHCP6, ND_DHCP6_NOMTU, + ND_DHCPYES, ND_DHCPYES_NOMTU) + + +class TestNetworkd(TestBase): + + # Common tests for dhcp override booleans + def assert_dhcp_overrides_bool(self, override_name, networkd_name): + # dhcp4 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + + # dhcp6 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) + + # dhcp4 and dhcp6 both yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % (override_name, override_name)) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) + + # dhcp4 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=false\n' % networkd_name}) + + # dhcp6 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=false\n' % networkd_name}) + + # dhcp4 and dhcp6 both no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=false\n' % networkd_name}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + # Common tests for dhcp override strings + def assert_dhcp_overrides_string(self, override_name, networkd_name): + # dhcp4 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=foo\n' % networkd_name}) + + # dhcp6 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: foo +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=foo\n' % networkd_name}) + + # dhcp4 and dhcp6 + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo + dhcp6: yes + dhcp6-overrides: + %s: foo +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=foo\n' % networkd_name}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo + dhcp6: yes + dhcp6-overrides: + %s: bar +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + # Common tests for dhcp override booleans + def assert_dhcp_mtu_overrides_bool(self, override_name, networkd_name): + # dhcp4 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + + # dhcp6 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) + + # dhcp4 and dhcp6 both yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % (override_name, override_name)) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) + + # dhcp4 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4_NOMTU % 'engreen'}) + + # dhcp6 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6_NOMTU % 'engreen'}) + + # dhcp4 and dhcp6 both no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES_NOMTU % 'engreen'}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + def assert_dhcp_overrides_guint(self, override_name, networkd_name): + # dhcp4 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 6000 +''' % override_name) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # dhcp6 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: 6000 +''' % override_name) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # dhcp4 and dhcp6 + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 6000 + dhcp6: yes + dhcp6-overrides: + %s: 6000 +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 3333 + dhcp6: yes + dhcp6-overrides: + %s: 5555 +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + def test_dhcp_overrides_use_dns(self): + self.assert_dhcp_overrides_bool('use-dns', 'UseDNS') + + def test_dhcp_overrides_use_ntp(self): + self.assert_dhcp_overrides_bool('use-ntp', 'UseNTP') + + def test_dhcp_overrides_send_hostname(self): + self.assert_dhcp_overrides_bool('send-hostname', 'SendHostname') + + def test_dhcp_overrides_use_hostname(self): + self.assert_dhcp_overrides_bool('use-hostname', 'UseHostname') + + def test_dhcp_overrides_hostname(self): + self.assert_dhcp_overrides_string('hostname', 'Hostname') + + def test_dhcp_overrides_use_mtu(self): + self.assert_dhcp_mtu_overrides_bool('use-mtu', 'UseMTU') + + def test_dhcp_overrides_default_metric(self): + self.assert_dhcp_overrides_guint('route-metric', 'RouteMetric') + + +class TestNetworkManager(TestBase): + + def test_override_default_metric_v4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + route-metric: 3333 +''') + # silently ignored since yes is the default + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +route-metric=3333 + +[ipv6] +method=ignore +'''}) + + def test_override_default_metric_v6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + dhcp6-overrides: + route-metric: 6666 +''') + # silently ignored since yes is the default + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +route-metric=6666 +'''}) diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py new file mode 100644 index 0000000..e1909e1 --- /dev/null +++ b/tests/generator/test_errors.py @@ -0,0 +1,674 @@ +# +# Tests for common invalid syntax/errors in config +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase + + +class TestConfigErrors(TestBase): + def test_malformed_yaml(self): + err = self.generate('network:\n version: %&', expect_fail=True) + self.assertIn('Invalid YAML', err) + self.assertIn('found character that cannot start any token', err) + + def test_wrong_indent(self): + err = self.generate('network:\n version: 2\n foo: *', expect_fail=True) + self.assertIn('Invalid YAML', err) + self.assertIn('inconsistent indentation', err) + + def test_yaml_expected_scalar(self): + err = self.generate('network:\n version: {}', expect_fail=True) + self.assertIn('expected scalar', err) + + def test_yaml_expected_sequence(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + interfaces: {}''', expect_fail=True) + self.assertIn('expected sequence', err) + + def test_yaml_expected_mapping(self): + err = self.generate('network:\n version', expect_fail=True) + self.assertIn('expected mapping', err) + + def test_invalid_bool(self): + err = self.generate('''network: + version: 2 + ethernets: + id0: + wakeonlan: wut +''', expect_fail=True) + self.assertIn("invalid boolean value 'wut'", err) + + def test_invalid_version(self): + err = self.generate('network:\n version: 1', expect_fail=True) + self.assertIn('Only version 2 is supported', err) + + def test_id_redef_type_mismatch(self): + err = self.generate('''network: + version: 2 + ethernets: + id0: + wakeonlan: true''', + confs={'redef': '''network: + version: 2 + bridges: + id0: + wakeonlan: true'''}, expect_fail=True) + self.assertIn("Updated definition 'id0' changes device type", err) + + def test_set_name_without_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + set-name: lom1 +''', expect_fail=True) + self.assertIn("def1: 'set-name:' requires 'match:' properties", err) + + def test_virtual_set_name(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + set_name: br1''', expect_fail=True) + self.assertIn("unknown key 'set_name'", err) + + def test_virtual_match(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + match: + driver: foo''', expect_fail=True) + self.assertIn("unknown key 'match'", err) + + def test_virtual_wol(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + wakeonlan: true''', expect_fail=True) + self.assertIn("unknown key 'wakeonlan'", err) + + def test_unknown_global_renderer(self): + err = self.generate('''network: + version: 2 + renderer: bogus +''', expect_fail=True) + self.assertIn("unknown renderer 'bogus'", err) + + def test_unknown_type_renderer(self): + err = self.generate('''network: + version: 2 + ethernets: + renderer: bogus +''', expect_fail=True) + self.assertIn("unknown renderer 'bogus'", err) + + def test_invalid_id(self): + err = self.generate('''network: + version: 2 + ethernets: + "eth 0": + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid name 'eth 0'", err) + + def test_invalid_name_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: | + fo o + bar + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid name 'fo o\nbar\n'", err) + + def test_invalid_mac_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + match: + macaddress: 00:11:ZZ + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid MAC address '00:11:ZZ', must be XX:XX:XX:XX:XX:XX", err) + + def test_glob_in_id(self): + err = self.generate('''network: + version: 2 + ethernets: + en*: + dhcp4: true''', expect_fail=True) + self.assertIn("Definition ID 'en*' must not use globbing", err) + + def test_wifi_duplicate_ssid(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "s3kr1t" + workplace: + password: "c0mpany" + dhcp4: yes''', expect_fail=True) + self.assertIn("wl0: Duplicate access point SSID 'workplace'", err) + + def test_wifi_no_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + dhcp4: yes''', expect_fail=True) + self.assertIn('wl0: No access points defined', err) + + def test_wifi_empty_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: {} + dhcp4: yes''', expect_fail=True) + self.assertIn('wl0: No access points defined', err) + + def test_wifi_ap_unknown_key(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + something: false + dhcp4: yes''', expect_fail=True) + self.assertIn("unknown key 'something'", err) + + def test_wifi_ap_unknown_mode(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + mode: bogus''', expect_fail=True) + self.assertIn("unknown wifi mode 'bogus'", err) + + def test_invalid_ipv4_address(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("malformed address '192.168.14/24', must be X.X.X.X/NN", err) + + def test_missing_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1''', expect_fail=True) + + self.assertIn("address '192.168.14.1' is missing /prefixlength", err) + + def test_empty_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1/''', expect_fail=True) + + self.assertIn("invalid prefix length in address '192.168.14.1/'", err) + + def test_invalid_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1/33''', expect_fail=True) + + self.assertIn("invalid prefix length in address '192.168.14.1/33'", err) + + def test_invalid_ipv6_address(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001:G::1/64''', expect_fail=True) + + self.assertIn("malformed address '2001:G::1/64', must be X.X.X.X/NN", err) + + def test_missing_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1''', expect_fail=True) + self.assertIn("address '2001::1' is missing /prefixlength", err) + + def test_invalid_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1/129''', expect_fail=True) + self.assertIn("invalid prefix length in address '2001::1/129'", err) + + def test_empty_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1/''', expect_fail=True) + self.assertIn("invalid prefix length in address '2001::1/'", err) + + def test_invalid_gateway4(self): + for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + gateway4: %s''' % a, expect_fail=True) + self.assertIn("invalid IPv4 address '%s'" % a, err) + + def test_invalid_gateway6(self): + for a in ['1234', '1:::c', '1234::1/50']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + gateway6: %s''' % a, expect_fail=True) + self.assertIn("invalid IPv6 address '%s'" % a, err) + + def test_invalid_nameserver_ipv4(self): + for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: [%s]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + + def test_invalid_nameserver_ipv6(self): + for a in ['1234', '1:::c', '1234::1/50']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: ["%s"]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + + def test_vlan_missing_id(self): + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {link: en1}''', expect_fail=True) + self.assertIn("missing 'id' property", err) + + def test_vlan_invalid_id(self): + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {id: a, link: en1}''', expect_fail=True) + self.assertIn("invalid unsigned int value 'a'", err) + + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {id: 4095, link: en1}''', expect_fail=True) + self.assertIn("invalid id '4095'", err) + + def test_vlan_missing_link(self): + err = self.generate('''network: + version: 2 + vlans: + ena: {id: 1}''', expect_fail=True) + self.assertIn("ena: missing 'link' property", err) + + def test_vlan_unknown_link(self): + err = self.generate('''network: + version: 2 + vlans: + ena: {id: 1, link: en1}''', expect_fail=True) + self.assertIn("ena: interface 'en1' is not defined", err) + + def test_device_bad_route_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: badlocation + via: 192.168.14.20 + metric: 100 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_bad_route_via(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: badgateway + metric: 100 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_bad_route_metric(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + metric: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_family_mismatch_ipv6_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 2001:dead:beef::0/16 + via: 10.1.1.1 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_family_mismatch_ipv4_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 10.10.10.0/24 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_missing_via(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 2001:dead:beef::2 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_type_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + type: prohibit + metric: 1 + addresses: + - 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 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + on-link: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + table: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_type(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + type: thisisinvalidtype + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_scope(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + scope: linky + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_mismatched_addresses(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + to: 2000:dead:beef::3/64 + table: 50 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_missing_address(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - table: 50 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_tos(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + type-of-service: 256 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_prio(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + priority: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + table: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_fwmark(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + mark: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_address(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - to: 10.10.10.0/24 + from: someinvalidaddress + mark: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_invalid_dhcp_identifier(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: invalid''', expect_fail=True) + + def test_invalid_accept_ra(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + 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) + + def test_invalid_yaml_tabs(self): + err = self.generate('''\t''', expect_fail=True) + self.assertIn("tabs are not allowed for indent", err) + + def test_invalid_yaml_undefined_alias(self): + err = self.generate('''network: + version: 2 + ethernets: + *engreen: + dhcp4: yes''', expect_fail=True) + self.assertIn("aliases are not supported", err) + + def test_invalid_yaml_undefined_alias_at_eof(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: *yes''', expect_fail=True) + self.assertIn("aliases are not supported", err) + diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py new file mode 100644 index 0000000..3cf456d --- /dev/null +++ b/tests/generator/test_ethernets.py @@ -0,0 +1,565 @@ +# +# Tests for ethernet devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, UDEV_MAC_RULE, UDEV_NO_MAC_RULE + + +class TestNetworkd(TestBase): + + def test_eth_wol(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + wakeonlan: true + dhcp4: n''') + + self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + # should not allow NM to manage everything + self.assertFalse(os.path.exists(self.nm_enable_all_conf)) + + def test_eth_mtu(self): + self.generate('''network: + version: 2 + ethernets: + eth1: + mtu: 1280 + dhcp4: n''') + + self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) + self.assert_networkd_udev(None) + + def test_eth_match_by_driver_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + driver: ixgbe + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) + # NM cannot match by driver, so blacklisting needs to happen via udev + self.assert_nm(None, None) + self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') + + def test_eth_match_by_mac_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=mac:11:22:33:44:55:66,''') + self.assert_nm_udev(None) + + def test_eth_implicit_name_match_dhcp4(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: y''') + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_networkd_udev(None) + + def test_eth_match_dhcp4(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + driver: ixgbe + dhcp4: true''') + + self.assert_networkd({'def1.network': '''[Match] +Driver=ixgbe + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_networkd_udev(None) + self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') + + def test_eth_match_name(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + dhcp4: true''') + + self.assert_networkd({'def1.network': ND_DHCP4 % 'green'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:green,''') + self.assert_nm_udev(None) + + def test_eth_set_mac(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd({'def1.network': ND_DHCP4 % 'green', + 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' + }) + self.assert_networkd_udev(None) + + def test_eth_match_name_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + set-name: blue + dhcp4: true''') + + # the .network needs to match on the renamed name + self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n', + 'def1.network': ND_DHCP4 % 'blue'}) + + # The udev rules engine does support renaming by name + self.assert_networkd_udev(None) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:blue,''') + + def test_eth_match_all_names(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: {name: "*"} + dhcp4: true''') + + self.assert_networkd({'def1.network': ND_DHCP4 % '*'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:*,''') + self.assert_nm_udev(None) + + def test_eth_match_all(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: {} + dhcp4: true''') + + self.assert_networkd({'def1.network': '[Match]\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n' + '[DHCP]\nRouteMetric=100\nUseMTU=true\n'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=type:ethernet,''') + self.assert_nm_udev(None) + + def test_match_multiple(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: en1s* + macaddress: 00:11:22:33:44:55 + dhcp4: on''') + self.assert_networkd({'def1.network': '''[Match] +MACAddress=00:11:22:33:44:55 +Name=en1s* + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=mac:00:11:22:33:44:55,''') + + +class TestNetworkManager(TestBase): + + def test_eth_wol(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + wakeonlan: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=1 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) + self.assert_nm_udev(None) + + def test_eth_mtu(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth1: + mtu: 1280 + dhcp4: n''') + + self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) + self.assert_nm({'eth1': '''[connection] +id=netplan-eth1 +type=ethernet +interface-name=eth1 + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +mtu=1280 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_eth_set_mac(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd({'eth0.link': '''[Match] +OriginalName=eth0 + +[Link] +WakeOnLan=off +MACAddress=00:01:02:03:04:05 +'''}) + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +cloned-mac-address=00:01:02:03:04:05 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_eth_match_by_driver(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + driver: ixgbe''', expect_fail=True) + self.assertIn('NetworkManager definitions do not support matching by driver', err) + + def test_eth_match_by_driver_rename(self): + # in this case udev will rename the device so that NM can use the name + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + driver: ixgbe + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=lom1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_nm_udev(None) + + def test_eth_match_by_mac_rename(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=lom1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_nm_udev(None) + + def test_eth_implicit_name_match_dhcp4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_eth_match_mac_dhcp4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +mac-address=11:22:33:44:55:66 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_eth_match_name(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: green + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=green + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_match_name_rename(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: green + set-name: blue + dhcp4: true''') + + # The udev rules engine does support renaming by name + self.assert_networkd_udev(None) + + # NM needs to match on the renamed name + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=blue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + # ... while udev renames it + self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n'}) + self.assert_nm_udev(None) + + def test_eth_match_name_glob(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: {name: "en*"} + dhcp4: true''', expect_fail=True) + self.assertIn('def1: NetworkManager definitions do not support name globbing', err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_eth_match_all(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: {} + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_match_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: engreen + macaddress: 00:11:22:33:44:55 + dhcp4: yes''') + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +mac-address=00:11:22:33:44:55 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py new file mode 100644 index 0000000..955ee61 --- /dev/null +++ b/tests/generator/test_routing.py @@ -0,0 +1,932 @@ +# +# Routing / IP rule tests for netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_route_v4_single(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_multiple(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 8.8.0.0/16 + via: 192.168.1.1 + - to: 10.10.10.8 + via: 192.168.1.2 + metric: 5000 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=8.8.0.0/16 +Gateway=192.168.1.1 + +[Route] +Destination=10.10.10.8 +Gateway=192.168.1.2 +Metric=5000 + +[Route] +Destination=11.11.11.0/24 +Gateway=192.168.1.3 +Metric=9999 +'''}) + + def test_route_v4_onlink(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + on-link: true + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +GatewayOnlink=true +Metric=100 +'''}) + + def test_route_v4_onlink_no(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + on-link: n + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_scope(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + scope: link + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Scope=link +Metric=100 +'''}) + + def test_route_v4_scope_redefine(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + scope: host + via: 192.168.14.20 + scope: link + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +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 +'''}) + + def test_route_v4_type_blackhole(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + type: blackhole + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Type=blackhole +Metric=100 +'''}) + + def test_route_v4_type_redefine(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + type: prohibit + via: 192.168.14.20 + type: unicast + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + table: 201 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +Table=201 +'''}) + + def test_route_v4_from(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + from: 192.168.14.2 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +PreferredSource=192.168.14.2 +Metric=100 +'''}) + + def test_route_v6_single(self): + self.generate('''network: + version: 2 + ethernets: + enblue: + addresses: ["192.168.1.3/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1''') + + self.assert_networkd({'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +'''}) + + def test_route_v6_type(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + type: prohibit''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +Type=prohibit +'''}) + + def test_route_v6_scope_host(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + scope: host''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +Scope=host +'''}) + + def test_route_v6_multiple(self): + self.generate('''network: + version: 2 + ethernets: + enblue: + addresses: ["192.168.1.3/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + - to: 2001:f00f:f00f::fe/64 + via: 2001:beef:feed::1 + metric: 1024''') + + self.assert_networkd({'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 + +[Route] +Destination=2001:f00f:f00f::fe/64 +Gateway=2001:beef:feed::1 +Metric=1024 +'''}) + + def test_ip_rule_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + table: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +Table=100 +'''}) + + def test_ip_rule_priority(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + priority: 99 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +Priority=99 +'''}) + + def test_ip_rule_fwmark(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - from: 10.10.10.0/24 + mark: 50 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +From=10.10.10.0/24 +FirewallMark=50 +'''}) + + def test_ip_rule_tos(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + type-of-service: 250 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +TypeOfService=250 +'''}) + + def test_use_routes(self): + """[networkd] Validate config generation when use-routes DHCP override is used""" + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true + dhcp4-overrides: + use-routes: false + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +UseRoutes=false +'''}) + + def test_default_metric(self): + """[networkd] Validate config generation when metric DHCP override is used""" + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp4-overrides: + route-metric: 3333 + dhcp6-overrides: + route-metric: 3333 + enred: + dhcp4: true + dhcp6: true + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=3333 +UseMTU=true +''', + 'enred.network': '''[Match] +Name=enred + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + +class TestNetworkManager(TestBase): + + def test_route_v4_single(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + metric: 100 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.14.20,100 + +[ipv6] +method=ignore +'''}) + + def test_route_v4_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 8.8.0.0/16 + via: 192.168.1.1 + metric: 5000 + - to: 10.10.10.8 + via: 192.168.1.2 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=8.8.0.0/16,192.168.1.1,5000 +route2=10.10.10.8,192.168.1.2 +route3=11.11.11.0/24,192.168.1.3,9999 + +[ipv6] +method=ignore +'''}) + + def test_route_v6_single(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enblue: + addresses: ["2001:f00f:f00f::2/64"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1''') + + self.assert_nm({'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=manual +address1=2001:f00f:f00f::2/64 +route1=2001:dead:beef::2/64,2001:beef:beef::1 +'''}) + + def test_route_v6_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enblue: + addresses: ["2001:f00f:f00f::2/64"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + - to: 2001:dead:feed::2/64 + via: 2001:beef:beef::2 + metric: 1000''') + + self.assert_nm({'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=manual +address1=2001:f00f:f00f::2/64 +route1=2001:dead:beef::2/64,2001:beef:beef::1 +route2=2001:dead:feed::2/64,2001:beef:beef::2,1000 +'''}) + + def test_routes_mixed(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:f00f::2/128"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + metric: 997 + - to: 8.8.0.0/16 + via: 192.168.1.1 + metric: 5000 + - to: 10.10.10.8 + via: 192.168.1.2 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + - to: 2001:f00f:f00f::fe/64 + via: 2001:beef:feed::1 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=8.8.0.0/16,192.168.1.1,5000 +route2=10.10.10.8,192.168.1.2 +route3=11.11.11.0/24,192.168.1.3,9999 + +[ipv6] +method=manual +address1=2001:f00f::2/128 +route1=2001:dead:beef::2/64,2001:beef:beef::1,997 +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_use_routes_v4(self): + """[NetworkManager] Validate config when use-routes DHCP4 override is used""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp4-overrides: + use-routes: false + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +ignore-auto-routes=true +never-default=true + +[ipv6] +method=ignore +'''}) + + def test_use_routes_v6(self): + """[NetworkManager] Validate config when use-routes DHCP6 override is used""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp6-overrides: + use-routes: false + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +ignore-auto-routes=true +never-default=true +'''}) + + def test_default_metric_v4(self): + """[NetworkManager] Validate config when setting a default metric for DHCPv4""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp4-overrides: + route-metric: 4000 + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +route-metric=4000 + +[ipv6] +method=auto +'''}) + + def test_default_metric_v6(self): + """[NetworkManager] Validate config when setting a default metric for DHCPv6""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp6-overrides: + route-metric: 5050 + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +route-metric=5050 +'''}) diff --git a/tests/generator/test_tunnels.py b/tests/generator/test_tunnels.py new file mode 100644 index 0000000..8399f84 --- /dev/null +++ b/tests/generator/test_tunnels.py @@ -0,0 +1,1066 @@ +# +# Tests for tunnel devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from .base import TestBase + +def prepare_config_for_mode(renderer, mode, key=None): + config = """network: + version: 2 + renderer: {} +""".format(renderer) + config += ''' + ethernets: + en1: {} +''' + + if mode == "ip6gre" \ + or mode == "ip6ip6" \ + or mode == "vti6" \ + or mode == "ipip6" \ + or mode == "ip6gretap": + local_ip = "fe80::dead:beef" + remote_ip = "2001:fe:ad:de:ad:be:ef:1" + else: + local_ip = "10.10.10.10" + remote_ip = "20.20.20.20" + + config += """ + tunnels: + tun0: + mode: {} + local: {} + remote: {} + addresses: [ 15.15.15.15/24 ] + gateway4: 20.20.20.21 +""".format(mode, local_ip, remote_ip) + + # Handle key/keys as str or dict as required by the test + if type(key) is str: + config += """ + key: {} +""".format(key) + elif type(key) is dict: + config += """ + keys: + input: {} + output: {} +""".format(key['input'], key['output']) + + return config + + +class TestNetworkd(TestBase): + + def test_sit(self): + """[networkd] Validate generation of SIT tunnels""" + config = prepare_config_for_mode('networkd', 'sit') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=sit + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_sit_he(self): + """[networkd] Validate generation of SIT tunnels (HE example)""" + # Test specifically a config like one that would enable Hurricane + # Electric IPv6 tunnels. + config = '''network: + version: 2 + renderer: networkd + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" # provided by HE as routed /64 + gateway4: 1.1.1.254 + tunnels: + he-ipv6: + mode: sit + remote: 2.2.2.2 + local: 1.1.1.1 + addresses: + - "2001:dead:beef::2/64" + gateway6: "2001:dead:beef::1" +''' + self.generate(config) + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +LinkLocalAddressing=ipv6 +Address=1.1.1.1/24 +Address=2001:cafe:face::1/64 +Gateway=1.1.1.254 +''', + 'he-ipv6.netdev': '''[NetDev] +Name=he-ipv6 +Kind=sit + +[Tunnel] +Independent=true +Local=1.1.1.1 +Remote=2.2.2.2 +''', + 'he-ipv6.network': '''[Match] +Name=he-ipv6 + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:dead:beef::2/64 +Gateway=2001:dead:beef::1 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti(self): + """[networkd] Validate generation of VTI tunnels""" + config = prepare_config_for_mode('networkd', 'vti') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_with_key_str(self): + """[networkd] Validate generation of VTI tunnels with input/output keys""" + config = prepare_config_for_mode('networkd', 'vti', key='1.1.1.1') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +InputKey=1.1.1.1 +OutputKey=1.1.1.1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_with_key_dict(self): + """[networkd] Validate generation of VTI tunnels with key dict""" + config = prepare_config_for_mode('networkd', 'vti', key={'input': 1234, 'output': 5678}) + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +InputKey=1234 +OutputKey=5678 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_invalid_key(self): + """[networkd] Validate VTI tunnel generation key handling""" + config = prepare_config_for_mode('networkd', 'vti', key='invalid') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) + + def test_vti6(self): + """[networkd] Validate generation of VTI6 tunnels""" + config = prepare_config_for_mode('networkd', 'vti6') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti6 + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti6_with_key(self): + """[networkd] Validate generation of VTI6 tunnels with input/output keys""" + config = prepare_config_for_mode('networkd', 'vti6', key='1.1.1.1') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti6 + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +InputKey=1.1.1.1 +OutputKey=1.1.1.1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti6_invalid_key(self): + """[networkd] Validate VTI6 tunnel generation key handling""" + config = prepare_config_for_mode('networkd', 'vti6', key='invalid') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) + + def test_ipip6(self): + """[networkd] Validate generation of IPIP6 tunnels""" + config = prepare_config_for_mode('networkd', 'ipip6') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6tnl + +[Tunnel] +Independent=true +Mode=ipip6 +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ipip(self): + """[networkd] Validate generation of IPIP tunnels""" + config = prepare_config_for_mode('networkd', 'ipip') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ipip + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_isatap(self): + """[networkd] Warning for ISATAP tunnel generation not supported""" + config = prepare_config_for_mode('networkd', 'isatap') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: ISATAP tunnel mode is not supported", out) + + def test_gre(self): + """[networkd] Validate generation of GRE tunnels""" + config = prepare_config_for_mode('networkd', 'gre') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=gre + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ip6gre(self): + """[networkd] Validate generation of IP6GRE tunnels""" + config = prepare_config_for_mode('networkd', 'ip6gre') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6gre + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_gretap(self): + """[networkd] Validate generation of GRETAP tunnels""" + config = prepare_config_for_mode('networkd', 'gretap') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=gretap + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ip6gretap(self): + """[networkd] Validate generation of IP6GRETAP tunnels""" + config = prepare_config_for_mode('networkd', 'ip6gretap') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6gretap + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + +class TestNetworkManager(TestBase): + + def test_isatap(self): + """[NetworkManager] Validate ISATAP tunnel generation""" + config = prepare_config_for_mode('NetworkManager', 'isatap') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=4 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_sit(self): + """[NetworkManager] Validate generation of SIT tunnels""" + config = prepare_config_for_mode('NetworkManager', 'sit') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=3 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_sit_he(self): + """[NetworkManager] Validate generation of SIT tunnels (HE example)""" + # Test specifically a config like one that would enable Hurricane + # Electric IPv6 tunnels. + config = '''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" # provided by HE as routed /64 + gateway4: 1.1.1.254 + tunnels: + he-ipv6: + mode: sit + remote: 2.2.2.2 + local: 1.1.1.1 + addresses: + - "2001:dead:beef::2/64" + gateway6: "2001:dead:beef::1" +''' + self.generate(config) + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=1.1.1.1/24 +gateway=1.1.1.254 + +[ipv6] +method=manual +address1=2001:cafe:face::1/64 +''', + 'he-ipv6': '''[connection] +id=netplan-he-ipv6 +type=ip-tunnel +interface-name=he-ipv6 + +[ip-tunnel] +mode=3 +local=1.1.1.1 +remote=2.2.2.2 + +[ipv4] +method=disabled + +[ipv6] +method=manual +address1=2001:dead:beef::2/64 +gateway=2001:dead:beef::1 +'''}) + + def test_vti(self): + """[NetworkManager] Validate generation of VTI tunnels""" + config = prepare_config_for_mode('NetworkManager', 'vti') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=5 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_vti6(self): + """[NetworkManager] Validate generation of VTI6 tunnels""" + config = prepare_config_for_mode('NetworkManager', 'vti6') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=9 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6ip6(self): + """[NetworkManager] Validate generation of IP6IP6 tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ip6ip6') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=6 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ipip(self): + """[NetworkManager] Validate generation of IPIP tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ipip') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=1 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_gre(self): + """[NetworkManager] Validate generation of GRE tunnels""" + config = prepare_config_for_mode('NetworkManager', 'gre') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=2 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_gre_with_keys(self): + """[NetworkManager] Validate generation of GRE tunnels with keys""" + config = prepare_config_for_mode('NetworkManager', 'gre', key={'input': 1111, 'output': 5555}) + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=2 +local=10.10.10.10 +remote=20.20.20.20 +input-key=1111 +output-key=5555 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6gre(self): + """[NetworkManager] Validate generation of IP6GRE tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ip6gre') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=8 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6gre_with_key(self): + """[NetworkManager] Validate generation of IP6GRE tunnels with key""" + config = prepare_config_for_mode('NetworkManager', 'ip6gre', key='9999') + self.generate(config) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=8 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 +input-key=9999 +output-key=9999 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + +class TestConfigErrors(TestBase): + + def test_missing_mode(self): + """Fail if tunnel mode is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + remote: 20.20.20.20 + local: 10.10.10.10 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'mode' property for tunnel", out) + + def test_invalid_mode(self): + """Ensure an invalid tunnel mode shows an error message""" + config = prepare_config_for_mode('networkd', 'invalid') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: tunnel mode 'invalid' is not supported", out) + + def test_invalid_mode_for_nm(self): + """Show an error if a mode is selected that can't be handled by the renderer""" + config = prepare_config_for_mode('NetworkManager', 'gretap') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: GRETAP tunnel mode is not supported by NetworkManager", out) + + def test_malformed_tunnel_ip(self): + """Fail if local/remote IP for tunnel are malformed""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 + local: 10.10.1invalid +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: malformed address '10.10.1invalid', must be X.X.X.X or X:X:X:X:X:X:X:X", out) + + def test_cidr_tunnel_ip(self): + """Fail if local/remote IP for tunnel include /prefix""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 + local: 10.10.10.10/21 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: address '10.10.10.10/21' should not include /prefixlength", out) + + def test_missing_local_ip(self): + """Fail if local IP is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'local' property for tunnel", out) + + def test_missing_remote_ip(self): + """Fail if remote IP is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'remote' property for tunnel", out) + + def test_wrong_local_ip_for_mode_v4(self): + """Show an error when an IPv6 local addr is used for an IPv4 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: fe80::2 + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv4 address for this tunnel type", out) + + def test_wrong_remote_ip_for_mode_v4(self): + """Show an error when an IPv6 remote addr is used for an IPv4 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: 10.10.10.10 + remote: 2006::1 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv4 address for this tunnel type", out) + + def test_wrong_local_ip_for_mode_v6(self): + """Show an error when an IPv4 local addr is used for an IPv6 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ip6gre + local: 10.10.10.10 + remote: 2001::3 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv6 address for this tunnel type", out) + + def test_wrong_remote_ip_for_mode_v6(self): + """Show an error when an IPv4 remote addr is used for an IPv6 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ip6gre + local: 2001::face + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv6 address for this tunnel type", out) + + def test_malformed_keys(self): + """Show an error if tunnel keys stanza is malformed""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + - input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid type for 'keys'", out) + + def test_networkd_invalid_input_key_use(self): + """[networkd] Show an error if input-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: networkd + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) + + def test_networkd_invalid_output_key_use(self): + """[networkd] Show an error if output-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: networkd + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + output: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) + + def test_nm_invalid_input_key_use(self): + """[NetworkManager] Show an error if input-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: NetworkManager + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) + + def test_nm_invalid_output_key_use(self): + """[NetworkManager] Show an error if output-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: NetworkManager + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + output: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py new file mode 100644 index 0000000..bbd4797 --- /dev/null +++ b/tests/generator/test_vlans.py @@ -0,0 +1,223 @@ +# +# Tests for VLAN devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import re + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_vlan(self): + self.generate('''network: + version: 2 + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + addresses: [1.2.3.4/24] + enred: + id: 3 + link: en1 + macaddress: aa:bb:cc:dd:ee:11 + engreen: {id: 2, link: en1, dhcp6: true}''') + + 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] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,interface-name:en1,interface-name:enblue,interface-name:enred,''') + self.assert_nm_udev(None) + + +class TestNetworkManager(TestBase): + + def test_vlan(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + addresses: [1.2.3.4/24] + engreen: {id: 2, link: en1, dhcp6: true}''') + + self.assert_networkd({}) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'enblue': '''[connection] +id=netplan-enblue +type=vlan +interface-name=enblue + +[vlan] +id=1 +parent=en1 + +[ipv4] +method=manual +address1=1.2.3.4/24 + +[ipv6] +method=ignore +''', + 'engreen': '''[connection] +id=netplan-engreen +type=vlan +interface-name=engreen + +[vlan] +id=2 +parent=en1 + +[ipv4] +method=link-local + +[ipv6] +method=auto +'''}) + self.assert_nm_udev(None) + + def test_vlan_parent_match(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + en-v: + match: {macaddress: "11:22:33:44:55:66"} + vlans: + engreen: {id: 2, link: en-v, dhcp4: true}''') + + self.assert_networkd({}) + + # get assigned UUID from en-v connection + with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-en-v')) as f: + m = re.search('uuid=([0-9a-fA-F-]{36})\n', f.read()) + self.assertTrue(m) + uuid = m.group(1) + self.assertNotEquals(uuid, "00000000-0000-0000-0000-000000000000") + + self.assert_nm({'en-v': '''[connection] +id=netplan-en-v +type=ethernet +uuid=%s + +[ethernet] +wake-on-lan=0 + +[802-3-ethernet] +mac-address=11:22:33:44:55:66 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''' % uuid, + 'engreen': '''[connection] +id=netplan-engreen +type=vlan +interface-name=engreen + +[vlan] +id=2 +parent=%s + +[ipv4] +method=auto + +[ipv6] +method=ignore +''' % uuid}) + self.assert_nm_udev(None) + diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py new file mode 100644 index 0000000..6bebe57 --- /dev/null +++ b/tests/generator/test_wifis.py @@ -0,0 +1,318 @@ +# +# Tests for VLAN devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import stat +import sys + +from .base import TestBase, ND_WIFI_DHCP4 + + +class TestNetworkd(TestBase): + + def test_wifi(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s3kr1t" + workplace: + password: "c0mpany" + peer2peer: + mode: adhoc + dhcp4: yes''') + + self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant + +network={ + ssid="Joe's Home" + key_mgmt=WPA-PSK + psk="s3kr1t" +} +network={ + ssid="workplace" + key_mgmt=WPA-PSK + psk="c0mpany" +} +network={ + ssid="peer2peer" + mode=1 + key_mgmt=NONE +} +''') + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/multi-user.target.wants/netplan-wpa@wl0.service'))) + + def test_wifi_route(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "c0mpany" + dhcp4: yes + routes: + - to: 10.10.10.0/24 + via: 8.8.8.8''') + + self.assert_networkd({'wl0.network': '''[Match] +Name=wl0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[Route] +Destination=10.10.10.0/24 +Gateway=8.8.8.8 + +[DHCP] +RouteMetric=600 +UseMTU=true +'''}) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + def test_wifi_match(self): + err = self.generate('''network: + version: 2 + wifis: + somewifi: + match: + driver: foo + access-points: + workplace: + password: "c0mpany" + dhcp4: yes''', expect_fail=True) + self.assertIn('networkd backend does not support wifi with match:', err) + + def test_wifi_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "c0mpany" + mode: ap + dhcp4: yes''', expect_fail=True) + self.assertIn('networkd does not support wifi in access point mode', err) + + +class TestNetworkManager(TestBase): + + def test_wifi_default(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + "Joe's Home": + password: "s3kr1t" + workplace: + password: "c0mpany" + dhcp4: yes''') + + self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] +id=netplan-wl0-Joe's Home +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Joe's Home +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=s3kr1t +''', + 'wl0-workplace': '''[connection] +id=netplan-wl0-workplace +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=c0mpany +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_wifi_match_mac(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + all: + match: + macaddress: 11:22:33:44:55:66 + access-points: + workplace: {}''') + + self.assert_nm({'all-workplace': '''[connection] +id=netplan-all-workplace +type=wifi + +[ethernet] +wake-on-lan=0 + +[802-11-wireless] +mac-address=11:22:33:44:55:66 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure +'''}) + + def test_wifi_match_all(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + all: + match: {} + access-points: + workplace: {mode: infrastructure}''') + + self.assert_nm({'all-workplace': '''[connection] +id=netplan-all-workplace +type=wifi + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure +'''}) + + def test_wifi_ap(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + homenet: + mode: ap + password: s3cret''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=shared + +[ipv6] +method=ignore + +[wifi] +ssid=homenet +mode=ap + +[wifi-security] +key-mgmt=wpa-psk +psk=s3cret +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_wifi_adhoc(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + homenet: + mode: adhoc''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=homenet +mode=adhoc +'''}) + diff --git a/tests/integration.py b/tests/integration.py deleted file mode 100755 index 9bc6675..0000000 --- a/tests/integration.py +++ /dev/null @@ -1,2142 +0,0 @@ -#!/usr/bin/python3 -# System integration tests of netplan-generate. NM and networkd are -# started on the generated configuration, using emulated ethernets (veth) and -# Wifi (mac80211-hwsim). These need to be run in a VM and do change the system -# configuration. -# -# Copyright (C) 2016 Canonical, Ltd. -# Author: Martin Pitt <martin.pitt@ubuntu.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import sys -import re -import time -import subprocess -import tempfile -import unittest -import shutil - -for program in ['wpa_supplicant', 'hostapd', 'dnsmasq']: - if subprocess.call(['which', program], stdout=subprocess.PIPE) != 0: - sys.stderr.write('%s is required for this test suite, but not available. Skipping\n' % program) - sys.exit(0) - -nm_uses_dnsmasq = b'dns=dnsmasq' in subprocess.check_output(['NetworkManager', '--print-config']) - - -def resolved_in_use(): - return os.path.isfile('/run/systemd/resolve/resolv.conf') - - -class NetworkTestBase(unittest.TestCase): - '''Common functionality for network test cases - - setUp() creates two test wlan devices, one for a simulated access point - (self.dev_w_ap), the other for a simulated client device - (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} - and self.dev_e2_{ap,client}. - - Each test should call self.setup_ap() or self.setup_eth() with the desired - configuration. - ''' - @classmethod - def setUpClass(klass): - # ensure we have this so that iw works - subprocess.check_call(['modprobe', 'cfg80211']) - - # ensure NM can manage our fake eths - os.makedirs('/run/udev/rules.d', exist_ok=True) - with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: - f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') - subprocess.check_call(['udevadm', 'control', '--reload']) - - # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels - out = subprocess.check_output(['iw', 'reg', 'get'], universal_newlines=True) - m = re.match(r'^(?:global\n)?country (\S+):', out) - assert m - klass.orig_country = m.group(1) - subprocess.check_call(['iw', 'reg', 'set', 'EU']) - - @classmethod - def tearDownClass(klass): - subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) - try: - os.remove('/run/NetworkManager/conf.d/test-blacklist.conf') - except FileNotFoundError: - pass - try: - os.remove('/run/udev/rules.d/99-nm-veth-test.rules') - except FileNotFoundError: - pass - - def tearDown(self): - subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa@*', - 'systemd-networkd.socket']) - # NM has KillMode=process and leaks dhclient processes - subprocess.call(['systemctl', 'kill', 'NetworkManager']) - subprocess.call(['systemctl', 'reset-failed', 'NetworkManager', 'systemd-networkd'], - stderr=subprocess.DEVNULL) - shutil.rmtree('/etc/netplan', ignore_errors=True) - shutil.rmtree('/run/NetworkManager', ignore_errors=True) - shutil.rmtree('/run/systemd/network', ignore_errors=True) - try: - os.remove('/run/systemd/generator/netplan.stamp') - except FileNotFoundError: - pass - - @classmethod - def create_devices(klass): - '''Create Access Point and Client devices with mac80211_hwsim and veth''' - - if os.path.exists('/sys/module/mac80211_hwsim'): - raise SystemError('mac80211_hwsim module already loaded') - if os.path.exists('/sys/class/net/eth42'): - raise SystemError('eth42 interface already exists') - - # create virtual ethernet devs - subprocess.check_call(['ip', 'link', 'add', 'name', 'eth42', 'type', - 'veth', 'peer', 'name', 'veth42']) - klass.dev_e_ap = 'veth42' - klass.dev_e_client = 'eth42' - out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], - universal_newlines=True) - klass.dev_e_client_mac = out.split()[2] - subprocess.check_call(['ip', 'link', 'add', 'name', 'eth43', 'type', - 'veth', 'peer', 'name', 'veth43']) - klass.dev_e2_ap = 'veth43' - klass.dev_e2_client = 'eth43' - out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth43'], - universal_newlines=True) - klass.dev_e2_client_mac = out.split()[2] - - # create virtual wlan devs - before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) - subprocess.check_call(['modprobe', 'mac80211_hwsim']) - # wait 5 seconds for fake devices to appear - timeout = 50 - while timeout > 0: - after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) - if len(after_wlan) - len(before_wlan) >= 2: - break - timeout -= 1 - time.sleep(0.1) - else: - raise SystemError('timed out waiting for fake devices to appear') - - devs = list(after_wlan - before_wlan) - klass.dev_w_ap = devs[0] - klass.dev_w_client = devs[1] - - # don't let NM trample over our fake AP - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - - @classmethod - def shutdown_devices(klass): - '''Remove test wlan devices''' - - subprocess.check_call(['rmmod', 'mac80211_hwsim']) - subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e_ap]) - subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e2_ap]) - subprocess.call(['ip', 'link', 'del', 'dev', 'mybr'], - stderr=subprocess.PIPE) - klass.dev_w_ap = None - klass.dev_w_client = None - klass.dev_e_ap = None - klass.dev_e_client = None - klass.dev_e2_ap = None - klass.dev_e2_client = None - - def setUp(self): - '''Create test devices and workdir''' - - self.create_devices() - self.addCleanup(self.shutdown_devices) - self.workdir_obj = tempfile.TemporaryDirectory() - self.workdir = self.workdir_obj.name - self.config = '/etc/netplan/01-main.yaml' - os.makedirs('/etc/netplan', exist_ok=True) - - # create static entropy file to avoid draining/blocking on /dev/random - self.entropy_file = os.path.join(self.workdir, 'entropy') - with open(self.entropy_file, 'wb') as f: - f.write(b'012345678901234567890') - - def setup_ap(self, hostapd_conf, ipv6_mode): - '''Set up simulated access point - - On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq - according to ipv6_mode, see start_dnsmasq(). - - This is torn down automatically at the end of the test. - ''' - # give our AP an IP - subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) - if ipv6_mode is not None: - subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.dev_w_ap]) - else: - subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.dev_w_ap]) - - self.start_hostapd(hostapd_conf) - self.start_dnsmasq(ipv6_mode, self.dev_w_ap) - - def setup_eth(self, ipv6_mode, start_dnsmasq=True): - '''Set up simulated ethernet router - - On self.dev_e_ap, run dnsmasq according to ipv6_mode, see - start_dnsmasq(). - - This is torn down automatically at the end of the test. - ''' - # give our router an IP - subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e_ap]) - if ipv6_mode is not None: - subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.dev_e_ap]) - subprocess.check_call(['ip', 'a', 'add', '2601::1/64', 'dev', self.dev_e2_ap]) - else: - subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.dev_e_ap]) - subprocess.check_call(['ip', 'a', 'add', '192.168.6.1/24', 'dev', self.dev_e2_ap]) - subprocess.check_call(['ip', 'link', 'set', self.dev_e_ap, 'up']) - subprocess.check_call(['ip', 'link', 'set', self.dev_e2_ap, 'up']) - if start_dnsmasq: - self.start_dnsmasq(ipv6_mode, self.dev_e_ap) - - # - # Internal implementation details - # - - @classmethod - def poll_text(klass, logpath, string, timeout=50): - '''Poll log file for a given string with a timeout. - - Timeout is given in deciseconds. - ''' - log = '' - while timeout > 0: - if os.path.exists(logpath): - break - timeout -= 1 - time.sleep(0.1) - assert timeout > 0, 'Timed out waiting for file %s to appear' % logpath - - with open(logpath) as f: - while timeout > 0: - line = f.readline() - if line: - log += line - if string in line: - break - continue - timeout -= 1 - time.sleep(0.1) - - assert timeout > 0, 'Timed out waiting for "%s":\n------------\n%s\n-------\n' % (string, log) - - def start_hostapd(self, conf): - hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') - with open(hostapd_conf, 'w') as f: - f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) - f.write(conf) - - log = os.path.join(self.workdir, 'hostapd.log') - p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], - stdout=subprocess.PIPE) - self.addCleanup(p.wait) - self.addCleanup(p.terminate) - self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED') - - def start_dnsmasq(self, ipv6_mode, iface): - '''Start dnsmasq. - - If ipv6_mode is None, IPv4 is set up with DHCP. If it is not None, it - must be a valid dnsmasq mode, i. e. a combination of "ra-only", - "slaac", "ra-stateless", and "ra-names". See dnsmasq(8). - ''' - if ipv6_mode is None: - if iface == self.dev_e2_ap: - dhcp_range = '192.168.6.10,192.168.6.200' - else: - dhcp_range = '192.168.5.10,192.168.5.200' - else: - if iface == self.dev_e2_ap: - dhcp_range = '2601::10,2601::20' - else: - dhcp_range = '2600::10,2600::20' - if ipv6_mode: - dhcp_range += ',' + ipv6_mode - - self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-%s.log' % iface) - lease_file = os.path.join(self.workdir, 'dnsmasq-%s.leases' % iface) - - p = subprocess.Popen(['dnsmasq', '--keep-in-foreground', '--log-queries', - '--log-facility=' + self.dnsmasq_log, - '--conf-file=/dev/null', - '--dhcp-leasefile=' + lease_file, - '--bind-interfaces', - '--interface=' + iface, - '--except-interface=lo', - '--enable-ra', - '--dhcp-range=' + dhcp_range]) - self.addCleanup(p.wait) - self.addCleanup(p.terminate) - - if ipv6_mode is not None: - self.poll_text(self.dnsmasq_log, 'IPv6 router advertisement enabled') - else: - self.poll_text(self.dnsmasq_log, 'DHCP, IP range') - - def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): - '''Assert that client interface is up''' - - out = subprocess.check_output(['ip', 'a', 'show', 'dev', iface], - universal_newlines=True) - if 'bond' not in iface: - self.assertIn('state UP', out) - if expected_ip_a: - for r in expected_ip_a: - self.assertRegex(out, r, out) - if unexpected_ip_a: - for r in unexpected_ip_a: - self.assertNotRegex(out, r, out) - - if iface == self.dev_w_client: - out = subprocess.check_output(['iw', 'dev', iface, 'link'], - universal_newlines=True) - # self.assertIn('Connected to ' + self.mac_w_ap, out) - self.assertIn('SSID: fake net', out) - - def generate_and_settle(self): - '''Generate config, launch and settle NM and networkd''' - - # regenerate netplan config - subprocess.check_call(['netplan', 'apply']) - # start NM so that we can verify that it does not manage anything - subprocess.check_call(['systemctl', 'start', '--no-block', 'NetworkManager.service']) - # wait until networkd is done - if self.is_active('systemd-networkd.service'): - if subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=50']) != 0: - subprocess.call(['journalctl', '-b', '--no-pager', '-t', 'systemd-networkd']) - st = subprocess.check_output(['networkctl'], stderr=subprocess.PIPE, universal_newlines=True) - st_e = subprocess.check_output(['networkctl', 'status', self.dev_e_client], - stderr=subprocess.PIPE, universal_newlines=True) - st_e2 = subprocess.check_output(['networkctl', 'status', self.dev_e2_client], - stderr=subprocess.PIPE, universal_newlines=True) - self.fail('timed out waiting for networkd to settle down:\n%s\n%s\n%s' % (st, st_e, st_e2)) - - if subprocess.call(['nm-online', '--quiet', '--timeout=120', '--wait-for-startup']) != 0: - self.fail('timed out waiting for NetworkManager to settle down') - - def nm_wait_connected(self, iface, timeout): - for t in range(timeout): - try: - out = subprocess.check_output(['nmcli', 'dev', 'show', iface]) - except subprocess.CalledProcessError: - out = b'' - if b'(connected' in out: - break - time.sleep(1) - else: - self.fail('timed out waiting for %s to get connected by NM:\n%s' % (iface, out.decode())) - - @classmethod - def is_active(klass, unit): - '''Check if given unit is active or activating''' - - p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) - out = p.communicate()[0] - return p.returncode == 0 or out.startswith(b'activating') - - -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) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - wifis: - wifi_ifs: - match: - driver: mac80211_hwsim - dhcp4: yes - access-points: - "fake net": {} - decoy: {}''' % {'r': self.backend}) - self.generate_and_settle() - p = subprocess.Popen(['netplan', 'generate', '--mapping', 'mac80211_hwsim'], - stdout=subprocess.PIPE) - out = p.communicate()[0] - self.assertEquals(p.returncode, 1) - self.assertIn(b'mac80211_hwsim', out) - - def test_eth_and_bridge(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - dhcp4: yes - ethbr: - match: {name: %(e2c)s} - bridges: - mybr: - interfaces: [ethbr] - dhcp4: yes''' % {'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]) - - # ensure that they do not get managed by NM for foreign backends - expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' - out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) - for i in [self.dev_e_client, self.dev_e2_client, 'mybr']: - self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) - - def test_eth_mtu(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - dhcp4: yes - enmtus: - match: {name: %(e2c)s} - mtu: 1492 - dhcp4: yes''' % {'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, - ['inet 192.168.6.[0-9]+/24']) - out = subprocess.check_output(['ip', 'a', 'show', self.dev_e2_client], - universal_newlines=True) - self.assertTrue('mtu 1492' in out, "checking MTU, should be 1492") - - def test_eth_mac(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - dhcp4: yes - enmac: - match: {name: %(e2c)s} - macaddress: 00:01:02:03:04:05 - dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e2_client, - ['inet 192.168.6.[0-9]+/24', '00:01:02:03:04:05'], - ['master']) - out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], - universal_newlines=True) - self.assertTrue('ether 00:01:02:03:04:05' in out) - subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, - 'address', self.dev_e2_client_mac]) - - def test_bridge_path_cost(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] - parameters: - path-cost: - ethbr: 50 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/brif/%s/path_cost' % self.dev_e2_client) as f: - self.assertEqual(f.read().strip(), '50') - - def test_bridge_ageing_time(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] - parameters: - ageing-time: 21 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/ageing_time') as f: - self.assertEqual(f.read().strip(), '2100') - - def test_bridge_max_age(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] - parameters: - max-age: 12 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/max_age') as f: - self.assertEqual(f.read().strip(), '1200') - - def test_bridge_hello_time(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] - parameters: - hello-time: 1 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/hello_time') as f: - self.assertEqual(f.read().strip(), '100') - - def test_bridge_forward_delay(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] - parameters: - forward-delay: 10 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/forward_delay') as f: - self.assertEqual(f.read().strip(), '1000') - - def test_bridge_stp_false(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] - parameters: - hello-time: 100000 - max-age: 100000 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/stp_state') as f: - self.assertEqual(f.read().strip(), '0') - - def test_bond_base(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - - def test_bond_primary_slave(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: {} - %(e2c)s: {} - bonds: - mybond: - interfaces: [%(ec)s, %(e2c)s] - parameters: - mode: active-backup - primary: %(ec)s - addresses: [ '10.10.10.1/24' ]''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 10.10.10.1/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - result = f.read().strip() - self.assertIn(self.dev_e_client, result) - self.assertIn(self.dev_e2_client, result) - with open('/sys/class/net/mybond/bonding/primary') as f: - self.assertEqual(f.read().strip(), '%(ec)s' % {'ec': self.dev_e_client}) - - def test_bond_all_slaves_active(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - all-slaves-active: true - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/all_slaves_active') as f: - self.assertEqual(f.read().strip(), '1') - - def test_bond_mode_8023ad(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: 802.3ad - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), '802.3ad 4') - - def test_bond_mode_8023ad_adselect(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: 802.3ad - ad-select: bandwidth - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/ad_select') as f: - self.assertEqual(f.read().strip(), 'bandwidth 1') - - def test_bond_mode_8023ad_lacp_rate(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: 802.3ad - lacp-rate: fast - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/lacp_rate') as f: - self.assertEqual(f.read().strip(), 'fast 1') - - def test_bond_mode_activebackup_failover_mac(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: active-backup - fail-over-mac-policy: follow - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), 'active-backup 1') - with open('/sys/class/net/mybond/bonding/fail_over_mac') as f: - self.assertEqual(f.read().strip(), 'follow 2') - - def test_bond_mode_balance_xor(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: balance-xor - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), 'balance-xor 2') - - def test_bond_mode_balance_rr(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: balance-rr - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), 'balance-rr 0') - - def test_bond_mode_balance_rr_pps(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: balance-rr - packets-per-slave: 15 - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), 'balance-rr 0') - with open('/sys/class/net/mybond/bonding/packets_per_slave') as f: - self.assertEqual(f.read().strip(), '15') - - def test_bond_resend_igmp(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - ethb2: - match: {name: %(e2c)s} - bonds: - mybond: - interfaces: [ethbn, ethb2] - parameters: - mode: balance-rr - 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() - self.assert_iface_up(self.dev_e_client, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - result = f.read().strip() - self.assertIn(self.dev_e_client, result) - self.assertIn(self.dev_e2_client, result) - with open('/sys/class/net/mybond/bonding/resend_igmp') as f: - self.assertEqual(f.read().strip(), '100') - - @unittest.skip("fails due to networkd bug setting routes with dhcp") - def test_routes_v4_with_dhcp(self): - self.setup_eth(None) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - dhcp4: yes - routes: - - to: 10.10.10.0/24 - via: 192.168.5.254 - 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 via 192.168.5.254', # from static route - 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_routes_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 - via: 192.168.5.254 - 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 via 192.168.5.254', # from DHCP - 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_routes_v6(self): - self.setup_eth(None) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - addresses: ["9876:BBBB::11/70"] - gateway6: "9876:BBBB::1" - routes: - - to: 2001:f00f:f00f::1/64 - via: 9876:BBBB::5 - metric: 799''' % {'r': self.backend, 'ec': self.dev_e_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet6 9876:bbbb::11/70']) - self.assertNotIn(b'default', - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) - self.assertIn(b'via 9876:bbbb::1', - subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) - self.assertIn(b'2001:f00f:f00f::/64 via 9876:bbbb::5', - subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) - self.assertIn(b'metric 799', - subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64'])) - - def test_manual_addresses(self): - self.setup_eth(None) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] - dhcp4: yes - %(e2c)s: - addresses: ["172.16.1.2/24"] - gateway4: "172.16.1.1" - nameservers: - addresses: [172.1.2.3] - search: ["fakesuffix"] -''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet 172.16.42.99/18', - 'inet6 1234:ffff::42/64', - 'inet 192.168.5.[0-9]+/24']) # from DHCP - self.assert_iface_up(self.dev_e2_client, - ['inet 172.16.1.2/24']) - - self.assertIn(b'default via 192.168.5.1', # from DHCP - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) - self.assertNotIn(b'default', - subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) - self.assertIn(b'default via 172.16.1.1', - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) - self.assertNotIn(b'default', - subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) - - # ensure that they do not get managed by NM for foreign backends - expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' - out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) - for i in [self.dev_e_client, self.dev_e2_client]: - self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) - - with open('/etc/resolv.conf') as f: - resolv_conf = f.read() - - if self.backend == 'NetworkManager' and nm_uses_dnsmasq: - sys.stdout.write('[NM with dnsmasq] ') - sys.stdout.flush() - self.assertRegex(resolv_conf, 'search.*fakesuffix') - # not easy to peek dnsmasq's brain, so check its logging - out = subprocess.check_output(['journalctl', '--quiet', '-tdnsmasq', '-ocat', '--since=-30s'], - universal_newlines=True) - self.assertIn('nameserver 172.1.2.3', out) - elif resolved_in_use(): - sys.stdout.write('[resolved] ') - sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) - self.assertIn('DNS Servers: 172.1.2.3', out) - self.assertIn('fakesuffix', out) - else: - sys.stdout.write('[/etc/resolv.conf] ') - sys.stdout.flush() - self.assertRegex(resolv_conf, 'search.*fakesuffix') - # /etc/resolve.conf often already has three nameserver entries - if 'nameserver 172.1.2.3' not in resolv_conf: - self.assertGreaterEqual(resolv_conf.count('nameserver'), 3) - - # change the addresses, make sure that "apply" does not leave leftovers - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - %(ec)s: - addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] - gateway6: "9876:BBBB::1" - %(e2c)s: - addresses: ["172.16.7.2/30", "4321:AAAA::99/80"] - dhcp4: yes -''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.start_dnsmasq(None, self.dev_e2_ap) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet 172.16.5.3/20'], - ['inet 192.168.5', # old DHCP - 'inet 172.16.42', # old static IPv4 - 'inet6 1234']) # old static IPv6 - self.assert_iface_up(self.dev_e2_client, - ['inet 172.16.7.2/30', - 'inet6 4321:aaaa::99/80', - 'inet 192.168.6.[0-9]+/24'], # from DHCP - ['inet 172.16.1']) # old static IPv4 - - self.assertNotIn(b'default', - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) - self.assertIn(b'via 9876:bbbb::1', - subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) - self.assertIn(b'default via 192.168.6.1', # from DHCP - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) - self.assertNotIn(b'default', - subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) - - def test_dhcp6(self): - self.setup_eth('slaac') - with open(self.config, 'w') as f: - f.write('''network: - version: 2 - renderer: %(r)s - ethernets: - %(ec)s: - dhcp6: yes - accept-ra: yes - %(e2c)s: {}''' % {'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, ['inet6 2600:'], ['inet 192.168']) - - def test_vlan(self): - # we create two VLANs on e2c, and run dnsmasq on ID 2002 to test DHCP via VLAN - self.setup_eth(None, start_dnsmasq=False) - self.start_dnsmasq(None, self.dev_e2_ap) - subprocess.check_call(['ip', 'link', 'add', 'link', self.dev_e2_ap, - 'name', 'nptestsrv', 'type', 'vlan', 'id', '2002']) - subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', 'nptestsrv']) - subprocess.check_call(['ip', 'link', 'set', 'nptestsrv', 'up']) - self.start_dnsmasq(None, 'nptestsrv') - with open(self.config, 'w') as f: - f.write('''network: - version: 2 - renderer: %(r)s - ethernets: - %(ec)s: {} - myether: - match: {name: %(e2c)s} - dhcp4: yes - vlans: - nptestone: - id: 1001 - link: myether - addresses: [10.9.8.7/24] - nptesttwo: - id: 2002 - link: myether - dhcp4: true - ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - - self.assert_iface_up('nptestone', ['nptestone@' + self.dev_e2_client, 'inet 10.9.8.7/24']) - self.assert_iface_up('nptesttwo', ['nptesttwo@' + self.dev_e2_client, 'inet 192.168.5']) - self.assertNotIn(b'default', - subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptestone'])) - self.assertIn(b'default via 192.168.5.1', # from DHCP - subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptesttwo'])) - - def test_vlan_mac_address(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'myvlan'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - vlans: - myvlan: - id: 101 - link: ethbn - macaddress: aa:bb:cc:dd:ee:22 - ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - self.assert_iface_up('myvlan', ['myvlan@' + self.dev_e_client]) - with open('/sys/class/net/myvlan/address') as f: - self.assertEqual(f.read().strip(), 'aa:bb:cc:dd:ee:22') - - def test_wifi_ipv4_open(self): - self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) - - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - wifis: - %(wc)s: - dhcp4: yes - access-points: - "fake net": {} - decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) - self.generate_and_settle() - # nm-online doesn't wait for wifis, argh - if self.backend == 'NetworkManager': - self.nm_wait_connected(self.dev_w_client, 60) - - self.assert_iface_up(self.dev_w_client, - ['inet 192.168.5.[0-9]+/24'], - ['master']) - self.assertIn(b'default via 192.168.5.1', # from DHCP - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) - if self.backend == 'NetworkManager': - out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], - universal_newlines=True) - self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) - self.assertRegex(out, 'IP4.DNS.*192.168.5.1') - else: - out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], - universal_newlines=True) - self.assertRegex(out, 'DNS.*192.168.5.1') - - def test_wifi_ipv4_wpa2(self): - self.setup_ap('''hw_mode=g -channel=1 -ssid=fake net -wpa=1 -wpa_key_mgmt=WPA-PSK -wpa_pairwise=TKIP -wpa_passphrase=12345678 -''', None) - - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - wifis: - %(wc)s: - dhcp4: yes - access-points: - "fake net": - password: 12345678 - decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) - self.generate_and_settle() - # nm-online doesn't wait for wifis, argh - if self.backend == 'NetworkManager': - self.nm_wait_connected(self.dev_w_client, 60) - - self.assert_iface_up(self.dev_w_client, - ['inet 192.168.5.[0-9]+/24'], - ['master']) - self.assertIn(b'default via 192.168.5.1', # from DHCP - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) - if self.backend == 'NetworkManager': - out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], - universal_newlines=True) - self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) - self.assertRegex(out, 'IP4.DNS.*192.168.5.1') - else: - out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], - universal_newlines=True) - self.assertRegex(out, 'DNS.*192.168.5.1') - - def test_mix_bridge_on_bond(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - bridges: - br0: - interfaces: [bond0] - addresses: ['192.168.0.2/24'] - bonds: - bond0: - interfaces: [ethb2] - parameters: - mode: balance-rr - ethernets: - ethbn: - match: {name: %(ec)s} - ethb2: - match: {name: %(e2c)s} -''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e2_client, - ['master bond0'], - ['inet ']) - self.assert_iface_up('bond0', - ['master br0']) - ipaddr = subprocess.check_output(['ip', 'a', 'show', 'dev', 'br0'], - universal_newlines=True) - self.assertIn('inet 192.168', ipaddr) - with open('/sys/class/net/bond0/bonding/slaves') as f: - result = f.read().strip() - self.assertIn(self.dev_e2_client, result) - - def test_mix_vlan_on_bridge_on_bond(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - version: 2 - vlans: - vlan1: - link: 'br0' - id: 1 - addresses: [ '10.10.10.1/24' ] - bridges: - br0: - interfaces: ['bond0', 'vlan2'] - parameters: - stp: false - path-cost: - bond0: 1000 - vlan2: 2000 - bonds: - bond0: - interfaces: ['br1'] - parameters: - mode: balance-rr - bridges: - br1: - interfaces: ['ethb2'] - vlans: - vlan2: - link: ethbn - id: 2 - ethernets: - ethbn: - match: {name: %(ec)s} - ethb2: - match: {name: %(e2c)s} -''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() - self.assert_iface_up('vlan1', ['vlan1@br0']) - self.assert_iface_up('vlan2', - ['vlan2@' + self.dev_e_client, 'master br0']) - self.assert_iface_up(self.dev_e2_client, - ['master br1'], - ['inet ']) - self.assert_iface_up('bond0', - ['master br0']) - - -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: - f.write('''network: - version: 2 - renderer: %(r)s - ethernets: - %(ec)s: - dhcp6: no - accept-ra: yes - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'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, ['inet6 2600:'], []) - - def test_eth_dhcp6_off_no_accept_ra(self): - self.setup_eth('slaac') - with open(self.config, 'w') as f: - f.write('''network: - version: 2 - renderer: %(r)s - ethernets: - %(ec)s: - dhcp6: no - accept-ra: no - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'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, [], ['inet6 2600:']) - - def test_bond_mac(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: - name: %(ec)s - macaddress: %(ec_mac)s - bonds: - mybond: - interfaces: [ethbn] - macaddress: 00:01:02:03:04:05 - dhcp4: yes''' % {'r': self.backend, - 'ec': self.dev_e_client, - 'e2c': self.dev_e2_client, - 'ec_mac': self.dev_e_client_mac}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['master mybond']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) - - def test_bridge_mac(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], 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: %(ec)s - macaddress: %(ec_mac)s - bridges: - br0: - interfaces: [ethbr] - macaddress: 00:01:02:03:04:05 - dhcp4: yes''' % {'r': self.backend, - 'ec': self.dev_e_client, - 'e2c': self.dev_e2_client, - 'ec_mac': self.dev_e_client_mac}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['master br0'], ['inet']) - self.assert_iface_up('br0', - ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) - - def test_bond_down_delay(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: active-backup - mii-monitor-interval: 5 - down-delay: 10s - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/downdelay') as f: - self.assertEqual(f.read().strip(), '10000') - - def test_bond_up_delay(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: active-backup - mii-monitor-interval: 5 - up-delay: 10000 - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/updelay') as f: - self.assertEqual(f.read().strip(), '10000') - - def test_bond_arp_interval(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-ip-targets: [ 192.168.5.1 ] - arp-interval: 50s - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_interval') as f: - self.assertEqual(f.read().strip(), '50000') - - def test_bond_arp_targets(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-interval: 50000 - arp-ip-targets: [ 192.168.5.1 ] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: - self.assertEqual(f.read().strip(), '192.168.5.1') - - def test_bond_arp_all_targets(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-ip-targets: [192.168.5.1] - arp-interval: 50000 - arp-all-targets: all - arp-validate: all - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: - self.assertEqual(f.read().strip(), 'all 1') - - def test_bond_arp_validate(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-ip-targets: [192.168.5.1] - arp-interval: 50000 - arp-validate: all - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - 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) - 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] - parameters: - port-priority: - ethbr: 42 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: - self.assertEqual(f.read().strip(), '42') - - @unittest.skip("networkd does not handle non-unicast routes correctly yet (Invalid argument)") - def test_route_type_blackhole(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - addresses: [ "10.20.10.1/24" ] - routes: - - to: 10.10.10.0/24 - via: 10.20.10.100 - type: blackhole''' % {'r': self.backend, 'ec': self.dev_e_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet ']) - self.assertIn(b'blackhole 10.10.10.0/24', - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) - - def test_route_on_link(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - dhcp4: no - addresses: [ "10.20.10.1/24" ] - routes: - - to: 20.0.0.0/24 - via: 10.10.10.10 - on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet ']) - self.assertIn(b'20.0.0.0/24 via 10.10.10.10 proto static onlink', - subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) - - def test_route_with_policy(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - addresses: [ "10.20.10.1/24" ] - routes: - - to: 40.0.0.0/24 - via: 10.20.10.55 - metric: 50 - - to: 40.0.0.0/24 - via: 10.20.10.88 - table: 99 - metric: 50 - routing-policy: - - from: 10.20.10.0/24 - to: 40.0.0.0/24 - table: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) - self.generate_and_settle() - self.assert_iface_up(self.dev_e_client, - ['inet ']) - self.assertIn(b'to 40.0.0.0/24 lookup 99', - subprocess.check_output(['ip', 'rule', 'show'])) - self.assertIn(b'40.0.0.0/24 via 10.20.10.88', - subprocess.check_output(['ip', 'route', 'show', 'table', '99'])) - - -class TestNetworkManager(NetworkTestBase, _CommonTests): - backend = 'NetworkManager' - - @unittest.skip("NetworkManager does not disable accept_ra: bug LP: #1704210") - def test_eth_dhcp6_off(self): - self.setup_eth('slaac') - with open(self.config, 'w') as f: - f.write('''network: - version: 2 - renderer: %(r)s - ethernets: - %(ec)s: - dhcp6: no - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'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, [], ['inet6 2600:']) - - @unittest.skip("NetworkManager does not support setting MAC for a bond") - def test_bond_mac(self): - pass - - @unittest.skip("NetworkManager does not support setting MAC for a bridge") - def test_bridge_mac(self): - pass - - def test_wifi_ap_open(self): - # we use dev_w_client and dev_w_ap in switched roles here, to keep the - # existing device blacklisting in NM; i. e. dev_w_client is the - # NM-managed AP, and dev_w_ap the manually managed client - with open(self.config, 'w') as f: - f.write('''network: - wifis: - renderer: NetworkManager - %(wc)s: - dhcp4: yes - access-points: - "fake net": - mode: ap''' % {'wc': self.dev_w_client}) - self.generate_and_settle() - - # nm-online doesn't wait for wifis, argh - self.nm_wait_connected(self.dev_w_client, 60) - - out = subprocess.check_output(['iw', 'dev', self.dev_w_client, 'info'], - universal_newlines=True) - self.assertIn('type AP', out) - self.assertIn('ssid fake net', out) - - # connect the other end - subprocess.check_call(['ip', 'link', 'set', self.dev_w_ap, 'up']) - subprocess.check_call(['iw', 'dev', self.dev_w_ap, 'connect', 'fake net']) - out = subprocess.check_output(['dhclient', '-1', '-v', self.dev_w_ap], - stderr=subprocess.STDOUT, universal_newlines=True) - self.assertIn('DHCPACK', out) - out = subprocess.check_output(['iw', 'dev', self.dev_w_ap, 'info'], - universal_newlines=True) - self.assertIn('type managed', out) - self.assertIn('ssid fake net', out) - out = subprocess.check_output(['ip', 'a', 'show', self.dev_w_ap], - universal_newlines=True) - self.assertIn('state UP', out) - self.assertIn('inet 10.', out) - - def test_bond_down_delay(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: active-backup - mii-monitor-interval: 5 - down-delay: 10000 - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/downdelay') as f: - self.assertEqual(f.read().strip(), '10000') - - def test_bond_up_delay(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: active-backup - mii-monitor-interval: 5 - up-delay: 10000 - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/updelay') as f: - self.assertEqual(f.read().strip(), '10000') - - def test_bond_arp_interval(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-ip-targets: [ 192.168.5.1 ] - arp-interval: 50000 - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_interval') as f: - self.assertEqual(f.read().strip(), '50000') - - def test_bond_arp_targets(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-interval: 50000 - arp-ip-targets: [ 192.168.5.1 ] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: - self.assertEqual(f.read().strip(), '192.168.5.1') - - def test_bond_arp_all_targets(self): - self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - interfaces: [ethbn] - parameters: - mode: balance-xor - arp-ip-targets: [192.168.5.1] - arp-interval: 50000 - arp-all-targets: all - arp-validate: all - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: - self.assertEqual(f.read().strip(), 'all 1') - - def test_bond_mode_balance_tlb_learn_interval(self): - self.setup_eth(None) - self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) - with open(self.config, 'w') as f: - f.write('''network: - renderer: %(r)s - ethernets: - ethbn: - match: {name: %(ec)s} - %(e2c)s: {} - bonds: - mybond: - parameters: - mode: balance-tlb - mii-monitor-interval: 5 - learn-packet-interval: 15 - interfaces: [ethbn] - dhcp4: yes''' % {'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, - ['master mybond'], - ['inet ']) - self.assert_iface_up('mybond', - ['inet 192.168.5.[0-9]+/24']) - with open('/sys/class/net/mybond/bonding/slaves') as f: - self.assertEqual(f.read().strip(), self.dev_e_client) - with open('/sys/class/net/mybond/bonding/mode') as f: - self.assertEqual(f.read().strip(), 'balance-tlb 5') - with open('/sys/class/net/mybond/bonding/lp_interval') as f: - self.assertEqual(f.read().strip(), '15') - - def test_bridge_priority(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] - parameters: - priority: 16384 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/bridge/priority') as f: - self.assertEqual(f.read().strip(), '16384') - - def test_bridge_port_priority(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] - parameters: - port-priority: - ethbr: 42 - stp: false - dhcp4: yes''' % {'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]) - with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: - self.assertEqual(f.read().strip(), '42') - - -unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..be79e88 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,17 @@ +# +# Integration tests. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/tests/integration/base.py b/tests/integration/base.py new file mode 100644 index 0000000..1c9de58 --- /dev/null +++ b/tests/integration/base.py @@ -0,0 +1,377 @@ +# +# System integration tests of netplan-generate. NM and networkd are +# started on the generated configuration, using emulated ethernets (veth) and +# Wifi (mac80211-hwsim). These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Martin Pitt <martin.pitt@ubuntu.com> +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import re +import time +import subprocess +import tempfile +import unittest +import shutil + +test_backends = "networkd NetworkManager" if "NETPLAN_TEST_BACKENDS" not in os.environ else os.environ["NETPLAN_TEST_BACKENDS"] + +for program in ['wpa_supplicant', 'hostapd', 'dnsmasq']: + if subprocess.call(['which', program], stdout=subprocess.PIPE) != 0: + sys.stderr.write('%s is required for this test suite, but not available. Skipping\n' % program) + sys.exit(0) + +nm_uses_dnsmasq = b'dns=dnsmasq' in subprocess.check_output(['NetworkManager', '--print-config']) + + +def resolved_in_use(): + return os.path.isfile('/run/systemd/resolve/resolv.conf') + + +class IntegrationTestsBase(unittest.TestCase): + '''Common functionality for network test cases + + setUp() creates two test wlan devices, one for a simulated access point + (self.dev_w_ap), the other for a simulated client device + (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} + and self.dev_e2_{ap,client}. + + Each test should call self.setup_ap() or self.setup_eth() with the desired + configuration. + ''' + @classmethod + def setUpClass(klass): + # ensure we have this so that iw works + subprocess.check_call(['modprobe', 'cfg80211']) + + # ensure NM can manage our fake eths + os.makedirs('/run/udev/rules.d', exist_ok=True) + with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + subprocess.check_call(['udevadm', 'control', '--reload']) + + # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels + out = subprocess.check_output(['iw', 'reg', 'get'], universal_newlines=True) + m = re.match(r'^(?:global\n)?country (\S+):', out) + assert m + klass.orig_country = m.group(1) + subprocess.check_call(['iw', 'reg', 'set', 'EU']) + + @classmethod + def tearDownClass(klass): + subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) + try: + os.remove('/run/NetworkManager/conf.d/test-blacklist.conf') + except FileNotFoundError: + pass + try: + os.remove('/run/udev/rules.d/99-nm-veth-test.rules') + except FileNotFoundError: + pass + + def tearDown(self): + subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa@*', + 'systemd-networkd.socket']) + # NM has KillMode=process and leaks dhclient processes + subprocess.call(['systemctl', 'kill', 'NetworkManager']) + subprocess.call(['systemctl', 'reset-failed', 'NetworkManager', 'systemd-networkd'], + stderr=subprocess.DEVNULL) + shutil.rmtree('/etc/netplan', ignore_errors=True) + shutil.rmtree('/run/NetworkManager', ignore_errors=True) + shutil.rmtree('/run/systemd/network', ignore_errors=True) + try: + os.remove('/run/systemd/generator/netplan.stamp') + except FileNotFoundError: + pass + + @classmethod + def create_devices(klass): + '''Create Access Point and Client devices with mac80211_hwsim and veth''' + + if os.path.exists('/sys/module/mac80211_hwsim'): + raise SystemError('mac80211_hwsim module already loaded') + if os.path.exists('/sys/class/net/eth42'): + raise SystemError('eth42 interface already exists') + + # create virtual ethernet devs + subprocess.check_call(['ip', 'link', 'add', 'name', 'eth42', 'type', + 'veth', 'peer', 'name', 'veth42']) + klass.dev_e_ap = 'veth42' + klass.dev_e_client = 'eth42' + klass.dev_e_ap_ip4 = '192.168.5.1/24' + klass.dev_e_ap_ip6 = '2600::1/64' + out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], + universal_newlines=True) + klass.dev_e_client_mac = out.split()[2] + subprocess.check_call(['ip', 'link', 'add', 'name', 'eth43', 'type', + 'veth', 'peer', 'name', 'veth43']) + klass.dev_e2_ap = 'veth43' + klass.dev_e2_client = 'eth43' + klass.dev_e2_ap_ip4 = '192.168.6.1/24' + klass.dev_e2_ap_ip6 = '2601::1/64' + out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth43'], + universal_newlines=True) + klass.dev_e2_client_mac = out.split()[2] + + # create virtual wlan devs + before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + subprocess.check_call(['modprobe', 'mac80211_hwsim']) + # wait 5 seconds for fake devices to appear + timeout = 50 + while timeout > 0: + after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + if len(after_wlan) - len(before_wlan) >= 2: + break + timeout -= 1 + time.sleep(0.1) + else: + raise SystemError('timed out waiting for fake devices to appear') + + devs = list(after_wlan - before_wlan) + klass.dev_w_ap = devs[0] + klass.dev_w_client = devs[1] + + # don't let NM trample over our fake AP + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) + with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + # work around https://launchpad.net/bugs/1615044 + with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: + f.write('[keyfile]\nunmanaged-devices=') + + @classmethod + def shutdown_devices(klass): + '''Remove test wlan devices''' + + subprocess.check_call(['rmmod', 'mac80211_hwsim']) + subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e_ap]) + subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e2_ap]) + subprocess.call(['ip', 'link', 'del', 'dev', 'mybr'], + stderr=subprocess.PIPE) + klass.dev_w_ap = None + klass.dev_w_client = None + klass.dev_e_ap = None + klass.dev_e_client = None + klass.dev_e2_ap = None + klass.dev_e2_client = None + + def setUp(self): + '''Create test devices and workdir''' + + self.create_devices() + self.addCleanup(self.shutdown_devices) + self.workdir_obj = tempfile.TemporaryDirectory() + self.workdir = self.workdir_obj.name + self.config = '/etc/netplan/01-main.yaml' + os.makedirs('/etc/netplan', exist_ok=True) + + # create static entropy file to avoid draining/blocking on /dev/random + self.entropy_file = os.path.join(self.workdir, 'entropy') + with open(self.entropy_file, 'wb') as f: + f.write(b'012345678901234567890') + + def setup_ap(self, hostapd_conf, ipv6_mode): + '''Set up simulated access point + + On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq + according to ipv6_mode, see start_dnsmasq(). + + This is torn down automatically at the end of the test. + ''' + # give our AP an IP + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) + if ipv6_mode is not None: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_w_ap]) + else: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_w_ap]) + + self.start_hostapd(hostapd_conf) + self.start_dnsmasq(ipv6_mode, self.dev_w_ap) + + def setup_eth(self, ipv6_mode, start_dnsmasq=True): + '''Set up simulated ethernet router + + On self.dev_e_ap, run dnsmasq according to ipv6_mode, see + start_dnsmasq(). + + This is torn down automatically at the end of the test. + ''' + # give our router an IP + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e_ap]) + if ipv6_mode is not None: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_e_ap]) + subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip6, 'dev', self.dev_e2_ap]) + else: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_e_ap]) + subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip4, 'dev', self.dev_e2_ap]) + subprocess.check_call(['ip', 'link', 'set', self.dev_e_ap, 'up']) + subprocess.check_call(['ip', 'link', 'set', self.dev_e2_ap, 'up']) + if start_dnsmasq: + self.start_dnsmasq(ipv6_mode, self.dev_e_ap) + + # + # Internal implementation details + # + + @classmethod + def poll_text(klass, logpath, string, timeout=50): + '''Poll log file for a given string with a timeout. + + Timeout is given in deciseconds. + ''' + log = '' + while timeout > 0: + if os.path.exists(logpath): + break + timeout -= 1 + time.sleep(0.1) + assert timeout > 0, 'Timed out waiting for file %s to appear' % logpath + + with open(logpath) as f: + while timeout > 0: + line = f.readline() + if line: + log += line + if string in line: + break + continue + timeout -= 1 + time.sleep(0.1) + + assert timeout > 0, 'Timed out waiting for "%s":\n------------\n%s\n-------\n' % (string, log) + + def start_hostapd(self, conf): + hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') + with open(hostapd_conf, 'w') as f: + f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) + f.write(conf) + + log = os.path.join(self.workdir, 'hostapd.log') + p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], + stdout=subprocess.PIPE) + self.addCleanup(p.wait) + self.addCleanup(p.terminate) + self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED') + + def start_dnsmasq(self, ipv6_mode, iface): + '''Start dnsmasq. + + If ipv6_mode is None, IPv4 is set up with DHCP. If it is not None, it + must be a valid dnsmasq mode, i. e. a combination of "ra-only", + "slaac", "ra-stateless", and "ra-names". See dnsmasq(8). + ''' + if ipv6_mode is None: + if iface == self.dev_e2_ap: + dhcp_range = '192.168.6.10,192.168.6.200' + else: + dhcp_range = '192.168.5.10,192.168.5.200' + else: + if iface == self.dev_e2_ap: + dhcp_range = '2601::10,2601::20' + else: + dhcp_range = '2600::10,2600::20' + if ipv6_mode: + dhcp_range += ',' + ipv6_mode + + self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-%s.log' % iface) + lease_file = os.path.join(self.workdir, 'dnsmasq-%s.leases' % iface) + + p = subprocess.Popen(['dnsmasq', '--keep-in-foreground', '--log-queries', + '--log-facility=' + self.dnsmasq_log, + '--conf-file=/dev/null', + '--dhcp-leasefile=' + lease_file, + '--bind-interfaces', + '--interface=' + iface, + '--except-interface=lo', + '--enable-ra', + '--dhcp-range=' + dhcp_range]) + self.addCleanup(p.wait) + self.addCleanup(p.terminate) + + if ipv6_mode is not None: + self.poll_text(self.dnsmasq_log, 'IPv6 router advertisement enabled') + else: + self.poll_text(self.dnsmasq_log, 'DHCP, IP range') + + def assert_iface(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface has been created''' + + out = subprocess.check_output(['ip', 'a', 'show', 'dev', iface], + universal_newlines=True) + if expected_ip_a: + for r in expected_ip_a: + self.assertRegex(out, r, out) + if unexpected_ip_a: + for r in unexpected_ip_a: + self.assertNotRegex(out, r, out) + + return out + + def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface is up''' + + out = self.assert_iface(iface, expected_ip_a=None, unexpected_ip_a=None) + if 'bond' not in iface: + self.assertIn('state UP', out) + + if iface == self.dev_w_client: + out = subprocess.check_output(['iw', 'dev', iface, 'link'], + universal_newlines=True) + # self.assertIn('Connected to ' + self.mac_w_ap, out) + self.assertIn('SSID: fake net', out) + + def generate_and_settle(self): + '''Generate config, launch and settle NM and networkd''' + + # regenerate netplan config + subprocess.check_call(['netplan', 'apply']) + # start NM so that we can verify that it does not manage anything + subprocess.check_call(['systemctl', 'start', '--no-block', 'NetworkManager.service']) + # wait until networkd is done + if self.is_active('systemd-networkd.service'): + if subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=50']) != 0: + subprocess.call(['journalctl', '-b', '--no-pager', '-t', 'systemd-networkd']) + st = subprocess.check_output(['networkctl'], stderr=subprocess.PIPE, universal_newlines=True) + st_e = subprocess.check_output(['networkctl', 'status', self.dev_e_client], + stderr=subprocess.PIPE, universal_newlines=True) + st_e2 = subprocess.check_output(['networkctl', 'status', self.dev_e2_client], + stderr=subprocess.PIPE, universal_newlines=True) + self.fail('timed out waiting for networkd to settle down:\n%s\n%s\n%s' % (st, st_e, st_e2)) + + if subprocess.call(['nm-online', '--quiet', '--timeout=120', '--wait-for-startup']) != 0: + self.fail('timed out waiting for NetworkManager to settle down') + + def nm_wait_connected(self, iface, timeout): + for t in range(timeout): + try: + out = subprocess.check_output(['nmcli', 'dev', 'show', iface]) + except subprocess.CalledProcessError: + out = b'' + if b'(connected' in out: + break + time.sleep(1) + else: + self.fail('timed out waiting for %s to get connected by NM:\n%s' % (iface, out.decode())) + + @classmethod + def is_active(klass, unit): + '''Check if given unit is active or activating''' + + p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) + out = p.communicate()[0] + return p.returncode == 0 or out.startswith(b'activating') diff --git a/tests/integration/bonds.py b/tests/integration/bonds.py new file mode 100644 index 0000000..720b688 --- /dev/null +++ b/tests/integration/bonds.py @@ -0,0 +1,744 @@ +#!/usr/bin/python3 +# +# Integration tests for bonds +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_bond_base(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + + def test_bond_primary_slave(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: {} + %(e2c)s: {} + bonds: + mybond: + interfaces: [%(ec)s, %(e2c)s] + parameters: + mode: active-backup + primary: %(ec)s + addresses: [ '10.10.10.1/24' ]''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 10.10.10.1/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e_client, result) + self.assertIn(self.dev_e2_client, result) + with open('/sys/class/net/mybond/bonding/primary') as f: + self.assertEqual(f.read().strip(), '%(ec)s' % {'ec': self.dev_e_client}) + + def test_bond_all_slaves_active(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + all-slaves-active: true + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/all_slaves_active') as f: + self.assertEqual(f.read().strip(), '1') + + def test_bond_mode_8023ad(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: 802.3ad + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), '802.3ad 4') + + def test_bond_mode_8023ad_adselect(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: 802.3ad + ad-select: bandwidth + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/ad_select') as f: + self.assertEqual(f.read().strip(), 'bandwidth 1') + + def test_bond_mode_8023ad_lacp_rate(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: 802.3ad + lacp-rate: fast + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/lacp_rate') as f: + self.assertEqual(f.read().strip(), 'fast 1') + + def test_bond_mode_activebackup_failover_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: active-backup + fail-over-mac-policy: follow + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'active-backup 1') + with open('/sys/class/net/mybond/bonding/fail_over_mac') as f: + self.assertEqual(f.read().strip(), 'follow 2') + + def test_bond_mode_balance_xor(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: balance-xor + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-xor 2') + + def test_bond_mode_balance_rr(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: balance-rr + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-rr 0') + + def test_bond_mode_balance_rr_pps(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: balance-rr + packets-per-slave: 15 + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-rr 0') + with open('/sys/class/net/mybond/bonding/packets_per_slave') as f: + self.assertEqual(f.read().strip(), '15') + + def test_bond_resend_igmp(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + bonds: + mybond: + interfaces: [ethbn, ethb2] + parameters: + mode: balance-rr + 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() + self.assert_iface_up(self.dev_e_client, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e_client, result) + self.assertIn(self.dev_e2_client, result) + with open('/sys/class/net/mybond/bonding/resend_igmp') as f: + self.assertEqual(f.read().strip(), '100') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_bond_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: + name: %(ec)s + macaddress: %(ec_mac)s + bonds: + mybond: + interfaces: [ethbn] + macaddress: 00:01:02:03:04:05 + dhcp4: yes''' % {'r': self.backend, + 'ec': self.dev_e_client, + 'e2c': self.dev_e2_client, + 'ec_mac': self.dev_e_client_mac}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['master mybond']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) + + def test_bond_down_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + down-delay: 10s + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/downdelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_up_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + up-delay: 10000 + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/updelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_arp_interval(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [ 192.168.5.1 ] + arp-interval: 50s + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_interval') as f: + self.assertEqual(f.read().strip(), '50000') + + def test_bond_arp_targets(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-interval: 50000 + arp-ip-targets: [ 192.168.5.1 ] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: + self.assertEqual(f.read().strip(), '192.168.5.1') + + def test_bond_arp_all_targets(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-all-targets: all + arp-validate: all + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: + self.assertEqual(f.read().strip(), 'all 1') + + def test_bond_arp_validate(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-validate: all + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_validate') as f: + self.assertEqual(f.read().strip(), 'all 3') + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not support setting MAC for a bond") + def test_bond_mac(self): + pass + + def test_bond_down_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + down-delay: 10000 + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/downdelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_up_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + up-delay: 10000 + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/updelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_arp_interval(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [ 192.168.5.1 ] + arp-interval: 50000 + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_interval') as f: + self.assertEqual(f.read().strip(), '50000') + + def test_bond_arp_targets(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-interval: 50000 + arp-ip-targets: [ 192.168.5.1 ] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: + self.assertEqual(f.read().strip(), '192.168.5.1') + + def test_bond_arp_all_targets(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-all-targets: all + arp-validate: all + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: + self.assertEqual(f.read().strip(), 'all 1') + + def test_bond_mode_balance_tlb_learn_interval(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + bonds: + mybond: + parameters: + mode: balance-tlb + mii-monitor-interval: 5 + learn-packet-interval: 15 + interfaces: [ethbn] + dhcp4: yes''' % {'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, + ['master mybond'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-tlb 5') + with open('/sys/class/net/mybond/bonding/lp_interval') as f: + self.assertEqual(f.read().strip(), '15') + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/bridges.py b/tests/integration/bridges.py new file mode 100644 index 0000000..4c66b28 --- /dev/null +++ b/tests/integration/bridges.py @@ -0,0 +1,461 @@ +#!/usr/bin/python3 +# +# Integration tests for bridges +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import time +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_eth_and_bridge(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + dhcp4: yes''' % {'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]) + + # ensure that they do not get managed by NM for foreign backends + expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' + out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) + for i in [self.dev_e_client, self.dev_e2_client, 'mybr']: + self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) + + def test_bridge_path_cost(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] + parameters: + path-cost: + ethbr: 50 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/brif/%s/path_cost' % self.dev_e2_client) as f: + self.assertEqual(f.read().strip(), '50') + + def test_bridge_ageing_time(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] + parameters: + ageing-time: 21 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/ageing_time') as f: + self.assertEqual(f.read().strip(), '2100') + + def test_bridge_max_age(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] + parameters: + max-age: 12 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/max_age') as f: + self.assertEqual(f.read().strip(), '1200') + + def test_bridge_hello_time(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] + parameters: + hello-time: 1 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/hello_time') as f: + self.assertEqual(f.read().strip(), '100') + + def test_bridge_forward_delay(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] + parameters: + forward-delay: 10 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/forward_delay') as f: + self.assertEqual(f.read().strip(), '1000') + + def test_bridge_stp_false(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] + parameters: + hello-time: 100000 + max-age: 100000 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/stp_state') as f: + self.assertEqual(f.read().strip(), '0') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_bridge_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], 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: %(ec)s + macaddress: %(ec_mac)s + bridges: + br0: + interfaces: [ethbr] + macaddress: 00:01:02:03:04:05 + dhcp4: yes''' % {'r': self.backend, + 'ec': self.dev_e_client, + 'e2c': self.dev_e2_client, + 'ec_mac': self.dev_e_client_mac}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['master br0'], ['inet']) + self.assert_iface_up('br0', + ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) + + 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) + 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] + parameters: + port-priority: + ethbr: 42 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: + self.assertEqual(f.read().strip(), '42') + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not support setting MAC for a bridge") + def test_bridge_mac(self): + pass + + def test_bridge_priority(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] + parameters: + priority: 16384 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/bridge/priority') as f: + self.assertEqual(f.read().strip(), '16384') + + def test_bridge_port_priority(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] + parameters: + port-priority: + ethbr: 42 + stp: false + dhcp4: yes''' % {'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]) + with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: + self.assertEqual(f.read().strip(), '42') + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py new file mode 100644 index 0000000..f4fed07 --- /dev/null +++ b/tests/integration/ethernets.py @@ -0,0 +1,249 @@ +#!/usr/bin/python3 +# +# Integration tests for ethernet devices and features common to all device +# types. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, nm_uses_dnsmasq, resolved_in_use, test_backends + + +class _CommonTests(): + + def test_eth_mtu(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + enmtus: + match: {name: %(e2c)s} + mtu: 1492 + dhcp4: yes''' % {'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, + ['inet 192.168.6.[0-9]+/24']) + out = subprocess.check_output(['ip', 'a', 'show', self.dev_e2_client], + universal_newlines=True) + self.assertTrue('mtu 1492' in out, "checking MTU, should be 1492") + + def test_eth_mac(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + enmac: + match: {name: %(e2c)s} + macaddress: 00:01:02:03:04:05 + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e2_client, + ['inet 192.168.6.[0-9]+/24', '00:01:02:03:04:05'], + ['master']) + out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], + universal_newlines=True) + self.assertTrue('ether 00:01:02:03:04:05' in out) + subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, + 'address', self.dev_e2_client_mac]) + + def test_manual_addresses(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] + dhcp4: yes + %(e2c)s: + addresses: ["172.16.1.2/24"] + gateway4: "172.16.1.1" + nameservers: + addresses: [172.1.2.3] + search: ["fakesuffix"] +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet 172.16.42.99/18', + 'inet6 1234:ffff::42/64', + 'inet 192.168.5.[0-9]+/24']) # from DHCP + self.assert_iface_up(self.dev_e2_client, + ['inet 172.16.1.2/24']) + + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'default via 172.16.1.1', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) + + # ensure that they do not get managed by NM for foreign backends + expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' + out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) + for i in [self.dev_e_client, self.dev_e2_client]: + self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) + + with open('/etc/resolv.conf') as f: + resolv_conf = f.read() + + if self.backend == 'NetworkManager' and nm_uses_dnsmasq: + sys.stdout.write('[NM with dnsmasq] ') + sys.stdout.flush() + self.assertRegex(resolv_conf, 'search.*fakesuffix') + # not easy to peek dnsmasq's brain, so check its logging + out = subprocess.check_output(['journalctl', '--quiet', '-tdnsmasq', '-ocat', '--since=-30s'], + universal_newlines=True) + self.assertIn('nameserver 172.1.2.3', out) + elif resolved_in_use(): + sys.stdout.write('[resolved] ') + sys.stdout.flush() + out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + self.assertIn('DNS Servers: 172.1.2.3', out) + self.assertIn('fakesuffix', out) + else: + sys.stdout.write('[/etc/resolv.conf] ') + sys.stdout.flush() + self.assertRegex(resolv_conf, 'search.*fakesuffix') + # /etc/resolve.conf often already has three nameserver entries + if 'nameserver 172.1.2.3' not in resolv_conf: + self.assertGreaterEqual(resolv_conf.count('nameserver'), 3) + + # change the addresses, make sure that "apply" does not leave leftovers + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] + gateway6: "9876:BBBB::1" + %(e2c)s: + addresses: ["172.16.7.2/30", "4321:AAAA::99/80"] + dhcp4: yes +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.start_dnsmasq(None, self.dev_e2_ap) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet 172.16.5.3/20'], + ['inet 192.168.5', # old DHCP + 'inet 172.16.42', # old static IPv4 + 'inet6 1234']) # old static IPv6 + self.assert_iface_up(self.dev_e2_client, + ['inet 172.16.7.2/30', + 'inet6 4321:aaaa::99/80', + 'inet 192.168.6.[0-9]+/24'], # from DHCP + ['inet 172.16.1']) # old static IPv4 + + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'via 9876:bbbb::1', + subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) + self.assertIn(b'default via 192.168.6.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) + + def test_dhcp6(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: yes + accept-ra: yes + %(e2c)s: {}''' % {'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, ['inet6 2600:'], ['inet 192.168']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_eth_dhcp6_off(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + accept-ra: yes + addresses: [ '192.168.1.100/24' ] + %(e2c)s: {}''' % {'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, ['inet6 2600:'], []) + + def test_eth_dhcp6_off_no_accept_ra(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + accept-ra: no + addresses: [ '192.168.1.100/24' ] + %(e2c)s: {}''' % {'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, [], ['inet6 2600:']) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not disable accept_ra: bug LP: #1704210") + def test_eth_dhcp6_off(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + addresses: [ '192.168.1.100/24' ] + %(e2c)s: {}''' % {'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, [], ['inet6 2600:']) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/regressions.py b/tests/integration/regressions.py new file mode 100644 index 0000000..10ef8dc --- /dev/null +++ b/tests/integration/regressions.py @@ -0,0 +1,91 @@ +#!/usr/bin/python3 +# +# Regression tests to catch previously-fixed issues. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_empty_yaml_lp1795343(self): + with open(self.config, 'w') as f: + f.write('''''') + self.generate_and_settle() + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_lp1802322_bond_mac_rename(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn1: + match: {name: %(ec)s} + dhcp4: no + ethbn2: + match: {name: %(e2c)s} + dhcp4: no + bonds: + mybond: + interfaces: [ethbn1, ethbn2] + macaddress: 00:0a:f7:72:a7:28 + mtu: 9000 + addresses: [ 192.168.5.9/24 ] + gateway4: 192.168.5.1 + parameters: + down-delay: 0 + lacp-rate: fast + mii-monitor-interval: 100 + mode: 802.3ad + transmit-hash-policy: layer3+4 + up-delay: 0 + ''' % {'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, + ['master mybond', '00:0a:f7:72:a7:28'], + ['inet ']) + self.assert_iface_up(self.dev_e2_client, + ['master mybond', '00:0a:f7:72:a7:28'], + ['inet ']) + self.assert_iface_up('mybond', + ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertIn(self.dev_e_client, f.read().strip()) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/routing.py b/tests/integration/routing.py new file mode 100644 index 0000000..06301d5 --- /dev/null +++ b/tests/integration/routing.py @@ -0,0 +1,214 @@ +#!/usr/bin/python3 +# +# Integration tests for routing functions +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + @unittest.skip("fails due to networkd bug setting routes with dhcp") + def test_routes_v4_with_dhcp(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + 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 via 192.168.5.254', # from static route + 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_routes_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 + via: 192.168.5.254 + 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 via 192.168.5.254', # from DHCP + 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_routes_v6(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["9876:BBBB::11/70"] + gateway6: "9876:BBBB::1" + routes: + - to: 2001:f00f:f00f::1/64 + via: 9876:BBBB::5 + metric: 799''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet6 9876:bbbb::11/70']) + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'via 9876:bbbb::1', + subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) + self.assertIn(b'2001:f00f:f00f::/64 via 9876:bbbb::5', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 799', + subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64'])) + + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _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'])) + + @unittest.skip("networkd does not handle non-unicast routes correctly yet (Invalid argument)") + def test_route_type_blackhole(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: [ "10.20.10.1/24" ] + routes: + - to: 10.10.10.0/24 + via: 10.20.10.100 + type: blackhole''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet ']) + self.assertIn(b'blackhole 10.10.10.0/24', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + + def test_route_on_link(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + dhcp4: no + addresses: [ "10.20.10.1/24" ] + routes: + - to: 20.0.0.0/24 + via: 10.10.10.10 + on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet ']) + self.assertIn(b'20.0.0.0/24 via 10.10.10.10 proto static onlink', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + + def test_route_with_policy(self): + self.setup_eth(None) + self.start_dnsmasq(None, self.dev_e2_ap) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: [ "10.20.10.1/24" ] + routes: + - to: 40.0.0.0/24 + via: 10.20.10.55 + metric: 50 + - to: 40.0.0.0/24 + via: 10.20.10.88 + table: 99 + metric: 50 + routing-policy: + - from: 10.20.10.0/24 + to: 40.0.0.0/24 + table: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['inet ']) + self.assertIn(b'to 40.0.0.0/24 lookup 99', + subprocess.check_output(['ip', 'rule', 'show'])) + self.assertIn(b'40.0.0.0/24 via 10.20.10.88', + subprocess.check_output(['ip', 'route', 'show', 'table', '99'])) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/run.py b/tests/integration/run.py new file mode 100755 index 0000000..0a5a7d5 --- /dev/null +++ b/tests/integration/run.py @@ -0,0 +1,79 @@ +#!/usr/bin/python3 +# +# Test runner for netplan integration tests. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import glob +import os +import subprocess +import textwrap + +tests_dir = os.path.dirname(os.path.abspath(__file__)) + +default_backends = [ 'networkd', 'NetworkManager' ] +fixtures = [ "__init__.py", "base.py", "run.py" ] + +possible_tests = [] +testfiles = glob.glob(os.path.join(tests_dir, "*.py")) +for pyfile in testfiles: + filename = os.path.basename(pyfile) + if filename not in fixtures: + possible_tests.append(filename.split('.')[0]) + +def dedupe(duped_list): + deduped = set() + for item in duped_list: + real_items = item.split(",") + for real_item in real_items: + deduped.add(real_item) + return deduped + +# XXX: omg, this is ugly :) +parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(""" +Test runner for netplan integration tests + +Available tests: +{} +""".format("\n".join(" - {}".format(x) for x in sorted(possible_tests))))) + +parser.add_argument('--test', action='append', help="List of tests to be run") +parser.add_argument('--backend', action='append', help="List of backends to test (NetworkManager, networkd)") + +args = parser.parse_args() + +requested_tests = set() +backends = set() + +if args.test is not None: + requested_tests = dedupe(args.test) +else: + requested_tests.update(possible_tests) + +if args.backend is not None: + backends = dedupe(args.backend) +else: + backends.update(default_backends) + +os.environ["NETPLAN_TEST_BACKENDS"] = ",".join(backends) + +for test in requested_tests: + subprocess.call(['python3', os.path.join(tests_dir, "{}.py".format(test))]) diff --git a/tests/integration/scenarios.py b/tests/integration/scenarios.py new file mode 100644 index 0000000..007c062 --- /dev/null +++ b/tests/integration/scenarios.py @@ -0,0 +1,132 @@ +#!/usr/bin/python3 +# +# Integration tests for complex networking scenarios +# (ie. mixes of various features, may test real live cases) +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_mix_bridge_on_bond(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + bridges: + br0: + interfaces: [bond0] + addresses: ['192.168.0.2/24'] + bonds: + bond0: + interfaces: [ethb2] + parameters: + mode: balance-rr + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e2_client, + ['master bond0'], + ['inet ']) + self.assert_iface_up('bond0', + ['master br0']) + ipaddr = subprocess.check_output(['ip', 'a', 'show', 'dev', 'br0'], + universal_newlines=True) + self.assertIn('inet 192.168', ipaddr) + with open('/sys/class/net/bond0/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e2_client, result) + + def test_mix_vlan_on_bridge_on_bond(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + vlans: + vlan1: + link: 'br0' + id: 1 + addresses: [ '10.10.10.1/24' ] + bridges: + br0: + interfaces: ['bond0', 'vlan2'] + parameters: + stp: false + path-cost: + bond0: 1000 + vlan2: 2000 + bonds: + bond0: + interfaces: ['br1'] + parameters: + mode: balance-rr + bridges: + br1: + interfaces: ['ethb2'] + vlans: + vlan2: + link: ethbn + id: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up('vlan1', ['vlan1@br0']) + self.assert_iface_up('vlan2', + ['vlan2@' + self.dev_e_client, 'master br0']) + self.assert_iface_up(self.dev_e2_client, + ['master br1'], + ['inet ']) + self.assert_iface_up('bond0', + ['master br0']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/tunnels.py b/tests/integration/tunnels.py new file mode 100644 index 0000000..9991a90 --- /dev/null +++ b/tests/integration/tunnels.py @@ -0,0 +1,194 @@ +#!/usr/bin/python3 +# Tunnel integration tests. NM and networkd are started on the generated +# configuration, using emulated ethernets (veth). +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + +class _CommonTests(): + + def test_tunnel_sit(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'sit-tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + sit-tun0: + mode: sit + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('sit-tun0', ['sit-tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_ipip(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: ipip + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_tunnel_gre(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: gre + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_gre6(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: ip6gre + local: fe80::1 + remote: 2001:dead:beef::2 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) + + def test_tunnel_vti(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: vti + keys: 1234 + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_vti6(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: vti6 + keys: 1234 + local: fe80::1 + remote: 2001:dead:beef::2 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + def test_tunnel_gre(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + tunnels: + tun0: + mode: gre + keys: 1234 + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/vlans.py b/tests/integration/vlans.py new file mode 100644 index 0000000..a6fe515 --- /dev/null +++ b/tests/integration/vlans.py @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +# +# Integration tests for VLAN virtual devices +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_vlan(self): + # we create two VLANs on e2c, and run dnsmasq on ID 2002 to test DHCP via VLAN + self.setup_eth(None, start_dnsmasq=False) + self.start_dnsmasq(None, self.dev_e2_ap) + subprocess.check_call(['ip', 'link', 'add', 'link', self.dev_e2_ap, + 'name', 'nptestsrv', 'type', 'vlan', 'id', '2002']) + subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', 'nptestsrv']) + subprocess.check_call(['ip', 'link', 'set', 'nptestsrv', 'up']) + self.start_dnsmasq(None, 'nptestsrv') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: {} + myether: + match: {name: %(e2c)s} + dhcp4: yes + vlans: + nptestone: + id: 1001 + link: myether + addresses: [10.9.8.7/24] + nptesttwo: + id: 2002 + link: myether + dhcp4: true + ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + + self.assert_iface_up('nptestone', ['nptestone@' + self.dev_e2_client, 'inet 10.9.8.7/24']) + self.assert_iface_up('nptesttwo', ['nptesttwo@' + self.dev_e2_client, 'inet 192.168.5']) + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptestone'])) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptesttwo'])) + + def test_vlan_mac_address(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'myvlan'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + %(e2c)s: {} + vlans: + myvlan: + id: 101 + link: ethbn + macaddress: aa:bb:cc:dd:ee:22 + ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up('myvlan', ['myvlan@' + self.dev_e_client]) + with open('/sys/class/net/myvlan/address') as f: + self.assertEqual(f.read().strip(), 'aa:bb:cc:dd:ee:22') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/wifi.py b/tests/integration/wifi.py new file mode 100644 index 0000000..41a215d --- /dev/null +++ b/tests/integration/wifi.py @@ -0,0 +1,176 @@ +#!/usr/bin/python3 +# +# Integration tests for wireless devices +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + @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) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + wifi_ifs: + match: + driver: mac80211_hwsim + dhcp4: yes + access-points: + "fake net": {} + decoy: {}''' % {'r': self.backend}) + self.generate_and_settle() + p = subprocess.Popen(['netplan', 'generate', '--mapping', 'mac80211_hwsim'], + stdout=subprocess.PIPE) + out = p.communicate()[0] + self.assertEquals(p.returncode, 1) + self.assertIn(b'mac80211_hwsim', out) + + def test_wifi_ipv4_open(self): + self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + %(wc)s: + dhcp4: yes + access-points: + "fake net": {} + decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) + self.generate_and_settle() + # nm-online doesn't wait for wifis, argh + if self.backend == 'NetworkManager': + self.nm_wait_connected(self.dev_w_client, 60) + + self.assert_iface_up(self.dev_w_client, + ['inet 192.168.5.[0-9]+/24'], + ['master']) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) + if self.backend == 'NetworkManager': + out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) + self.assertRegex(out, 'IP4.DNS.*192.168.5.1') + else: + out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'DNS.*192.168.5.1') + + def test_wifi_ipv4_wpa2(self): + self.setup_ap('''hw_mode=g +channel=1 +ssid=fake net +wpa=1 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +wpa_passphrase=12345678 +''', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + %(wc)s: + dhcp4: yes + access-points: + "fake net": + password: 12345678 + decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) + self.generate_and_settle() + # nm-online doesn't wait for wifis, argh + if self.backend == 'NetworkManager': + self.nm_wait_connected(self.dev_w_client, 60) + + self.assert_iface_up(self.dev_w_client, + ['inet 192.168.5.[0-9]+/24'], + ['master']) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) + if self.backend == 'NetworkManager': + out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) + self.assertRegex(out, 'IP4.DNS.*192.168.5.1') + else: + out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'DNS.*192.168.5.1') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + def test_wifi_ap_open(self): + # we use dev_w_client and dev_w_ap in switched roles here, to keep the + # existing device blacklisting in NM; i. e. dev_w_client is the + # NM-managed AP, and dev_w_ap the manually managed client + with open(self.config, 'w') as f: + f.write('''network: + wifis: + renderer: NetworkManager + %(wc)s: + dhcp4: yes + access-points: + "fake net": + mode: ap''' % {'wc': self.dev_w_client}) + self.generate_and_settle() + + # nm-online doesn't wait for wifis, argh + self.nm_wait_connected(self.dev_w_client, 60) + + out = subprocess.check_output(['iw', 'dev', self.dev_w_client, 'info'], + universal_newlines=True) + self.assertIn('type AP', out) + self.assertIn('ssid fake net', out) + + # connect the other end + subprocess.check_call(['ip', 'link', 'set', self.dev_w_ap, 'up']) + subprocess.check_call(['iw', 'dev', self.dev_w_ap, 'connect', 'fake net']) + out = subprocess.check_output(['dhclient', '-1', '-v', self.dev_w_ap], + stderr=subprocess.STDOUT, universal_newlines=True) + self.assertIn('DHCPACK', out) + out = subprocess.check_output(['iw', 'dev', self.dev_w_ap, 'info'], + universal_newlines=True) + self.assertIn('type managed', out) + self.assertIn('ssid fake net', out) + out = subprocess.check_output(['ip', 'a', 'show', self.dev_w_ap], + universal_newlines=True) + self.assertIn('state UP', out) + self.assertIn('inet 10.', out) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/validate_docs.sh b/tests/validate_docs.sh index 2a63359..2b5e345 100755 --- a/tests/validate_docs.sh +++ b/tests/validate_docs.sh @@ -18,27 +18,27 @@ for term in $(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/p # 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 + continue fi # 2. "[blah, ]``blah``[, ``blah2``]: (scalar|bool|...) - if egrep "\`\`$term\`\`.*\((scalar|bool|mapping|sequence of scalars)\)" doc/netplan.md > /dev/null; then - continue + 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 + continue fi # 4. search doesn't get a full description but it's good enough if [[ $term = "search" ]]; then - continue + continue fi # 5. gratuit_i_ous arp gets a special note if [[ $term = "gratuitious-arp" ]]; then - continue + continue fi echo ERROR: The key "$term" is defined in the parser but not documented. |