diff options
-rwxr-xr-x | cloudinit/cmd/devel/hotplug_hook.py | 4 | ||||
-rw-r--r-- | cloudinit/distros/__init__.py | 18 | ||||
-rw-r--r-- | cloudinit/net/activators.py | 46 | ||||
-rw-r--r-- | config/cloud.cfg.tmpl | 1 | ||||
-rw-r--r-- | doc/rtd/topics/network-config.rst | 27 | ||||
-rw-r--r-- | tests/unittests/cmd/devel/test_hotplug_hook.py | 10 | ||||
-rw-r--r-- | tests/unittests/distros/test_netconfig.py | 59 | ||||
-rw-r--r-- | tests/unittests/test_net_activators.py | 31 |
8 files changed, 147 insertions, 49 deletions
diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py index bc8f3ef3..f95e8cc0 100755 --- a/cloudinit/cmd/devel/hotplug_hook.py +++ b/cloudinit/cmd/devel/hotplug_hook.py @@ -10,7 +10,7 @@ import time from cloudinit import log, reporting, stages from cloudinit.event import EventScope, EventType -from cloudinit.net import activators, read_sys_net_safe +from cloudinit.net import read_sys_net_safe from cloudinit.net.network_state import parse_net_config_data from cloudinit.reporting import events from cloudinit.sources import DataSource, DataSourceNotFoundException @@ -132,7 +132,7 @@ class NetHandler(UeventHandler): bring_up=False, ) interface_name = os.path.basename(self.devpath) - activator = activators.select_activator() + activator = self.datasource.distro.network_activator() if self.action == "add": if not activator.bring_up_interface(interface_name): raise RuntimeError( diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index e27a3f93..ffa41093 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -16,7 +16,7 @@ import stat import string import urllib.parse from io import StringIO -from typing import Any, Mapping, Type +from typing import Any, Mapping, Optional, Type from cloudinit import importer from cloudinit import log as logging @@ -118,6 +118,17 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): "_write_network_config needs implementation.\n" % self.name ) + @property + def network_activator(self) -> Optional[Type[activators.NetworkActivator]]: + """Return the configured network activator for this environment.""" + priority = util.get_cfg_by_path( + self._cfg, ("network", "activators"), None + ) + try: + return activators.select_activator(priority=priority) + except activators.NoActivatorException: + return None + def _write_network_state(self, network_state): priority = util.get_cfg_by_path( self._cfg, ("network", "renderers"), None @@ -242,9 +253,8 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # Now try to bring them up if bring_up: LOG.debug("Bringing up newly configured network interfaces") - try: - network_activator = activators.select_activator() - except activators.NoActivatorException: + network_activator = self.network_activator + if not network_activator: LOG.warning( "No network activator found, not bringing up " "network interfaces" diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index f2cc078f..b6af3770 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -1,7 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging from abc import ABC, abstractmethod -from typing import Iterable, List, Type +from typing import Dict, Iterable, List, Optional, Type, Union from cloudinit import subp, util from cloudinit.net.eni import available as eni_available @@ -32,7 +32,7 @@ def _alter_interface(cmd, device_name) -> bool: class NetworkActivator(ABC): @staticmethod @abstractmethod - def available() -> bool: + def available(target: Optional[str] = None) -> bool: """Return True if activator is available, otherwise return False.""" raise NotImplementedError() @@ -97,7 +97,7 @@ class IfUpDownActivator(NetworkActivator): # E.g., NetworkManager has a ifupdown plugin that requires the name # of a specific connection. @staticmethod - def available(target=None) -> bool: + def available(target: str = None) -> bool: """Return true if ifupdown can be used on this system.""" return eni_available(target=target) @@ -254,33 +254,43 @@ class NetworkdActivator(NetworkActivator): # This section is mostly copied and pasted from renderers.py. An abstract # version to encompass both seems overkill at this point DEFAULT_PRIORITY = [ - IfUpDownActivator, - NetplanActivator, - NetworkManagerActivator, - NetworkdActivator, + "eni", + "netplan", + "network-manager", + "networkd", ] +NAME_TO_ACTIVATOR: Dict[str, Type[NetworkActivator]] = { + "eni": IfUpDownActivator, + "netplan": NetplanActivator, + "network-manager": NetworkManagerActivator, + "networkd": NetworkdActivator, +} + def search_activator( - priority=None, target=None + priority: List[str], target: Union[str, None] ) -> List[Type[NetworkActivator]]: - if priority is None: - priority = DEFAULT_PRIORITY - unknown = [i for i in priority if i not in DEFAULT_PRIORITY] if unknown: raise ValueError( "Unknown activators provided in priority list: %s" % unknown ) - - return [activator for activator in priority if activator.available(target)] + activator_classes = [NAME_TO_ACTIVATOR[name] for name in priority] + return [ + activator_cls + for activator_cls in activator_classes + if activator_cls.available(target) + ] -def select_activator(priority=None, target=None) -> Type[NetworkActivator]: +def select_activator( + priority: Optional[List[str]] = None, target: Optional[str] = None +) -> Type[NetworkActivator]: + if priority is None: + priority = DEFAULT_PRIORITY found = search_activator(priority, target) if not found: - if priority is None: - priority = DEFAULT_PRIORITY tmsg = "" if target and target != "/": tmsg = " in target=%s" % target @@ -289,5 +299,7 @@ def select_activator(priority=None, target=None) -> Type[NetworkActivator]: "through list: %s" % (tmsg, priority) ) selected = found[0] - LOG.debug("Using selected activator: %s", selected) + LOG.debug( + "Using selected activator: %s from priority: %s", selected, priority + ) return selected diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 8da8f18c..5be80f53 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -215,6 +215,7 @@ system_info: {# SRU_BLOCKER: do not ship network renderers on Xenial, Bionic or Eoan #} network: renderers: ['netplan', 'eni', 'sysconfig'] + activators: ['netplan', 'eni', 'network-manager', 'networkd'] # Automatically discover the best ntp_client ntp_client: auto # Other config here will be given to the distro class and/or path classes diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst index 682637c4..3e48555f 100644 --- a/doc/rtd/topics/network-config.rst +++ b/doc/rtd/topics/network-config.rst @@ -80,7 +80,8 @@ Disabling Network Activation Some datasources may not be initialized until after network has been brought up. In this case, cloud-init will attempt to bring up the interfaces specified -by the datasource metadata. +by the datasource metadata using a network activator discovered by +`cloudinit.net.activators.select_activators`_. This behavior can be disabled in the cloud-init configuration dictionary, merged from ``/etc/cloud/cloud.cfg`` and ``/etc/cloud/cloud.cfg.d/*``:: @@ -215,6 +216,15 @@ network configuration for supported backends such as ``systemd-networkd`` and Sysconfig format is used by RHEL, CentOS, Fedora and other derivatives. +- **NetBSD, OpenBSD, FreeBSD** + +Network renders supporting BSD releases which typically write configuration to +``/etc/rc.conf``. Unique to BSD renderers is that each renderer also calls +something akin to `FreeBSD.start_services`_ which will invoke applicable +network services to setup the network, making network activators unneeded +for BSD flavors at the moment. + + Network Output Policy ===================== @@ -225,6 +235,18 @@ is as follows: - Sysconfig - Netplan - NetworkManager +- FreeBSD +- NetBSD +- OpenBSD +- Networkd + +The default policy for selecting a network ``activator`` in order of preference +is as follows: +- ENI: using `ifup`, `ifdown` to manage device setup/teardown +- Netplan: using `netplan apply` to manage device setup/teardown +- NetworkManager: using `nmcli` to manage device setup/teardown +- Networkd: using `ip` to manage device setup/teardown + When applying the policy, `Cloud-init`_ checks if the current instance has the correct binaries and paths to support the renderer. The first renderer that @@ -234,6 +256,7 @@ supplying an updated configuration in cloud-config. :: system_info: network: renderers: ['netplan', 'network-manager', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] + activators: ['eni', 'netplan', 'network-manager', 'networkd'] Network Configuration Tools @@ -295,5 +318,7 @@ Example output converting V2 to sysconfig: .. _SmartOS JSON Metadata: https://eng.joyent.com/mdata/datadict.html .. _UpCloud JSON metadata: https://developers.upcloud.com/1.3/8-servers/#metadata-service .. _Vultr JSON metadata: https://www.vultr.com/metadata/ +.. _cloudinit.net.activators.select_activators: https://github.com/canonical/cloud-init/blob/main/cloudinit/net/activators.py#L279 +.. _FreeBSD.start_services: https://github.com/canonical/cloud-init/blob/main/cloudinit/net/freebsd.py#L28 .. vi: textwidth=79 diff --git a/tests/unittests/cmd/devel/test_hotplug_hook.py b/tests/unittests/cmd/devel/test_hotplug_hook.py index 5ecb5969..d2ef82b1 100644 --- a/tests/unittests/cmd/devel/test_hotplug_hook.py +++ b/tests/unittests/cmd/devel/test_hotplug_hook.py @@ -19,7 +19,9 @@ FAKE_MAC = "11:22:33:44:55:66" @pytest.fixture def mocks(): m_init = mock.MagicMock(spec=Init) + m_activator = mock.MagicMock(spec=NetworkActivator) m_distro = mock.MagicMock(spec=Distro) + m_distro.network_activator = mock.PropertyMock(return_value=m_activator) m_datasource = mock.MagicMock(spec=DataSource) m_datasource.distro = m_distro m_init.datasource = m_datasource @@ -41,18 +43,11 @@ def mocks(): return_value=m_network_state, ) - m_activator = mock.MagicMock(spec=NetworkActivator) - select_activator = mock.patch( - "cloudinit.cmd.devel.hotplug_hook.activators.select_activator", - return_value=m_activator, - ) - sleep = mock.patch("time.sleep") read_sys_net.start() update_event_enabled.start() parse_net.start() - select_activator.start() m_sleep = sleep.start() yield namedtuple("mocks", "m_init m_network_state m_activator m_sleep")( @@ -65,7 +60,6 @@ def mocks(): read_sys_net.stop() update_event_enabled.stop() parse_net.stop() - select_activator.stop() sleep.stop() diff --git a/tests/unittests/distros/test_netconfig.py b/tests/unittests/distros/test_netconfig.py index a25be481..38e92f0e 100644 --- a/tests/unittests/distros/test_netconfig.py +++ b/tests/unittests/distros/test_netconfig.py @@ -9,6 +9,7 @@ from unittest import mock from cloudinit import distros, helpers, safeyaml, settings, subp, util from cloudinit.distros.parsers.sys_conf import SysConf +from cloudinit.net.activators import IfUpDownActivator from tests.unittests.helpers import FilesystemMockingTestCase, dir2dict BASE_NET_CFG = """ @@ -252,12 +253,17 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase): super(TestNetCfgDistroBase, self).setUp() self.add_patch("cloudinit.util.system_is_snappy", "m_snappy") - def _get_distro(self, dname, renderers=None): + def _get_distro(self, dname, renderers=None, activators=None): cls = distros.fetch(dname) cfg = settings.CFG_BUILTIN cfg["system_info"]["distro"] = dname + system_info_network_cfg = {} if renderers: - cfg["system_info"]["network"] = {"renderers": renderers} + system_info_network_cfg["renderers"] = renderers + if activators: + system_info_network_cfg["activators"] = activators + if system_info_network_cfg: + cfg["system_info"]["network"] = system_info_network_cfg paths = helpers.Paths({}) return cls(dname, cfg.get("system_info"), paths) @@ -371,7 +377,9 @@ ifconfig_eth1=DHCP class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): def setUp(self): super(TestNetCfgDistroUbuntuEni, self).setUp() - self.distro = self._get_distro("ubuntu", renderers=["eni"]) + self.distro = self._get_distro( + "ubuntu", renderers=["eni"], activators=["eni"] + ) def eni_path(self): return "/etc/network/interfaces.d/50-cloud-init.cfg" @@ -398,6 +406,51 @@ class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): self.assertEqual(expected, results[cfgpath]) self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + def test_apply_network_config_and_bringup_filters_priority_eni_ub(self): + """Network activator search priority can be overridden from config.""" + expected_cfgs = { + self.eni_path(): V1_NET_CFG_OUTPUT, + } + # ub_distro.apply_network_config(V1_NET_CFG, False) + with mock.patch( + "cloudinit.net.activators.select_activator" + ) as select_activator: + select_activator.return_value = IfUpDownActivator + self._apply_and_verify_eni( + self.distro.apply_network_config, + V1_NET_CFG, + expected_cfgs=expected_cfgs.copy(), + bringup=True, + ) + # 2nd call to select_activator via distro.network_activator prop + assert IfUpDownActivator == self.distro.network_activator + self.assertEqual( + [mock.call(priority=["eni"])] * 2, select_activator.call_args_list + ) + + def test_apply_network_config_and_bringup_activator_defaults_ub(self): + """Network activator search priority defaults when unspecified.""" + expected_cfgs = { + self.eni_path(): V1_NET_CFG_OUTPUT, + } + # Don't set activators to see DEFAULT_PRIORITY + self.distro = self._get_distro("ubuntu", renderers=["eni"]) + with mock.patch( + "cloudinit.net.activators.select_activator" + ) as select_activator: + select_activator.return_value = IfUpDownActivator + self._apply_and_verify_eni( + self.distro.apply_network_config, + V1_NET_CFG, + expected_cfgs=expected_cfgs.copy(), + bringup=True, + ) + # 2nd call to select_activator via distro.network_activator prop + assert IfUpDownActivator == self.distro.network_activator + self.assertEqual( + [mock.call(priority=None)] * 2, select_activator.call_args_list + ) + def test_apply_network_config_eni_ub(self): expected_cfgs = { self.eni_path(): V1_NET_CFG_OUTPUT, diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py index 7494b438..afd9056a 100644 --- a/tests/unittests/test_net_activators.py +++ b/tests/unittests/test_net_activators.py @@ -6,6 +6,7 @@ import pytest from cloudinit.net.activators import ( DEFAULT_PRIORITY, + NAME_TO_ACTIVATOR, IfUpDownActivator, NetplanActivator, NetworkdActivator, @@ -79,23 +80,23 @@ def unavailable_mocks(): class TestSearchAndSelect: - def test_defaults(self, available_mocks): - resp = search_activator() - assert resp == DEFAULT_PRIORITY + def test_empty_list(self, available_mocks): + resp = search_activator(priority=DEFAULT_PRIORITY, target=None) + assert resp == [NAME_TO_ACTIVATOR[name] for name in DEFAULT_PRIORITY] activator = select_activator() - assert activator == DEFAULT_PRIORITY[0] + assert activator == NAME_TO_ACTIVATOR[DEFAULT_PRIORITY[0]] def test_priority(self, available_mocks): - new_order = [NetplanActivator, NetworkManagerActivator] - resp = search_activator(priority=new_order) - assert resp == new_order + new_order = ["netplan", "network-manager"] + resp = search_activator(priority=new_order, target=None) + assert resp == [NAME_TO_ACTIVATOR[name] for name in new_order] activator = select_activator(priority=new_order) - assert activator == new_order[0] + assert activator == NAME_TO_ACTIVATOR[new_order[0]] def test_target(self, available_mocks): - search_activator(target="/tmp") + search_activator(priority=DEFAULT_PRIORITY, target="/tmp") assert "/tmp" == available_mocks.m_which.call_args[1]["target"] select_activator(target="/tmp") @@ -106,20 +107,22 @@ class TestSearchAndSelect: return_value=False, ) def test_first_not_available(self, m_available, available_mocks): - resp = search_activator() - assert resp == DEFAULT_PRIORITY[1:] + resp = search_activator(priority=DEFAULT_PRIORITY, target=None) + assert resp == [ + NAME_TO_ACTIVATOR[activator] for activator in DEFAULT_PRIORITY[1:] + ] resp = select_activator() - assert resp == DEFAULT_PRIORITY[1] + assert resp == NAME_TO_ACTIVATOR[DEFAULT_PRIORITY[1]] def test_priority_not_exist(self, available_mocks): with pytest.raises(ValueError): - search_activator(priority=["spam", "eggs"]) + search_activator(priority=["spam", "eggs"], target=None) with pytest.raises(ValueError): select_activator(priority=["spam", "eggs"]) def test_none_available(self, unavailable_mocks): - resp = search_activator() + resp = search_activator(priority=DEFAULT_PRIORITY, target=None) assert resp == [] with pytest.raises(NoActivatorException): |