summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrej Shadura <andrew.shadura@collabora.co.uk>2018-12-29 16:31:02 +0100
committerAndrej Shadura <andrew.shadura@collabora.co.uk>2018-12-29 16:32:39 +0100
commitd218075f5b95421e472b397da901c697903f4405 (patch)
treee5ac173f0ff0d1737e94ddc65f996b410ceb404a
parentd37bd89307157a5d76f77f2eb89ac5523156794b (diff)
parent1ea3ec106a8d5f2f6deb3886a1e2dcdbfabfd37c (diff)
Merge branch 'upstream/latest' into debian/master
-rw-r--r--.gitignore8
-rw-r--r--CONTRIBUTING56
-rw-r--r--Makefile4
-rw-r--r--README.md1
-rw-r--r--TODO6
-rw-r--r--debian/tests/control85
-rw-r--r--doc/netplan.md287
-rw-r--r--examples/bridge.yaml3
-rw-r--r--examples/ipv6_tunnel.yaml16
-rw-r--r--examples/route_metric.yaml11
-rw-r--r--examples/wpa_enterprise.yaml26
-rw-r--r--netplan/cli/commands/apply.py29
-rw-r--r--netplan/cli/commands/ip.py2
-rw-r--r--rpm/netplan.spec21
-rw-r--r--snap/snapcraft.yaml42
-rw-r--r--src/generate.c18
-rw-r--r--src/networkd.c290
-rw-r--r--src/nm.c124
-rw-r--r--src/parse.c788
-rw-r--r--src/parse.h98
-rwxr-xr-xtests/generate.py5217
-rw-r--r--tests/generator/__init__.py17
-rw-r--r--tests/generator/base.py169
-rw-r--r--tests/generator/test_args.py172
-rw-r--r--tests/generator/test_auth.py456
-rw-r--r--tests/generator/test_bonds.py756
-rw-r--r--tests/generator/test_bridges.py714
-rw-r--r--tests/generator/test_common.py1244
-rw-r--r--tests/generator/test_dhcp_overrides.py423
-rw-r--r--tests/generator/test_errors.py674
-rw-r--r--tests/generator/test_ethernets.py565
-rw-r--r--tests/generator/test_routing.py932
-rw-r--r--tests/generator/test_tunnels.py1066
-rw-r--r--tests/generator/test_vlans.py223
-rw-r--r--tests/generator/test_wifis.py318
-rwxr-xr-xtests/integration.py2142
-rw-r--r--tests/integration/__init__.py17
-rw-r--r--tests/integration/base.py377
-rw-r--r--tests/integration/bonds.py744
-rw-r--r--tests/integration/bridges.py461
-rw-r--r--tests/integration/ethernets.py249
-rw-r--r--tests/integration/regressions.py91
-rw-r--r--tests/integration/routing.py214
-rwxr-xr-xtests/integration/run.py79
-rw-r--r--tests/integration/scenarios.py132
-rw-r--r--tests/integration/tunnels.py194
-rw-r--r--tests/integration/vlans.py103
-rw-r--r--tests/integration/wifi.py176
-rwxr-xr-xtests/validate_docs.sh12
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.
diff --git a/Makefile b/Makefile
index 3826f16..44f94d4 100644
--- a/Makefile
+++ b/Makefile
@@ -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:
diff --git a/README.md b/README.md
index e1e3526..e4d98da 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/TODO b/TODO
index 4d4675d..610b28a 100644
--- a/TODO
+++ b/TODO
@@ -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);
}
diff --git a/src/nm.c b/src/nm.c
index 2c7005e..d8a6a7c 100644
--- a/src/nm.c
+++ b/src/nm.c
@@ -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.