summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcloudinit/cmd/devel/hotplug_hook.py4
-rw-r--r--cloudinit/distros/__init__.py18
-rw-r--r--cloudinit/net/activators.py46
-rw-r--r--config/cloud.cfg.tmpl1
-rw-r--r--doc/rtd/topics/network-config.rst27
-rw-r--r--tests/unittests/cmd/devel/test_hotplug_hook.py10
-rw-r--r--tests/unittests/distros/test_netconfig.py59
-rw-r--r--tests/unittests/test_net_activators.py31
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):