Merge pull request #4228 from dm0-/coreos-1554

networkd: support marking links unmanaged
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2016-12-02 09:14:27 -05:00 committed by GitHub
commit 169f11d5da
5 changed files with 205 additions and 47 deletions

View File

@ -232,6 +232,18 @@
the network otherwise.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>Unmanaged=</varname></term>
<listitem>
<para>A boolean. When <literal>yes</literal>, no attempts are
made to bring up or configure matching links, equivalent to
when there are no matching network files. Defaults to
<literal>no</literal>.</para>
<para>This is useful for preventing later matching network
files from interfering with certain interfaces that are fully
controlled by other applications.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@ -2523,6 +2523,9 @@ static int link_initialized_and_synced(sd_netlink *rtnl, sd_netlink_message *m,
if (r == -ENOENT) {
link_enter_unmanaged(link);
return 1;
} else if (r == 0 && network->unmanaged) {
link_enter_unmanaged(link);
return 0;
} else if (r < 0)
return r;

View File

@ -29,6 +29,7 @@ Match.Architecture, config_parse_net_condition,
Link.MACAddress, config_parse_hwaddr, 0, offsetof(Network, mac)
Link.MTUBytes, config_parse_iec_size, 0, offsetof(Network, mtu)
Link.ARP, config_parse_tristate, 0, offsetof(Network, arp)
Link.Unmanaged, config_parse_bool, 0, offsetof(Network, unmanaged)
Network.Description, config_parse_string, 0, offsetof(Network, description)
Network.Bridge, config_parse_netdev, 0, offsetof(Network, bridge)
Network.Bond, config_parse_netdev, 0, offsetof(Network, bond)

View File

@ -176,6 +176,7 @@ struct Network {
struct ether_addr *mac;
size_t mtu;
int arp;
bool unmanaged;
uint32_t iaid;
DUID duid;

View File

@ -30,6 +30,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <http://www.gnu.org/licenses/>.
import errno
import os
import sys
import time
@ -39,16 +40,101 @@ import subprocess
import shutil
import socket
networkd_active = subprocess.call(['systemctl', 'is-active', '--quiet',
'systemd-networkd']) == 0
have_dnsmasq = shutil.which('dnsmasq')
HAVE_DNSMASQ = shutil.which('dnsmasq') is not None
NETWORK_UNITDIR = '/run/systemd/network'
NETWORKD_WAIT_ONLINE = shutil.which('systemd-networkd-wait-online',
path='/usr/lib/systemd:/lib/systemd')
RESOLV_CONF = '/run/systemd/resolve/resolv.conf'
@unittest.skipIf(networkd_active,
'networkd is already active')
class ClientTestBase:
def setUpModule():
"""Initialize the environment, and perform sanity checks on it."""
if NETWORKD_WAIT_ONLINE is None:
raise OSError(errno.ENOENT, 'systemd-networkd-wait-online not found')
# Do not run any tests if the system is using networkd already.
if subprocess.call(['systemctl', 'is-active', '--quiet',
'systemd-networkd.service']) == 0:
raise unittest.SkipTest('networkd is already active')
# Avoid "Failed to open /dev/tty" errors in containers.
os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
# Ensure the unit directory exists so tests can dump files into it.
os.makedirs(NETWORK_UNITDIR, exist_ok=True)
class NetworkdTestingUtilities:
"""Provide a set of utility functions to facilitate networkd tests.
This class must be inherited along with unittest.TestCase to define
some required methods.
"""
def write_network(self, unit_name, contents):
"""Write a network unit file, and queue it to be removed."""
unit_path = os.path.join(NETWORK_UNITDIR, unit_name)
with open(unit_path, 'w') as unit:
unit.write(contents)
self.addCleanup(os.remove, unit_path)
def write_network_dropin(self, unit_name, dropin_name, contents):
"""Write a network unit drop-in, and queue it to be removed."""
dropin_dir = os.path.join(NETWORK_UNITDIR, "%s.d" % unit_name)
dropin_path = os.path.join(dropin_dir, "%s.conf" % dropin_name)
os.makedirs(dropin_dir, exist_ok=True)
with open(dropin_path, 'w') as dropin:
dropin.write(contents)
self.addCleanup(os.remove, dropin_path)
def assert_link_states(self, **kwargs):
"""Match networkctl link states to the given ones.
Each keyword argument should be the name of a network interface
with its expected value of the "SETUP" column in output from
networkctl. The interfaces have five seconds to come online
before the check is performed. Every specified interface must
be present in the output, and any other interfaces found in the
output are ignored.
A special interface state "managed" is supported, which matches
any value in the "SETUP" column other than "unmanaged".
"""
if not kwargs:
return
interfaces = set(kwargs)
# Wait for the requested interfaces, but don't fail for them.
subprocess.call([NETWORKD_WAIT_ONLINE, '--timeout=5'] +
['--interface=%s' % iface for iface in kwargs])
# Validate each link state found in the networkctl output.
out = subprocess.check_output(['networkctl', '--no-legend']).rstrip()
for line in out.decode('utf-8').split('\n'):
fields = line.split()
if len(fields) >= 5 and fields[1] in kwargs:
iface = fields[1]
expected = kwargs[iface]
actual = fields[-1]
if (actual != expected and
not (expected == 'managed' and actual != 'unmanaged')):
self.fail("Link %s expects state %s, found %s" %
(iface, expected, actual))
interfaces.remove(iface)
# Ensure that all requested interfaces have been covered.
if interfaces:
self.fail("Missing links in status output: %s" % interfaces)
class ClientTestBase(NetworkdTestingUtilities):
"""Provide common methods for testing networkd against servers."""
@classmethod
def setUpClass(klass):
klass.orig_log_level = subprocess.check_output(
@ -65,19 +151,7 @@ class ClientTestBase:
self.if_router = 'router_eth42'
self.workdir_obj = tempfile.TemporaryDirectory()
self.workdir = self.workdir_obj.name
self.config = '/run/systemd/network/test_eth42.network'
# avoid "Failed to open /dev/tty" errors in containers
os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
# determine path to systemd-networkd-wait-online
for p in ['/usr/lib/systemd/systemd-networkd-wait-online',
'/lib/systemd/systemd-networkd-wait-online']:
if os.path.exists(p):
self.networkd_wait_online = p
break
else:
self.fail('systemd-networkd-wait-online not found')
self.config = 'test_eth42.network'
# get current journal cursor
subprocess.check_output(['journalctl', '--sync'])
@ -93,12 +167,6 @@ class ClientTestBase:
subprocess.call(['ip', 'link', 'del', 'dummy0'],
stderr=subprocess.DEVNULL)
def writeConfig(self, fname, contents):
os.makedirs(os.path.dirname(fname), exist_ok=True)
with open(fname, 'w') as f:
f.write(contents)
self.addCleanup(os.remove, fname)
def show_journal(self, unit):
'''Show journal of given unit since start of the test'''
@ -126,7 +194,7 @@ class ClientTestBase:
def do_test(self, coldplug=True, ipv6=False, extra_opts='',
online_timeout=10, dhcp_mode='yes'):
subprocess.check_call(['systemctl', 'start', 'systemd-resolved'])
self.writeConfig(self.config, '''\
self.write_network(self.config, '''\
[Match]
Name=%s
[Network]
@ -146,7 +214,7 @@ DHCP=%s
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
try:
subprocess.check_call([self.networkd_wait_online, '--interface',
subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface',
self.iface, '--timeout=%i' % online_timeout])
if ipv6:
@ -192,7 +260,7 @@ DHCP=%s
self.assertRegex(out, br'DNS:\s+192.168.5.1')
except (AssertionError, subprocess.CalledProcessError):
# show networkd status, journal, and DHCP server log on failure
with open(self.config) as f:
with open(os.path.join(NETWORK_UNITDIR, self.config)) as f:
print('\n---- %s ----\n%s' % (self.config, f.read()))
print('---- interface status ----')
sys.stdout.flush()
@ -247,12 +315,12 @@ DHCP=%s
self.do_test(coldplug=False, ipv6=True)
def test_route_only_dns(self):
self.writeConfig('/run/systemd/network/myvpn.netdev', '''\
self.write_network('myvpn.netdev', '''\
[NetDev]
Name=dummy0
Kind=dummy
MACAddress=12:34:56:78:9a:bc''')
self.writeConfig('/run/systemd/network/myvpn.network', '''\
self.write_network('myvpn.network', '''\
[Match]
Name=dummy0
[Network]
@ -273,20 +341,16 @@ Domains= ~company''')
self.assertNotIn('nameserver 192.168.42.1\n', contents)
def test_route_only_dns_all_domains(self):
with open('/run/systemd/network/myvpn.netdev', 'w') as f:
f.write('''[NetDev]
self.write_network('myvpn.netdev', '''[NetDev]
Name=dummy0
Kind=dummy
MACAddress=12:34:56:78:9a:bc''')
with open('/run/systemd/network/myvpn.network', 'w') as f:
f.write('''[Match]
self.write_network('myvpn.network', '''[Match]
Name=dummy0
[Network]
Address=192.168.42.100
DNS=192.168.42.1
Domains= ~company ~.''')
self.addCleanup(os.remove, '/run/systemd/network/myvpn.netdev')
self.addCleanup(os.remove, '/run/systemd/network/myvpn.network')
self.do_test(coldplug=True, ipv6=False,
extra_opts='IPv6AcceptRouterAdvertisements=False')
@ -303,7 +367,7 @@ Domains= ~company ~.''')
self.assertIn('nameserver 192.168.42.1\n', contents)
@unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
@unittest.skipUnless(HAVE_DNSMASQ, 'dnsmasq not installed')
class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
'''Test networkd client against dnsmasq'''
@ -366,7 +430,7 @@ class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
# create interface for generic connections; this will map all DNS names
# to 192.168.42.1
self.create_iface(dnsmasq_opts=['--address=/#/192.168.42.1'])
self.writeConfig('/run/systemd/network/general.network', '''\
self.write_network('general.network', '''\
[Match]
Name=%s
[Network]
@ -392,7 +456,7 @@ IPv6AcceptRA=False''' % self.iface)
self.addCleanup(vpn_dnsmasq.wait)
self.addCleanup(vpn_dnsmasq.kill)
self.writeConfig('/run/systemd/network/vpn.network', '''\
self.write_network('vpn.network', '''\
[Match]
Name=testvpnclient
[Network]
@ -402,7 +466,7 @@ DNS=10.241.3.1
Domains= ~company ~lab''')
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
subprocess.check_call([self.networkd_wait_online, '--interface', self.iface,
subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface', self.iface,
'--interface=testvpnclient', '--timeout=20'])
# ensure we start fresh with every test
@ -592,12 +656,12 @@ exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//
# we don't use this interface for this test
self.if_router = None
self.writeConfig('/run/systemd/network/test.netdev', '''\
self.write_network('test.netdev', '''\
[NetDev]
Name=dummy0
Kind=dummy
MACAddress=12:34:56:78:9a:bc''')
self.writeConfig('/run/systemd/network/test.network', '''\
self.write_network('test.network', '''\
[Match]
Name=dummy0
[Network]
@ -624,12 +688,12 @@ Domains= one two three four five six seven eight nine ten''')
name_prefix = 'a' * 60
self.writeConfig('/run/systemd/network/test.netdev', '''\
self.write_network('test.netdev', '''\
[NetDev]
Name=dummy0
Kind=dummy
MACAddress=12:34:56:78:9a:bc''')
self.writeConfig('/run/systemd/network/test.network', '''\
self.write_network('test.network', '''\
[Match]
Name=dummy0
[Network]
@ -652,18 +716,18 @@ Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p=name_prefix))
# we don't use this interface for this test
self.if_router = None
self.writeConfig('/run/systemd/network/test.netdev', '''\
self.write_network('test.netdev', '''\
[NetDev]
Name=dummy0
Kind=dummy
MACAddress=12:34:56:78:9a:bc''')
self.writeConfig('/run/systemd/network/test.network', '''\
self.write_network('test.network', '''\
[Match]
Name=dummy0
[Network]
Address=192.168.42.100
DNS=192.168.42.1''')
self.writeConfig('/run/systemd/network/test.network.d/dns.conf', '''\
self.write_network_dropin('test.network', 'dns', '''\
[Network]
DNS=127.0.0.1''')
@ -704,6 +768,83 @@ DNS=127.0.0.1''')
raise
class UnmanagedClientTest(unittest.TestCase, NetworkdTestingUtilities):
"""Test if networkd manages the correct interfaces."""
def setUp(self):
"""Write .network files to match the named veth devices."""
# Define the veth+peer pairs to be created.
# Their pairing doesn't actually matter, only their names do.
self.veths = {
'm1def': 'm0unm',
'm1man': 'm1unm',
}
# Define the contents of .network files to be read in order.
self.configs = (
"[Match]\nName=m1def\n",
"[Match]\nName=m1unm\n[Link]\nUnmanaged=yes\n",
"[Match]\nName=m1*\n[Link]\nUnmanaged=no\n",
)
# Write out the .network files to be cleaned up automatically.
for i, config in enumerate(self.configs):
self.write_network("%02d-test.network" % i, config)
def tearDown(self):
"""Stop networkd."""
subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
def create_iface(self):
"""Create temporary veth pairs for interface matching."""
for veth, peer in self.veths.items():
subprocess.check_call(['ip', 'link', 'add',
'name', veth, 'type', 'veth',
'peer', 'name', peer])
self.addCleanup(subprocess.call,
['ip', 'link', 'del', 'dev', peer])
def test_unmanaged_setting(self):
"""Verify link states with Unmanaged= settings, hot-plug."""
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
self.create_iface()
self.assert_link_states(m1def='managed',
m1man='managed',
m1unm='unmanaged',
m0unm='unmanaged')
def test_unmanaged_setting_coldplug(self):
"""Verify link states with Unmanaged= settings, cold-plug."""
self.create_iface()
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
self.assert_link_states(m1def='managed',
m1man='managed',
m1unm='unmanaged',
m0unm='unmanaged')
def test_catchall_config(self):
"""Verify link states with a catch-all config, hot-plug."""
# Don't actually catch ALL interfaces. It messes up the host.
self.write_network('all.network', "[Match]\nName=m[01]???\n")
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
self.create_iface()
self.assert_link_states(m1def='managed',
m1man='managed',
m1unm='unmanaged',
m0unm='managed')
def test_catchall_config_coldplug(self):
"""Verify link states with a catch-all config, cold-plug."""
# Don't actually catch ALL interfaces. It messes up the host.
self.write_network('all.network', "[Match]\nName=m[01]???\n")
self.create_iface()
subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
self.assert_link_states(m1def='managed',
m1man='managed',
m1unm='unmanaged',
m0unm='managed')
if __name__ == '__main__':
unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
verbosity=2))