backport upstream patches
This commit is contained in:
parent
5501bdf1e5
commit
e319d0a355
@ -0,0 +1,231 @@
|
||||
From c53f04aeb2acf9526a2ebf3d3320f149ac46caa6 Mon Sep 17 00:00:00 2001
|
||||
From: Ani Sinha <anisinha@redhat.com>
|
||||
Date: Tue, 2 May 2023 20:35:45 +0530
|
||||
Subject: [PATCH] Do not generate dsa and ed25519 key types when crypto FIPS
|
||||
mode is enabled (#2142)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/c53f04aeb2acf9526a2ebf3d3320f149ac46caa6
|
||||
Conflict:(1)Add extra information to cc_ssh.py:
|
||||
+import logging
|
||||
+LOG = logging.getLogger(__name__)
|
||||
(2)format diffs.
|
||||
(3)add 'M_PATH = "cloudinit.util."' in test_util.py
|
||||
|
||||
DSA and ED25519 key types are not supported when FIPS is enabled in crypto.
|
||||
Check if FIPS has been enabled on the system and if so, do not generate those
|
||||
key types. Presently the check is only available on Linux systems.
|
||||
|
||||
LP: 2017761
|
||||
RHBZ: 2187164
|
||||
|
||||
Signed-off-by: Ani Sinha <anisinha@redhat.com>
|
||||
---
|
||||
cloudinit/config/cc_ssh.py | 23 ++++++++++++++++-
|
||||
cloudinit/util.py | 12 +++++++++
|
||||
cloudinit/config/tests/test_ssh.py | 40 ++++++++++++++++++++++--------
|
||||
tests/unittests/test_util.py | 26 +++++++++++++++++++
|
||||
4 files changed, 90 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
|
||||
index 1053ab6..2fda565 100755
|
||||
--- a/cloudinit/config/cc_ssh.py
|
||||
+++ b/cloudinit/config/cc_ssh.py
|
||||
@@ -167,8 +167,12 @@ from cloudinit import ssh_util
|
||||
from cloudinit import subp
|
||||
from cloudinit import util
|
||||
|
||||
+import logging
|
||||
+LOG = logging.getLogger(__name__)
|
||||
|
||||
GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa', 'ed25519']
|
||||
+FIPS_UNSUPPORTED_KEY_NAMES = ["dsa", "ed25519"]
|
||||
+
|
||||
KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
|
||||
PUBLISH_HOST_KEYS = True
|
||||
# Don't publish the dsa hostkey by default since OpenSSH recommends not using
|
||||
@@ -231,9 +235,26 @@ def handle(_name, cfg, cloud, log, _args):
|
||||
genkeys = util.get_cfg_option_list(cfg,
|
||||
'ssh_genkeytypes',
|
||||
GENERATE_KEY_NAMES)
|
||||
+ # remove keys that are not supported in fips mode if its enabled
|
||||
+ key_names = (
|
||||
+ genkeys
|
||||
+ if not util.fips_enabled()
|
||||
+ else [
|
||||
+ names
|
||||
+ for names in genkeys
|
||||
+ if names not in FIPS_UNSUPPORTED_KEY_NAMES
|
||||
+ ]
|
||||
+ )
|
||||
+ skipped_keys = set(genkeys).difference(key_names)
|
||||
+ if skipped_keys:
|
||||
+ LOG.debug(
|
||||
+ "skipping keys that are not supported in fips mode: %s",
|
||||
+ ",".join(skipped_keys),
|
||||
+ )
|
||||
+
|
||||
lang_c = os.environ.copy()
|
||||
lang_c['LANG'] = 'C'
|
||||
- for keytype in genkeys:
|
||||
+ for keytype in key_names:
|
||||
keyfile = KEY_FILE_TPL % (keytype)
|
||||
if os.path.exists(keyfile):
|
||||
continue
|
||||
diff --git a/cloudinit/util.py b/cloudinit/util.py
|
||||
index 78164de..c18aecf 100644
|
||||
--- a/cloudinit/util.py
|
||||
+++ b/cloudinit/util.py
|
||||
@@ -1418,6 +1418,18 @@ def get_cmdline():
|
||||
return _get_cmdline()
|
||||
|
||||
|
||||
+def fips_enabled() -> bool:
|
||||
+ fips_proc = "/proc/sys/crypto/fips_enabled"
|
||||
+ try:
|
||||
+ contents = load_file(fips_proc).strip()
|
||||
+ return contents == "1"
|
||||
+ except (IOError, OSError):
|
||||
+ # for BSD systems and Linux systems where the proc entry is not
|
||||
+ # available, we assume FIPS is disabled to retain the old behavior
|
||||
+ # for now.
|
||||
+ return False
|
||||
+
|
||||
+
|
||||
def pipe_in_out(in_fh, out_fh, chunk_size=1024, chunk_cb=None):
|
||||
bytes_piped = 0
|
||||
while True:
|
||||
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
|
||||
index 714949c..b8fb610 100644
|
||||
--- a/cloudinit/config/tests/test_ssh.py
|
||||
+++ b/cloudinit/config/tests/test_ssh.py
|
||||
@@ -91,12 +91,16 @@ class TestHandleSsh:
|
||||
expected_calls = [mock.call(set(keys), user)] + expected_calls
|
||||
assert expected_calls == m_setup_keys.call_args_list
|
||||
|
||||
+ @pytest.mark.parametrize("fips_enabled", (True, False))
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_no_cfg(self, m_path_exists, m_nug,
|
||||
- m_glob, m_setup_keys):
|
||||
+ @mock.patch(MODPATH + "util.fips_enabled")
|
||||
+ def test_handle_no_cfg(
|
||||
+ self, m_fips, m_path_exists, m_nug, m_glob, m_setup_keys, fips_enabled
|
||||
+ ):
|
||||
"""Test handle with no config ignores generating existing keyfiles."""
|
||||
+ m_fips.return_value = fips_enabled
|
||||
cfg = {}
|
||||
keys = ["key1"]
|
||||
m_glob.return_value = [] # Return no matching keys to prevent removal
|
||||
@@ -109,12 +113,22 @@ class TestHandleSsh:
|
||||
options = ssh_util.DISABLE_USER_OPTS.replace("$USER", "NONE")
|
||||
options = options.replace("$DISABLE_USER", "root")
|
||||
m_glob.assert_called_once_with('/etc/ssh/ssh_host_*key*')
|
||||
- assert [
|
||||
- mock.call("/etc/ssh/ssh_host_rsa_key"),
|
||||
- mock.call("/etc/ssh/ssh_host_dsa_key"),
|
||||
- mock.call("/etc/ssh/ssh_host_ecdsa_key"),
|
||||
- mock.call("/etc/ssh/ssh_host_ed25519_key"),
|
||||
- ] in m_path_exists.call_args_list
|
||||
+ m_fips.assert_called_once()
|
||||
+
|
||||
+ if not m_fips():
|
||||
+ expected_calls = [
|
||||
+ mock.call("/etc/ssh/ssh_host_rsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_dsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_ecdsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_ed25519_key"),
|
||||
+ ]
|
||||
+ else:
|
||||
+ # Enabled fips doesn't generate dsa or ed25519
|
||||
+ expected_calls = [
|
||||
+ mock.call("/etc/ssh/ssh_host_rsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_ecdsa_key"),
|
||||
+ ]
|
||||
+ assert expected_calls in m_path_exists.call_args_list
|
||||
assert [
|
||||
mock.call(set(keys), "root", options=options)
|
||||
] == m_setup_keys.call_args_list
|
||||
@@ -122,8 +136,10 @@ class TestHandleSsh:
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_dont_allow_public_ssh_keys(self, m_path_exists, m_nug,
|
||||
- m_glob, m_setup_keys):
|
||||
+ @mock.patch(MODPATH + "util.fips_enabled", return_value=False)
|
||||
+ def test_dont_allow_public_ssh_keys(
|
||||
+ self, m_fips, m_path_exists, m_nug, m_glob, m_setup_keys
|
||||
+ ):
|
||||
"""Test allow_public_ssh_keys=False ignores ssh public keys from
|
||||
platform.
|
||||
"""
|
||||
@@ -166,8 +182,10 @@ class TestHandleSsh:
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
+ @mock.patch(MODPATH + "util.fips_enabled", return_value=False)
|
||||
def test_handle_default_root(
|
||||
self,
|
||||
+ m_fips,
|
||||
m_path_exists,
|
||||
m_nug,
|
||||
m_glob,
|
||||
@@ -232,8 +250,10 @@ class TestHandleSsh:
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
+ @mock.patch(MODPATH + "util.fips_enabled", return_value=False)
|
||||
def test_handle_publish_hostkeys(
|
||||
self,
|
||||
+ m_fips,
|
||||
m_path_exists,
|
||||
m_nug,
|
||||
m_glob,
|
||||
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
|
||||
index 2ab3bad..7368b3b 100644
|
||||
--- a/tests/unittests/test_util.py
|
||||
+++ b/tests/unittests/test_util.py
|
||||
@@ -16,6 +16,7 @@ from cloudinit import subp
|
||||
from cloudinit import importer, util
|
||||
from cloudinit.tests import helpers
|
||||
|
||||
+M_PATH = "cloudinit.util."
|
||||
|
||||
class FakeSelinux(object):
|
||||
|
||||
@@ -287,6 +287,31 @@ class TestGetCmdline(helpers.TestCase):
|
||||
self.assertEqual("abcd 123", ret)
|
||||
|
||||
|
||||
+class TestFipsEnabled:
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "fips_enabled_content,expected",
|
||||
+ (
|
||||
+ pytest.param(None, False, id="false_when_no_fips_enabled_file"),
|
||||
+ pytest.param("0\n", False, id="false_when_fips_disabled"),
|
||||
+ pytest.param("1\n", True, id="true_when_fips_enabled"),
|
||||
+ pytest.param("1", True, id="true_when_fips_enabled_no_newline"),
|
||||
+ ),
|
||||
+ )
|
||||
+ @mock.patch(M_PATH + "load_file")
|
||||
+ def test_fips_enabled_based_on_proc_crypto(
|
||||
+ self, load_file, fips_enabled_content, expected, tmpdir
|
||||
+ ):
|
||||
+ def fake_load_file(path):
|
||||
+ assert path == "/proc/sys/crypto/fips_enabled"
|
||||
+ if fips_enabled_content is None:
|
||||
+ raise IOError("No file exists Bob")
|
||||
+ return fips_enabled_content
|
||||
+
|
||||
+ load_file.side_effect = fake_load_file
|
||||
+
|
||||
+ assert expected is util.fips_enabled()
|
||||
+
|
||||
+
|
||||
class TestLoadYaml(helpers.CiTestCase):
|
||||
mydefault = "7b03a8ebace993d806255121073fed52"
|
||||
with_logs = True
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
60
backport-Do-not-silently-ignore-integer-uid-1280.patch
Normal file
60
backport-Do-not-silently-ignore-integer-uid-1280.patch
Normal file
@ -0,0 +1,60 @@
|
||||
From 2837b835f101d81704f018a4f872b1d660eb6f3e Mon Sep 17 00:00:00 2001
|
||||
From: Brett Holman <bholman.devel@gmail.com>
|
||||
Date: Wed, 23 Feb 2022 11:57:59 -0700
|
||||
Subject: [PATCH] Do not silently ignore integer uid (#1280)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/2837b835f101d81704f018a4f872b1d660eb6f3e
|
||||
Conflict:NA
|
||||
|
||||
The docs do not make it obvious that uid is supposed to be of type string.
|
||||
Current behavior is to silently ignore integer uid.
|
||||
|
||||
LP: #1875772
|
||||
---
|
||||
cloudinit/distros/__init__.py | 2 ++
|
||||
tests/integration_tests/modules/test_users_groups.py | 8 ++++++++
|
||||
2 files changed, 10 insertions(+)
|
||||
|
||||
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
|
||||
index 9a695be..2ee8c9e 100755
|
||||
--- a/cloudinit/distros/__init__.py
|
||||
+++ b/cloudinit/distros/__init__.py
|
||||
@@ -495,6 +495,8 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
|
||||
if not util.is_group(group):
|
||||
self.create_group(group)
|
||||
LOG.debug("created group '%s' for user '%s'", group, name)
|
||||
+ if "uid" in kwargs.keys():
|
||||
+ kwargs["uid"] = str(kwargs["uid"])
|
||||
|
||||
# Check the values and create the command
|
||||
for key, val in sorted(kwargs.items()):
|
||||
diff --git a/tests/integration_tests/modules/test_users_groups.py b/tests/integration_tests/modules/test_users_groups.py
|
||||
index bcb17b7..326df67 100644
|
||||
--- a/tests/integration_tests/modules/test_users_groups.py
|
||||
+++ b/tests/integration_tests/modules/test_users_groups.py
|
||||
@@ -39,6 +39,10 @@ AHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
|
||||
gecos: Magic Cloud App Daemon User
|
||||
inactive: true
|
||||
system: true
|
||||
+ - name: eric
|
||||
+ uid: 1742
|
||||
+ - name: archivist
|
||||
+ uid: '1743'
|
||||
"""
|
||||
|
||||
|
||||
@@ -76,6 +80,10 @@ class TestUsersGroups:
|
||||
),
|
||||
# Test the cloudy user
|
||||
(["passwd", "cloudy"], r"cloudy:x:[0-9]{3,4}:"),
|
||||
+ # Test str uid
|
||||
+ (["passwd", "eric"], r"eric:x:1742:"),
|
||||
+ # Test int uid
|
||||
+ (["passwd", "archivist"], r"archivist:x:1743:"),
|
||||
],
|
||||
)
|
||||
def test_users_groups(self, regex, getent_args, class_client):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
iFrom 4d6576560de01ab4f4a75924a5b7b81fd9e5bd2a Mon Sep 17 00:00:00 2001
|
||||
From: Brett Holman <bholman.devel@gmail.com>
|
||||
Date: Thu, 28 Apr 2022 08:41:28 -0500
|
||||
Subject: [PATCH] Don't fail if IB and Ethernet devices 'collide' (#1411)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/4d6576560de01ab4f4a75924a5b7b81fd9e5bd2a
|
||||
Conflict:format diff.
|
||||
|
||||
Current behavior for the collision of Ethernet mac
|
||||
address and the "openstack 6 byte IB format" is to fail[1].
|
||||
This isn't a problem for the respective Ethernet and IB
|
||||
networks, so don't force cloud-init to fail.
|
||||
|
||||
[1] https://bugs.launchpad.net/cloud-init/+bug/1871326
|
||||
---
|
||||
cloudinit/net/__init__.py | 40 ++++++++++++++++++++++++++++++++-------
|
||||
1 file changed, 33 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
||||
index b007c9a..fba133e 100644
|
||||
--- a/cloudinit/net/__init__.py
|
||||
+++ b/cloudinit/net/__init__.py
|
||||
@@ -903,15 +903,41 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict:
|
||||
"duplicate mac found! both '%s' and '%s' have mac '%s'" %
|
||||
(name, ret[mac], mac))
|
||||
ret[mac] = name
|
||||
- # Try to get an Infiniband hardware address (in 6 byte Ethernet format)
|
||||
- # for the interface.
|
||||
+
|
||||
+ # Pretend that an Infiniband GUID is an ethernet address for Openstack
|
||||
+ # configuration purposes
|
||||
+ # TODO: move this format to openstack
|
||||
ib_mac = get_ib_interface_hwaddr(name, True)
|
||||
if ib_mac:
|
||||
- if ib_mac in ret:
|
||||
- raise RuntimeError(
|
||||
- "duplicate mac found! both '%s' and '%s' have mac '%s'" %
|
||||
- (name, ret[ib_mac], ib_mac))
|
||||
- ret[ib_mac] = name
|
||||
+ # If an Ethernet mac address happens to collide with a few bits in
|
||||
+ # an IB GUID, prefer the ethernet address.
|
||||
+ #
|
||||
+ # Log a message in case a user is troubleshooting openstack, but
|
||||
+ # don't fall over, since this really isn't _a_ problem, and
|
||||
+ # openstack makes weird assumptions that cause it to fail it's
|
||||
+ # really not _our_ problem.
|
||||
+ #
|
||||
+ # These few bits selected in get_ib_interface_hwaddr() are not
|
||||
+ # guaranteed to be globally unique in InfiniBand, and really make
|
||||
+ # no sense to compare them to Ethernet mac addresses. This appears
|
||||
+ # to be a # workaround for openstack-specific behavior[1], and for
|
||||
+ # now leave it to avoid breaking openstack
|
||||
+ # but this should be removed from get_interfaces_by_mac_on_linux()
|
||||
+ # because IB GUIDs are not mac addresses, and operate on a separate
|
||||
+ # L2 protocol so address collision doesn't matter.
|
||||
+ #
|
||||
+ # [1] sources/helpers/openstack.py:convert_net_json() expects
|
||||
+ # net.get_interfaces_by_mac() to return IB addresses in this format
|
||||
+ if ib_mac not in ret:
|
||||
+ ret[ib_mac] = name
|
||||
+ else:
|
||||
+ LOG.warning(
|
||||
+ "Ethernet and InfiniBand interfaces have the same address"
|
||||
+ " both '%s' and '%s' have address '%s'.",
|
||||
+ name,
|
||||
+ ret[ib_mac],
|
||||
+ ib_mac,
|
||||
+ )
|
||||
return ret
|
||||
|
||||
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
572
backport-Drop-support-of-sk-keys-in-cc_ssh-1451.patch
Normal file
572
backport-Drop-support-of-sk-keys-in-cc_ssh-1451.patch
Normal file
@ -0,0 +1,572 @@
|
||||
From 2db1c58512760fcb5a850df852e44833c97ed856 Mon Sep 17 00:00:00 2001
|
||||
From: Alberto Contreras <alberto.contreras@canonical.com>
|
||||
Date: Fri, 13 May 2022 21:48:28 +0200
|
||||
Subject: [PATCH] Drop support of *-sk keys in cc_ssh (#1451)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/2db1c58512760fcb5a850df852e44833c97ed856
|
||||
Conflict:(1)only change tests/unittests/config/test_cc_ssh.py
|
||||
(2)don't add test_handle_invalid_ssh_keys_are_skipped
|
||||
(3)don't change TestSshSchema
|
||||
(4)format diffs.
|
||||
|
||||
- Delete *-sk keys from cloud-init-schema.json under
|
||||
cc_ssh.{ssh_keys,ssh_genkeytypes}
|
||||
- Log a warning if some given key is unsupported or unknown.
|
||||
- Port tests to Pytests, add some types and increase unittest
|
||||
coverage.
|
||||
---
|
||||
cloudinit/config/tests/test_ssh.py | 457 ++++++++++++-----------------
|
||||
1 file changed, 192 insertions(+), 265 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
|
||||
index 87ccdb6..714949c 100644
|
||||
--- a/cloudinit/config/tests/test_ssh.py
|
||||
+++ b/cloudinit/config/tests/test_ssh.py
|
||||
@@ -1,11 +1,14 @@
|
||||
# This file is part of cloud-init. See LICENSE file for license information.
|
||||
|
||||
import os.path
|
||||
+from typing import Optional
|
||||
+from unittest import mock
|
||||
|
||||
from cloudinit.config import cc_ssh
|
||||
from cloudinit import ssh_util
|
||||
-from cloudinit.tests.helpers import CiTestCase, mock
|
||||
+from tests.unittests.util import get_cloud
|
||||
import logging
|
||||
+import pytest
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,68 +17,79 @@ KEY_NAMES_NO_DSA = [name for name in cc_ssh.GENERATE_KEY_NAMES
|
||||
if name not in 'dsa']
|
||||
|
||||
|
||||
+@pytest.fixture(scope="function")
|
||||
+def publish_hostkey_test_setup(tmpdir):
|
||||
+ test_hostkeys = {
|
||||
+ "dsa": ("ssh-dss", "AAAAB3NzaC1kc3MAAACB"),
|
||||
+ "ecdsa": ("ecdsa-sha2-nistp256", "AAAAE2VjZ"),
|
||||
+ "ed25519": ("ssh-ed25519", "AAAAC3NzaC1lZDI"),
|
||||
+ "rsa": ("ssh-rsa", "AAAAB3NzaC1yc2EAAA"),
|
||||
+ }
|
||||
+ test_hostkey_files = []
|
||||
+ hostkey_tmpdir = tmpdir
|
||||
+ for key_type in cc_ssh.GENERATE_KEY_NAMES:
|
||||
+ filename = "ssh_host_%s_key.pub" % key_type
|
||||
+ filepath = os.path.join(hostkey_tmpdir, filename)
|
||||
+ test_hostkey_files.append(filepath)
|
||||
+ with open(filepath, "w") as f:
|
||||
+ f.write(" ".join(test_hostkeys[key_type]))
|
||||
+
|
||||
+ cc_ssh.KEY_FILE_TPL = os.path.join(hostkey_tmpdir, "ssh_host_%s_key")
|
||||
+ yield test_hostkeys, test_hostkey_files
|
||||
+
|
||||
+
|
||||
+def _replace_options(user: Optional[str] = None) -> str:
|
||||
+ options = ssh_util.DISABLE_USER_OPTS
|
||||
+ if user:
|
||||
+ new_user = user
|
||||
+ else:
|
||||
+ new_user = "NONE"
|
||||
+ options = options.replace("$USER", new_user)
|
||||
+ options = options.replace("$DISABLE_USER", "root")
|
||||
+ return options
|
||||
+
|
||||
@mock.patch(MODPATH + "ssh_util.setup_user_keys")
|
||||
-class TestHandleSsh(CiTestCase):
|
||||
+class TestHandleSsh:
|
||||
"""Test cc_ssh handling of ssh config."""
|
||||
|
||||
- def _publish_hostkey_test_setup(self):
|
||||
- self.test_hostkeys = {
|
||||
- 'dsa': ('ssh-dss', 'AAAAB3NzaC1kc3MAAACB'),
|
||||
- 'ecdsa': ('ecdsa-sha2-nistp256', 'AAAAE2VjZ'),
|
||||
- 'ed25519': ('ssh-ed25519', 'AAAAC3NzaC1lZDI'),
|
||||
- 'rsa': ('ssh-rsa', 'AAAAB3NzaC1yc2EAAA'),
|
||||
- }
|
||||
- self.test_hostkey_files = []
|
||||
- hostkey_tmpdir = self.tmp_dir()
|
||||
- for key_type in cc_ssh.GENERATE_KEY_NAMES:
|
||||
- key_data = self.test_hostkeys[key_type]
|
||||
- filename = 'ssh_host_%s_key.pub' % key_type
|
||||
- filepath = os.path.join(hostkey_tmpdir, filename)
|
||||
- self.test_hostkey_files.append(filepath)
|
||||
- with open(filepath, 'w') as f:
|
||||
- f.write(' '.join(key_data))
|
||||
-
|
||||
- cc_ssh.KEY_FILE_TPL = os.path.join(hostkey_tmpdir, 'ssh_host_%s_key')
|
||||
-
|
||||
- def test_apply_credentials_with_user(self, m_setup_keys):
|
||||
- """Apply keys for the given user and root."""
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- cc_ssh.apply_credentials(keys, user, False, ssh_util.DISABLE_USER_OPTS)
|
||||
- self.assertEqual([mock.call(set(keys), user),
|
||||
- mock.call(set(keys), "root", options="")],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- def test_apply_credentials_with_no_user(self, m_setup_keys):
|
||||
- """Apply keys for root only."""
|
||||
- keys = ["key1"]
|
||||
- user = None
|
||||
- cc_ssh.apply_credentials(keys, user, False, ssh_util.DISABLE_USER_OPTS)
|
||||
- self.assertEqual([mock.call(set(keys), "root", options="")],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- def test_apply_credentials_with_user_disable_root(self, m_setup_keys):
|
||||
- """Apply keys for the given user and disable root ssh."""
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- options = ssh_util.DISABLE_USER_OPTS
|
||||
- cc_ssh.apply_credentials(keys, user, True, options)
|
||||
- options = options.replace("$USER", user)
|
||||
- options = options.replace("$DISABLE_USER", "root")
|
||||
- self.assertEqual([mock.call(set(keys), user),
|
||||
- mock.call(set(keys), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- def test_apply_credentials_with_no_user_disable_root(self, m_setup_keys):
|
||||
- """Apply keys no user and disable root ssh."""
|
||||
- keys = ["key1"]
|
||||
- user = None
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "keys,user,disable_root_opts",
|
||||
+ [
|
||||
+ # For the given user and root.
|
||||
+ pytest.param(["key1"], "clouduser", False, id="with_user"),
|
||||
+ # For root only.
|
||||
+ pytest.param(["key1"], None, False, id="with_no_user"),
|
||||
+ # For the given user and disable root ssh.
|
||||
+ pytest.param(
|
||||
+ ["key1"],
|
||||
+ "clouduser",
|
||||
+ True,
|
||||
+ id="with_user_disable_root",
|
||||
+ ),
|
||||
+ # No user and disable root ssh.
|
||||
+ pytest.param(
|
||||
+ ["key1"],
|
||||
+ None,
|
||||
+ True,
|
||||
+ id="with_no_user_disable_root",
|
||||
+ ),
|
||||
+ ],
|
||||
+ )
|
||||
+ def test_apply_credentials(
|
||||
+ self, m_setup_keys, keys, user, disable_root_opts
|
||||
+ ):
|
||||
options = ssh_util.DISABLE_USER_OPTS
|
||||
- cc_ssh.apply_credentials(keys, user, True, options)
|
||||
- options = options.replace("$USER", "NONE")
|
||||
- options = options.replace("$DISABLE_USER", "root")
|
||||
- self.assertEqual([mock.call(set(keys), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
+ cc_ssh.apply_credentials(keys, user, disable_root_opts, options)
|
||||
+ if not disable_root_opts:
|
||||
+ expected_options = ""
|
||||
+ else:
|
||||
+ expected_options = _replace_options(user)
|
||||
+ expected_calls = [
|
||||
+ mock.call(set(keys), "root", options=expected_options)
|
||||
+ ]
|
||||
+ if user:
|
||||
+ expected_calls = [mock.call(set(keys), user)] + expected_calls
|
||||
+ assert expected_calls == m_setup_keys.call_args_list
|
||||
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@@ -90,20 +104,20 @@ class TestHandleSsh(CiTestCase):
|
||||
m_path_exists.return_value = True
|
||||
m_nug.return_value = ([], {})
|
||||
cc_ssh.PUBLISH_HOST_KEYS = False
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
+ cloud = get_cloud(distro="ubuntu", metadata={"public-keys": keys})
|
||||
cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
options = ssh_util.DISABLE_USER_OPTS.replace("$USER", "NONE")
|
||||
options = options.replace("$DISABLE_USER", "root")
|
||||
m_glob.assert_called_once_with('/etc/ssh/ssh_host_*key*')
|
||||
- self.assertIn(
|
||||
- [mock.call('/etc/ssh/ssh_host_rsa_key'),
|
||||
- mock.call('/etc/ssh/ssh_host_dsa_key'),
|
||||
- mock.call('/etc/ssh/ssh_host_ecdsa_key'),
|
||||
- mock.call('/etc/ssh/ssh_host_ed25519_key')],
|
||||
- m_path_exists.call_args_list)
|
||||
- self.assertEqual([mock.call(set(keys), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
+ assert [
|
||||
+ mock.call("/etc/ssh/ssh_host_rsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_dsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_ecdsa_key"),
|
||||
+ mock.call("/etc/ssh/ssh_host_ed25519_key"),
|
||||
+ ] in m_path_exists.call_args_list
|
||||
+ assert [
|
||||
+ mock.call(set(keys), "root", options=options)
|
||||
+ ] == m_setup_keys.call_args_list
|
||||
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@@ -120,231 +134,144 @@ class TestHandleSsh(CiTestCase):
|
||||
# Mock os.path.exits to True to short-circuit the key writing logic
|
||||
m_path_exists.return_value = True
|
||||
m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
-
|
||||
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
|
||||
- options = options.replace("$DISABLE_USER", "root")
|
||||
- self.assertEqual([mock.call(set(), user),
|
||||
- mock.call(set(), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_no_cfg_and_default_root(self, m_path_exists, m_nug,
|
||||
- m_glob, m_setup_keys):
|
||||
- """Test handle with no config and a default distro user."""
|
||||
- cfg = {}
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- m_glob.return_value = [] # Return no matching keys to prevent removal
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
+ cloud = get_cloud(distro="ubuntu", metadata={"public-keys": keys})
|
||||
cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
|
||||
options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
|
||||
options = options.replace("$DISABLE_USER", "root")
|
||||
- self.assertEqual([mock.call(set(keys), user),
|
||||
- mock.call(set(keys), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_cfg_with_explicit_disable_root(self, m_path_exists, m_nug,
|
||||
- m_glob, m_setup_keys):
|
||||
- """Test handle with explicit disable_root and a default distro user."""
|
||||
- # This test is identical to test_handle_no_cfg_and_default_root,
|
||||
- # except this uses an explicit cfg value
|
||||
- cfg = {"disable_root": True}
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- m_glob.return_value = [] # Return no matching keys to prevent removal
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
-
|
||||
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
|
||||
- options = options.replace("$DISABLE_USER", "root")
|
||||
- self.assertEqual([mock.call(set(keys), user),
|
||||
- mock.call(set(keys), "root", options=options)],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
+ assert [
|
||||
+ mock.call(set(), user),
|
||||
+ mock.call(set(), "root", options=options),
|
||||
+ ] == m_setup_keys.call_args_list
|
||||
+
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "cfg,mock_get_public_ssh_keys,empty_opts",
|
||||
+ [
|
||||
+ pytest.param({}, False, False, id="no_cfg"),
|
||||
+ pytest.param(
|
||||
+ {"disable_root": True},
|
||||
+ False,
|
||||
+ False,
|
||||
+ id="explicit_disable_root",
|
||||
+ ),
|
||||
+ # When disable_root == False, the ssh redirect for root is skipped
|
||||
+ pytest.param(
|
||||
+ {"disable_root": False},
|
||||
+ True,
|
||||
+ True,
|
||||
+ id="cfg_without_disable_root",
|
||||
+ ),
|
||||
+ ],
|
||||
+ )
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_cfg_without_disable_root(self, m_path_exists, m_nug,
|
||||
- m_glob, m_setup_keys):
|
||||
- """Test handle with disable_root == False."""
|
||||
- # When disable_root == False, the ssh redirect for root is skipped
|
||||
- cfg = {"disable_root": False}
|
||||
+ def test_handle_default_root(
|
||||
+ self,
|
||||
+ m_path_exists,
|
||||
+ m_nug,
|
||||
+ m_glob,
|
||||
+ m_setup_keys,
|
||||
+ cfg,
|
||||
+ mock_get_public_ssh_keys,
|
||||
+ empty_opts,
|
||||
+ ):
|
||||
+ """Test handle with a default distro user."""
|
||||
keys = ["key1"]
|
||||
user = "clouduser"
|
||||
m_glob.return_value = [] # Return no matching keys to prevent removal
|
||||
# Mock os.path.exits to True to short-circuit the key writing logic
|
||||
m_path_exists.return_value = True
|
||||
m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
+ cloud = get_cloud(distro="ubuntu", metadata={"public-keys": keys})
|
||||
+ if mock_get_public_ssh_keys:
|
||||
+ cloud.get_public_ssh_keys = mock.Mock(return_value=keys)
|
||||
cloud.get_public_ssh_keys = mock.Mock(return_value=keys)
|
||||
cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
|
||||
- self.assertEqual([mock.call(set(keys), user),
|
||||
- mock.call(set(keys), "root", options="")],
|
||||
- m_setup_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_publish_hostkeys_default(
|
||||
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
||||
- """Test handle with various configs for ssh_publish_hostkeys."""
|
||||
- self._publish_hostkey_test_setup()
|
||||
- cc_ssh.PUBLISH_HOST_KEYS = True
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- # Return no matching keys for first glob, test keys for second.
|
||||
- m_glob.side_effect = iter([
|
||||
- [],
|
||||
- self.test_hostkey_files,
|
||||
- ])
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cloud.datasource.publish_host_keys = mock.Mock()
|
||||
-
|
||||
- cfg = {}
|
||||
- expected_call = [self.test_hostkeys[key_type] for key_type
|
||||
- in KEY_NAMES_NO_DSA]
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
- self.assertEqual([mock.call(expected_call)],
|
||||
- cloud.datasource.publish_host_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_publish_hostkeys_config_enable(
|
||||
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
||||
- """Test handle with various configs for ssh_publish_hostkeys."""
|
||||
- self._publish_hostkey_test_setup()
|
||||
- cc_ssh.PUBLISH_HOST_KEYS = False
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- # Return no matching keys for first glob, test keys for second.
|
||||
- m_glob.side_effect = iter([
|
||||
- [],
|
||||
- self.test_hostkey_files,
|
||||
- ])
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cloud.datasource.publish_host_keys = mock.Mock()
|
||||
-
|
||||
- cfg = {'ssh_publish_hostkeys': {'enabled': True}}
|
||||
- expected_call = [self.test_hostkeys[key_type] for key_type
|
||||
- in KEY_NAMES_NO_DSA]
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
- self.assertEqual([mock.call(expected_call)],
|
||||
- cloud.datasource.publish_host_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_publish_hostkeys_config_disable(
|
||||
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
||||
- """Test handle with various configs for ssh_publish_hostkeys."""
|
||||
- self._publish_hostkey_test_setup()
|
||||
- cc_ssh.PUBLISH_HOST_KEYS = True
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- # Return no matching keys for first glob, test keys for second.
|
||||
- m_glob.side_effect = iter([
|
||||
- [],
|
||||
- self.test_hostkey_files,
|
||||
- ])
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cloud.datasource.publish_host_keys = mock.Mock()
|
||||
-
|
||||
- cfg = {'ssh_publish_hostkeys': {'enabled': False}}
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
- self.assertFalse(cloud.datasource.publish_host_keys.call_args_list)
|
||||
- cloud.datasource.publish_host_keys.assert_not_called()
|
||||
-
|
||||
+ if empty_opts:
|
||||
+ options = ""
|
||||
+ else:
|
||||
+ options = _replace_options(user)
|
||||
+ assert [
|
||||
+ mock.call(set(keys), user),
|
||||
+ mock.call(set(keys), "root", options=options),
|
||||
+ ] == m_setup_keys.call_args_list
|
||||
+
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "cfg, expected_key_types",
|
||||
+ [
|
||||
+ pytest.param({}, KEY_NAMES_NO_DSA, id="default"),
|
||||
+ pytest.param(
|
||||
+ {"ssh_publish_hostkeys": {"enabled": True}},
|
||||
+ KEY_NAMES_NO_DSA,
|
||||
+ id="config_enable",
|
||||
+ ),
|
||||
+ pytest.param(
|
||||
+ {"ssh_publish_hostkeys": {"enabled": False}},
|
||||
+ None,
|
||||
+ id="config_disable",
|
||||
+ ),
|
||||
+ pytest.param(
|
||||
+ {
|
||||
+ "ssh_publish_hostkeys": {
|
||||
+ "enabled": True,
|
||||
+ "blacklist": ["dsa", "rsa"],
|
||||
+ }
|
||||
+ },
|
||||
+ ["ecdsa", "ed25519"],
|
||||
+ id="config_blacklist",
|
||||
+ ),
|
||||
+ pytest.param(
|
||||
+ {"ssh_publish_hostkeys": {"enabled": True, "blacklist": []}},
|
||||
+ cc_ssh.GENERATE_KEY_NAMES,
|
||||
+ id="empty_blacklist",
|
||||
+ ),
|
||||
+ ],
|
||||
+ )
|
||||
@mock.patch(MODPATH + "glob.glob")
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_publish_hostkeys_config_blacklist(
|
||||
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
||||
+ def test_handle_publish_hostkeys(
|
||||
+ self,
|
||||
+ m_path_exists,
|
||||
+ m_nug,
|
||||
+ m_glob,
|
||||
+ m_setup_keys,
|
||||
+ publish_hostkey_test_setup,
|
||||
+ cfg,
|
||||
+ expected_key_types,
|
||||
+ ):
|
||||
"""Test handle with various configs for ssh_publish_hostkeys."""
|
||||
- self._publish_hostkey_test_setup()
|
||||
+ test_hostkeys, test_hostkey_files = publish_hostkey_test_setup
|
||||
cc_ssh.PUBLISH_HOST_KEYS = True
|
||||
keys = ["key1"]
|
||||
user = "clouduser"
|
||||
# Return no matching keys for first glob, test keys for second.
|
||||
m_glob.side_effect = iter([
|
||||
[],
|
||||
- self.test_hostkey_files,
|
||||
+ test_hostkey_files,
|
||||
])
|
||||
# Mock os.path.exits to True to short-circuit the key writing logic
|
||||
m_path_exists.return_value = True
|
||||
m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
+ cloud = get_cloud(distro='ubuntu', metadata={'public-keys': keys})
|
||||
cloud.datasource.publish_host_keys = mock.Mock()
|
||||
|
||||
- cfg = {'ssh_publish_hostkeys': {'enabled': True,
|
||||
- 'blacklist': ['dsa', 'rsa']}}
|
||||
- expected_call = [self.test_hostkeys[key_type] for key_type
|
||||
- in ['ecdsa', 'ed25519']]
|
||||
- cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
- self.assertEqual([mock.call(expected_call)],
|
||||
- cloud.datasource.publish_host_keys.call_args_list)
|
||||
-
|
||||
- @mock.patch(MODPATH + "glob.glob")
|
||||
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
- @mock.patch(MODPATH + "os.path.exists")
|
||||
- def test_handle_publish_hostkeys_empty_blacklist(
|
||||
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
||||
- """Test handle with various configs for ssh_publish_hostkeys."""
|
||||
- self._publish_hostkey_test_setup()
|
||||
- cc_ssh.PUBLISH_HOST_KEYS = True
|
||||
- keys = ["key1"]
|
||||
- user = "clouduser"
|
||||
- # Return no matching keys for first glob, test keys for second.
|
||||
- m_glob.side_effect = iter([
|
||||
- [],
|
||||
- self.test_hostkey_files,
|
||||
- ])
|
||||
- # Mock os.path.exits to True to short-circuit the key writing logic
|
||||
- m_path_exists.return_value = True
|
||||
- m_nug.return_value = ({user: {"default": user}}, {})
|
||||
- cloud = self.tmp_cloud(
|
||||
- distro='ubuntu', metadata={'public-keys': keys})
|
||||
- cloud.datasource.publish_host_keys = mock.Mock()
|
||||
-
|
||||
- cfg = {'ssh_publish_hostkeys': {'enabled': True,
|
||||
- 'blacklist': []}}
|
||||
- expected_call = [self.test_hostkeys[key_type] for key_type
|
||||
- in cc_ssh.GENERATE_KEY_NAMES]
|
||||
+ expected_calls = []
|
||||
+ if expected_key_types is not None:
|
||||
+ expected_calls = [
|
||||
+ mock.call(
|
||||
+ [
|
||||
+ test_hostkeys[key_type]
|
||||
+ for key_type in expected_key_types
|
||||
+ ]
|
||||
+ )
|
||||
+ ]
|
||||
cc_ssh.handle("name", cfg, cloud, LOG, None)
|
||||
- self.assertEqual([mock.call(expected_call)],
|
||||
- cloud.datasource.publish_host_keys.call_args_list)
|
||||
+ assert (
|
||||
+ expected_calls == cloud.datasource.publish_host_keys.call_args_list
|
||||
+ )
|
||||
|
||||
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
||||
@mock.patch(MODPATH + "util.write_file")
|
||||
@@ -360,7 +287,7 @@ class TestHandleSsh(CiTestCase):
|
||||
public_name = "{}_public".format(key_type)
|
||||
cert_name = "{}_certificate".format(key_type)
|
||||
|
||||
- # Actual key contents don"t have to be realistic
|
||||
+ # Actual key contents don't have to be realistic
|
||||
private_value = "{}_PRIVATE_KEY".format(key_type)
|
||||
public_value = "{}_PUBLIC_KEY".format(key_type)
|
||||
cert_value = "{}_CERT_KEY".format(key_type)
|
||||
@@ -397,9 +324,9 @@ class TestHandleSsh(CiTestCase):
|
||||
m_nug.return_value = ([], {})
|
||||
with mock.patch(MODPATH + 'ssh_util.parse_ssh_config',
|
||||
return_value=[]):
|
||||
- cc_ssh.handle("name", cfg, self.tmp_cloud(distro='ubuntu'),
|
||||
- LOG, None)
|
||||
+ cc_ssh.handle("name", cfg, get_cloud(distro="ubuntu"), LOG, None)
|
||||
|
||||
# Check that all expected output has been done.
|
||||
for call_ in expected_calls:
|
||||
- self.assertIn(call_, m_write_file.call_args_list)
|
||||
+ assert call_ in m_write_file.call_args_list
|
||||
+
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
From 6e725f36647407d201af0603d7db11fc96a93d4d Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Tue, 13 Dec 2022 10:55:23 -0600
|
||||
Subject: [PATCH] Ensure network ready before cloud-init service runs on
|
||||
RHEL
|
||||
(#1893)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/6e725f36647407d201af0603d7db11fc96a93d4d
|
||||
Conflict:NA
|
||||
|
||||
LP: #1998655
|
||||
---
|
||||
systemd/cloud-init.service.tmpl | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl
|
||||
index 25830e0..d62b46f 100644
|
||||
--- a/systemd/cloud-init.service.tmpl
|
||||
+++ b/systemd/cloud-init.service.tmpl
|
||||
@@ -16,6 +16,7 @@ After=networking.service
|
||||
"openeuler", "rhel", "rocky", "virtuozzo"] %}
|
||||
After=network.service
|
||||
After=NetworkManager.service
|
||||
+After=NetworkManager-wait-online.service
|
||||
{% endif %}
|
||||
{% if variant in ["suse"] %}
|
||||
After=wicked.service
|
||||
--
|
||||
2.27.0
|
||||
|
||||
549
backport-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
Normal file
549
backport-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
Normal file
@ -0,0 +1,549 @@
|
||||
From b97a30f0a05c1dea918c46ca9c05c869d15fe2d5 Mon Sep 17 00:00:00 2001
|
||||
From: Harald <hjensas@redhat.com>
|
||||
Date: Tue, 8 Feb 2022 15:49:00 +0100
|
||||
Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/b97a30f0a05c1dea918c46ca9c05c869d15fe2d5
|
||||
Conflict:format diffs.
|
||||
|
||||
This change converts the IPv6 netmask from the network_data.json[1]
|
||||
format to the CIDR style, <IPv6_addr>/<prefix>.
|
||||
|
||||
Using an IPv6 address like ffff:ffff:ffff:ffff:: does not work with
|
||||
NetworkManager, nor networkscripts.
|
||||
|
||||
NetworkManager will ignore the route, logging:
|
||||
ifcfg-rh: ignoring invalid route at \
|
||||
"::/:: via fd00:fd00:fd00:2::fffe dev " \
|
||||
(/etc/sysconfig/network-scripts/route6-:3): \
|
||||
Argument for "::/::" is not ADDR/PREFIX format
|
||||
|
||||
Similarly if using networkscripts, ip route fail with error:
|
||||
Error: inet6 prefix is expected rather than \
|
||||
"fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".
|
||||
|
||||
Also a bit of refactoring ...
|
||||
|
||||
cloudinit.net.sysconfig.Route.to_string:
|
||||
* Move a couple of lines around to reduce repeated code.
|
||||
* if "ADDRESS" not in key -> continute, so that the
|
||||
code block following it can be de-indented.
|
||||
cloudinit.net.network_state:
|
||||
* Refactors the ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix
|
||||
removes mask_to_net_prefix methods. Utilize ipaddress library to
|
||||
do some of the heavy lifting.
|
||||
|
||||
LP: #1959148
|
||||
---
|
||||
cloudinit/net/__init__.py | 7 +-
|
||||
cloudinit/net/network_state.py | 101 +++++++-----------
|
||||
cloudinit/net/sysconfig.py | 91 +++++++++-------
|
||||
cloudinit/sources/DataSourceOpenNebula.py | 2 +-
|
||||
.../sources/helpers/vmware/imc/config_nic.py | 4 +-
|
||||
cloudinit/net/tests/test_init.py | 4 +-
|
||||
cloudinit/net/tests/test_network_state.py | 58 +++++++++-
|
||||
tests/unittests/test_net.py | 78 +++++++++++++-
|
||||
8 files changed, 233 insertions(+), 112 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
||||
index a503210..d3ac4c8 100644
|
||||
--- a/cloudinit/net/__init__.py
|
||||
+++ b/cloudinit/net/__init__.py
|
||||
@@ -15,7 +15,7 @@ from typing import Any, Dict
|
||||
|
||||
from cloudinit import subp
|
||||
from cloudinit import util
|
||||
-from cloudinit.net.network_state import mask_to_net_prefix
|
||||
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
|
||||
from cloudinit.url_helper import UrlError, readurl
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -1140,10 +1140,11 @@ class EphemeralIPv4Network(object):
|
||||
'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
|
||||
interface, ip, prefix_or_mask, broadcast))
|
||||
try:
|
||||
- self.prefix = mask_to_net_prefix(prefix_or_mask)
|
||||
+ self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
- 'Cannot setup network: {0}'.format(e)
|
||||
+ "Cannot setup network, invalid prefix or "
|
||||
+ "netmask: {0}".format(e)
|
||||
) from e
|
||||
|
||||
self.connectivity_url_data = connectivity_url_data
|
||||
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
|
||||
index ac44304..0a4e0c6 100644
|
||||
--- a/cloudinit/net/network_state.py
|
||||
+++ b/cloudinit/net/network_state.py
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import copy
|
||||
import functools
|
||||
+import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
@@ -878,10 +879,16 @@ def _normalize_net_keys(network, address_keys=()):
|
||||
try:
|
||||
prefix = int(maybe_prefix)
|
||||
except ValueError:
|
||||
- # this supports input of <address>/255.255.255.0
|
||||
- prefix = mask_to_net_prefix(maybe_prefix)
|
||||
- elif netmask:
|
||||
- prefix = mask_to_net_prefix(netmask)
|
||||
+ if ipv6:
|
||||
+ # this supports input of ffff:ffff:ffff::
|
||||
+ prefix = ipv6_mask_to_net_prefix(maybe_prefix)
|
||||
+ else:
|
||||
+ # this supports input of 255.255.255.0
|
||||
+ prefix = ipv4_mask_to_net_prefix(maybe_prefix)
|
||||
+ elif netmask and not ipv6:
|
||||
+ prefix = ipv4_mask_to_net_prefix(netmask)
|
||||
+ elif netmask and ipv6:
|
||||
+ prefix = ipv6_mask_to_net_prefix(netmask)
|
||||
elif 'prefix' in net:
|
||||
prefix = int(net['prefix'])
|
||||
else:
|
||||
@@ -978,73 +985,41 @@ def ipv4_mask_to_net_prefix(mask):
|
||||
str(24) => 24
|
||||
"24" => 24
|
||||
"""
|
||||
- if isinstance(mask, int):
|
||||
- return mask
|
||||
- if isinstance(mask, str):
|
||||
- try:
|
||||
- return int(mask)
|
||||
- except ValueError:
|
||||
- pass
|
||||
- else:
|
||||
- raise TypeError("mask '%s' is not a string or int")
|
||||
-
|
||||
- if '.' not in mask:
|
||||
- raise ValueError("netmask '%s' does not contain a '.'" % mask)
|
||||
-
|
||||
- toks = mask.split(".")
|
||||
- if len(toks) != 4:
|
||||
- raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks)))
|
||||
-
|
||||
- return sum([bin(int(x)).count('1') for x in toks])
|
||||
-
|
||||
+ return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen
|
||||
|
||||
def ipv6_mask_to_net_prefix(mask):
|
||||
"""Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix.
|
||||
|
||||
- If 'mask' is an integer or string representation of one then
|
||||
- int(mask) will be returned.
|
||||
+ If the input is already an integer or a string representation of
|
||||
+ an integer, then int(mask) will be returned.
|
||||
+ "ffff:ffff:ffff::" => 48
|
||||
+ "48" => 48
|
||||
"""
|
||||
-
|
||||
- if isinstance(mask, int):
|
||||
- return mask
|
||||
- if isinstance(mask, str):
|
||||
- try:
|
||||
- return int(mask)
|
||||
- except ValueError:
|
||||
- pass
|
||||
- else:
|
||||
- raise TypeError("mask '%s' is not a string or int")
|
||||
-
|
||||
- if ':' not in mask:
|
||||
- raise ValueError("mask '%s' does not have a ':'")
|
||||
-
|
||||
- bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00,
|
||||
- 0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc,
|
||||
- 0xfffe, 0xffff]
|
||||
- prefix = 0
|
||||
- for word in mask.split(':'):
|
||||
- if not word or int(word, 16) == 0:
|
||||
- break
|
||||
- prefix += bitCount.index(int(word, 16))
|
||||
-
|
||||
- return prefix
|
||||
-
|
||||
-
|
||||
-def mask_to_net_prefix(mask):
|
||||
- """Return the network prefix for the netmask provided.
|
||||
-
|
||||
- Supports ipv4 or ipv6 netmasks."""
|
||||
try:
|
||||
- # if 'mask' is a prefix that is an integer.
|
||||
- # then just return it.
|
||||
- return int(mask)
|
||||
+ # In the case the mask is already a prefix
|
||||
+ prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen
|
||||
+ return prefixlen
|
||||
except ValueError:
|
||||
+ # ValueError means mask is an IPv6 address representation and need
|
||||
+ # conversion.
|
||||
pass
|
||||
- if is_ipv6_addr(mask):
|
||||
- return ipv6_mask_to_net_prefix(mask)
|
||||
- else:
|
||||
- return ipv4_mask_to_net_prefix(mask)
|
||||
-
|
||||
+
|
||||
+ netmask = ipaddress.ip_address(mask)
|
||||
+ mask_int = int(netmask)
|
||||
+ # If the mask is all zeroes, just return it
|
||||
+ if mask_int == 0:
|
||||
+ return mask_int
|
||||
+
|
||||
+ trailing_zeroes = min(
|
||||
+ ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length()
|
||||
+ )
|
||||
+ leading_ones = mask_int >> trailing_zeroes
|
||||
+ prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes
|
||||
+ all_ones = (1 << prefixlen) - 1
|
||||
+ if leading_ones != all_ones:
|
||||
+ raise ValueError("Invalid network mask '%s'" % mask)
|
||||
+
|
||||
+ return prefixlen
|
||||
|
||||
def mask_and_ipv4_to_bcast_addr(mask, ip):
|
||||
"""Calculate the broadcast address from the subnet mask and ip addr.
|
||||
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
|
||||
index 32a2d5a..4c6caef 100644
|
||||
--- a/cloudinit/net/sysconfig.py
|
||||
+++ b/cloudinit/net/sysconfig.py
|
||||
@@ -12,6 +12,7 @@ from cloudinit import util
|
||||
from cloudinit import subp
|
||||
from cloudinit.distros.parsers import networkmanager_conf
|
||||
from cloudinit.distros.parsers import resolv_conf
|
||||
+from cloudinit.net import network_state
|
||||
|
||||
from . import renderer
|
||||
from .network_state import (
|
||||
@@ -172,44 +173,60 @@ class Route(ConfigMap):
|
||||
# (because Route can contain a mix of IPv4 and IPv6)
|
||||
reindex = -1
|
||||
for key in sorted(self._conf.keys()):
|
||||
- if 'ADDRESS' in key:
|
||||
- index = key.replace('ADDRESS', '')
|
||||
- address_value = str(self._conf[key])
|
||||
- # only accept combinations:
|
||||
- # if proto ipv6 only display ipv6 routes
|
||||
- # if proto ipv4 only display ipv4 routes
|
||||
- # do not add ipv6 routes if proto is ipv4
|
||||
- # do not add ipv4 routes if proto is ipv6
|
||||
- # (this array will contain a mix of ipv4 and ipv6)
|
||||
- if proto == "ipv4" and not self.is_ipv6_route(address_value):
|
||||
- netmask_value = str(self._conf['NETMASK' + index])
|
||||
- gateway_value = str(self._conf['GATEWAY' + index])
|
||||
- # increase IPv4 index
|
||||
- reindex = reindex + 1
|
||||
- buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
|
||||
- _quote_value(address_value)))
|
||||
- buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
|
||||
- _quote_value(gateway_value)))
|
||||
- buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
|
||||
- _quote_value(netmask_value)))
|
||||
- metric_key = 'METRIC' + index
|
||||
- if metric_key in self._conf:
|
||||
- metric_value = str(self._conf['METRIC' + index])
|
||||
- buf.write("%s=%s\n" % ('METRIC' + str(reindex),
|
||||
- _quote_value(metric_value)))
|
||||
- elif proto == "ipv6" and self.is_ipv6_route(address_value):
|
||||
- netmask_value = str(self._conf['NETMASK' + index])
|
||||
- gateway_value = str(self._conf['GATEWAY' + index])
|
||||
- metric_value = (
|
||||
- 'metric ' + str(self._conf['METRIC' + index])
|
||||
- if 'METRIC' + index in self._conf else '')
|
||||
- buf.write(
|
||||
- "%s/%s via %s %s dev %s\n" % (address_value,
|
||||
- netmask_value,
|
||||
- gateway_value,
|
||||
- metric_value,
|
||||
- self._route_name))
|
||||
+ if "ADDRESS" not in key:
|
||||
+ continue
|
||||
|
||||
+ index = key.replace("ADDRESS", "")
|
||||
+ address_value = str(self._conf[key])
|
||||
+ netmask_value = str(self._conf["NETMASK" + index])
|
||||
+ gateway_value = str(self._conf["GATEWAY" + index])
|
||||
+
|
||||
+ # only accept combinations:
|
||||
+ # if proto ipv6 only display ipv6 routes
|
||||
+ # if proto ipv4 only display ipv4 routes
|
||||
+ # do not add ipv6 routes if proto is ipv4
|
||||
+ # do not add ipv4 routes if proto is ipv6
|
||||
+ # (this array will contain a mix of ipv4 and ipv6)
|
||||
+ if proto == "ipv4" and not self.is_ipv6_route(address_value):
|
||||
+ # increase IPv4 index
|
||||
+ reindex = reindex + 1
|
||||
+ buf.write(
|
||||
+ "%s=%s\n"
|
||||
+ % ("ADDRESS" + str(reindex), _quote_value(address_value))
|
||||
+ )
|
||||
+ buf.write(
|
||||
+ "%s=%s\n"
|
||||
+ % ("GATEWAY" + str(reindex), _quote_value(gateway_value))
|
||||
+ )
|
||||
+ buf.write(
|
||||
+ "%s=%s\n"
|
||||
+ % ("NETMASK" + str(reindex), _quote_value(netmask_value))
|
||||
+ )
|
||||
+ metric_key = "METRIC" + index
|
||||
+ if metric_key in self._conf:
|
||||
+ metric_value = str(self._conf["METRIC" + index])
|
||||
+ buf.write("%s=%s\n"
|
||||
+ % ("METRIC" + str(reindex), _quote_value(metric_value))
|
||||
+ )
|
||||
+ elif proto == "ipv6" and self.is_ipv6_route(address_value):
|
||||
+ prefix_value = network_state.ipv6_mask_to_net_prefix(
|
||||
+ netmask_value
|
||||
+ )
|
||||
+ metric_value = (
|
||||
+ "metric " + str(self._conf["METRIC" + index])
|
||||
+ if "METRIC" + index in self._conf
|
||||
+ else ""
|
||||
+ )
|
||||
+ buf.write(
|
||||
+ "%s/%s via %s %s dev %s\n"
|
||||
+ % (
|
||||
+ address_value,
|
||||
+ prefix_value,
|
||||
+ gateway_value,
|
||||
+ metric_value,
|
||||
+ self._route_name,
|
||||
+ )
|
||||
+ )
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
|
||||
index 21603fb..8b37f39 100644
|
||||
--- a/cloudinit/sources/DataSourceOpenNebula.py
|
||||
+++ b/cloudinit/sources/DataSourceOpenNebula.py
|
||||
@@ -237,7 +237,7 @@ class OpenNebulaNetwork(object):
|
||||
# Set IPv4 address
|
||||
devconf['addresses'] = []
|
||||
mask = self.get_mask(c_dev)
|
||||
- prefix = str(net.mask_to_net_prefix(mask))
|
||||
+ prefix = str(net.ipv4_mask_to_net_prefix(mask))
|
||||
devconf['addresses'].append(
|
||||
self.get_ip(c_dev, mac) + '/' + prefix)
|
||||
|
||||
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
|
||||
index 9cd2c0c..3a45c67 100644
|
||||
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
|
||||
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
-from cloudinit.net.network_state import mask_to_net_prefix
|
||||
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
|
||||
from cloudinit import subp
|
||||
from cloudinit import util
|
||||
|
||||
@@ -180,7 +180,7 @@ class NicConfigurator(object):
|
||||
"""
|
||||
route_list = []
|
||||
|
||||
- cidr = mask_to_net_prefix(netmask)
|
||||
+ cidr = ipv4_mask_to_net_prefix(netmask)
|
||||
|
||||
for gateway in gateways:
|
||||
destination = "%s/%d" % (gen_subnet(gateway, netmask), cidr)
|
||||
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
|
||||
index 2ef5ab7..215cabf 100644
|
||||
--- a/cloudinit/net/tests/test_init.py
|
||||
+++ b/cloudinit/net/tests/test_init.py
|
||||
@@ -588,7 +588,9 @@ class TestEphemeralIPV4Network(CiTestCase):
|
||||
with net.EphemeralIPv4Network(**params):
|
||||
pass
|
||||
error = context_manager.exception
|
||||
- self.assertIn('Cannot setup network: netmask', str(error))
|
||||
+ self.assertIn(
|
||||
+ "Cannot setup network, invalid prefix or netmask: ", str(error)
|
||||
+ )
|
||||
self.assertEqual(0, m_subp.call_count)
|
||||
|
||||
def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
|
||||
diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
|
||||
index 45e9917..f03db50 100644
|
||||
--- a/cloudinit/net/tests/test_network_state.py
|
||||
+++ b/cloudinit/net/tests/test_network_state.py
|
||||
@@ -1,5 +1,5 @@
|
||||
# This file is part of cloud-init. See LICENSE file for license information.
|
||||
-
|
||||
+import ipaddress
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@@ -161,4 +161,60 @@ class TestNetworkStateParseNameservers:
|
||||
'spam.local',
|
||||
] == sorted(config.dns_searchdomains)
|
||||
|
||||
+class TestNetworkStateHelperFunctions(CiTestCase):
|
||||
+ def test_mask_to_net_prefix_ipv4(self):
|
||||
+ netmask_value = "255.255.255.0"
|
||||
+ expected = 24
|
||||
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+ def test_mask_to_net_prefix_all_bits_ipv4(self):
|
||||
+ netmask_value = "255.255.255.255"
|
||||
+ expected = 32
|
||||
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+ def test_mask_to_net_prefix_to_many_bits_ipv4(self):
|
||||
+ netmask_value = "33"
|
||||
+ self.assertRaises(
|
||||
+ ValueError, network_state.ipv4_mask_to_net_prefix, netmask_value
|
||||
+ )
|
||||
+
|
||||
+ def test_mask_to_net_prefix_all_bits_ipv6(self):
|
||||
+ netmask_value = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
|
||||
+ expected = 128
|
||||
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+ def test_mask_to_net_prefix_ipv6(self):
|
||||
+ netmask_value = "ffff:ffff:ffff:ffff::"
|
||||
+ expected = 64
|
||||
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+ def test_mask_to_net_prefix_raises_value_error(self):
|
||||
+ netmask_value = "ff:ff:ff:ff::"
|
||||
+ self.assertRaises(
|
||||
+ ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value
|
||||
+ )
|
||||
+
|
||||
+ def test_mask_to_net_prefix_to_many_bits_ipv6(self):
|
||||
+ netmask_value = "129"
|
||||
+ self.assertRaises(
|
||||
+ ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value
|
||||
+ )
|
||||
+
|
||||
+ def test_mask_to_net_prefix_ipv4_object(self):
|
||||
+ netmask_value = ipaddress.IPv4Address("255.255.255.255")
|
||||
+ expected = 32
|
||||
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+ def test_mask_to_net_prefix_ipv6_object(self):
|
||||
+ netmask_value = ipaddress.IPv6Address("ffff:ffff:ffff::")
|
||||
+ expected = 48
|
||||
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
|
||||
+ assert prefix_value == expected
|
||||
+
|
||||
+
|
||||
# vi: ts=4 expandtab
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index 764e1c7..5013277 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -2237,10 +2237,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
|
||||
routes:
|
||||
- gateway: 2001:67c:1562:1
|
||||
network: 2001:67c:1
|
||||
- netmask: ffff:ffff:0
|
||||
+ netmask: "ffff:ffff::"
|
||||
- gateway: 3001:67c:1562:1
|
||||
network: 3001:67c:1
|
||||
- netmask: ffff:ffff:0
|
||||
+ netmask: "ffff:ffff::"
|
||||
metric: 10000
|
||||
"""),
|
||||
'expected_netplan': textwrap.dedent("""
|
||||
@@ -2507,8 +2507,8 @@ iface bond0 inet6 static
|
||||
'route6-bond0': textwrap.dedent("""\
|
||||
# Created by cloud-init on instance boot automatically, do not edit.
|
||||
#
|
||||
- 2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1 dev bond0
|
||||
- 3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0
|
||||
+ 2001:67c:1/32 via 2001:67c:1562:1 dev bond0
|
||||
+ 3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0
|
||||
"""),
|
||||
'route-bond0': textwrap.dedent("""\
|
||||
ADDRESS0=10.1.3.0
|
||||
@@ -3297,6 +3297,76 @@ USERCTL=no
|
||||
renderer.render_network_state(ns, target=render_dir)
|
||||
self.assertEqual([], os.listdir(render_dir))
|
||||
|
||||
+ def test_invalid_network_mask_ipv6(self):
|
||||
+ net_json = {
|
||||
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
|
||||
+ "networks": [
|
||||
+ {
|
||||
+ "network_id": "public-ipv6",
|
||||
+ "type": "ipv6",
|
||||
+ "netmask": "",
|
||||
+ "link": "tap1a81968a-79",
|
||||
+ "routes": [
|
||||
+ {
|
||||
+ "gateway": "2001:DB8::1",
|
||||
+ "netmask": "ff:ff:ff:ff::",
|
||||
+ "network": "2001:DB8:1::1",
|
||||
+ },
|
||||
+ ],
|
||||
+ "ip_address": "2001:DB8::10",
|
||||
+ "id": "network1",
|
||||
+ }
|
||||
+ ],
|
||||
+ "links": [
|
||||
+ {
|
||||
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
|
||||
+ "mtu": None,
|
||||
+ "type": "bridge",
|
||||
+ "id": "tap1a81968a-79",
|
||||
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
|
||||
+ },
|
||||
+ ],
|
||||
+ }
|
||||
+ macs = {"fa:16:3e:ed:9a:59": "eth0"}
|
||||
+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
|
||||
+ with self.assertRaises(ValueError):
|
||||
+ network_state.parse_net_config_data(network_cfg, skip_broken=False)
|
||||
+
|
||||
+ def test_invalid_network_mask_ipv4(self):
|
||||
+ net_json = {
|
||||
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
|
||||
+ "networks": [
|
||||
+ {
|
||||
+ "network_id": "public-ipv4",
|
||||
+ "type": "ipv4",
|
||||
+ "netmask": "",
|
||||
+ "link": "tap1a81968a-79",
|
||||
+ "routes": [
|
||||
+ {
|
||||
+ "gateway": "172.20.0.1",
|
||||
+ "netmask": "255.234.255.0",
|
||||
+ "network": "172.19.0.0",
|
||||
+ },
|
||||
+ ],
|
||||
+ "ip_address": "172.20.0.10",
|
||||
+ "id": "network1",
|
||||
+ }
|
||||
+ ],
|
||||
+ "links": [
|
||||
+ {
|
||||
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
|
||||
+ "mtu": None,
|
||||
+ "type": "bridge",
|
||||
+ "id": "tap1a81968a-79",
|
||||
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
|
||||
+ },
|
||||
+ ],
|
||||
+ }
|
||||
+ macs = {"fa:16:3e:ed:9a:59": "eth0"}
|
||||
+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
|
||||
+ with self.assertRaises(ValueError):
|
||||
+ network_state.parse_net_config_data(network_cfg, skip_broken=False)
|
||||
+
|
||||
def test_openstack_rendering_samples(self):
|
||||
for os_sample in OS_SAMPLES:
|
||||
render_dir = self.tmp_dir()
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
157
backport-Fix-KeyError-in-iproute-pformat-3287.patch
Normal file
157
backport-Fix-KeyError-in-iproute-pformat-3287.patch
Normal file
@ -0,0 +1,157 @@
|
||||
From 7c52b8a5fbcfe1e7442bbd011956b52e303745c5 Mon Sep 17 00:00:00 2001
|
||||
From: Dmitry Zykov <44090270+zykovd@users.noreply.github.com>
|
||||
Date: Fri, 12 May 2023 21:48:10 +0300
|
||||
Subject: [PATCH] Fix KeyError in iproute pformat (#3287)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/7c52b8a5fbcfe1e7442bbd011956b52e303745c5
|
||||
Conflict:format diff.
|
||||
|
||||
This fixes KeyError on specific network configuration when running
|
||||
cloud-init on "network" stage. The same problem was mentioned in
|
||||
#746 and #1041.
|
||||
---
|
||||
cloudinit/netinfo.py | 16 +++++++----
|
||||
.../route-formatted-output-missing-gateway | 19 +++++++++++++
|
||||
.../sample-iproute-output-v4-missing-gateway | 1 +
|
||||
.../sample-iproute-output-v6-missing-gateway | 10 +++++++
|
||||
cloudinit/tests/test_netinfo.py | 27 +++++++++++++++++++
|
||||
5 files changed, 68 insertions(+), 5 deletions(-)
|
||||
create mode 100644 tests/data/netinfo/route-formatted-output-missing-gateway
|
||||
create mode 100644 tests/data/netinfo/sample-iproute-output-v4-missing-gateway
|
||||
create mode 100644 tests/data/netinfo/sample-iproute-output-v6-missing-gateway
|
||||
|
||||
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
|
||||
index 628e290..f26399a 100644
|
||||
--- a/cloudinit/netinfo.py
|
||||
+++ b/cloudinit/netinfo.py
|
||||
@@ -460,9 +460,12 @@ def route_pformat():
|
||||
tbl_v4 = SimpleTable(fields_v4)
|
||||
for (n, r) in enumerate(routes.get('ipv4')):
|
||||
route_id = str(n)
|
||||
- tbl_v4.add_row([route_id, r['destination'],
|
||||
- r['gateway'], r['genmask'],
|
||||
- r['iface'], r['flags']])
|
||||
+ try:
|
||||
+ tbl_v4.add_row([route_id, r['destination'],
|
||||
+ r['gateway'], r['genmask'],
|
||||
+ r['iface'], r['flags']])
|
||||
+ except KeyError as e:
|
||||
+ util.logexc(LOG, "Route info formatting error: %s" % e)
|
||||
route_s = tbl_v4.get_string()
|
||||
max_len = len(max(route_s.splitlines(), key=len))
|
||||
header = util.center("Route IPv4 info", "+", max_len)
|
||||
@@ -475,8 +478,11 @@ def route_pformat():
|
||||
route_id = str(n)
|
||||
if r['iface'] == 'lo':
|
||||
continue
|
||||
- tbl_v6.add_row([route_id, r['destination'],
|
||||
- r['gateway'], r['iface'], r['flags']])
|
||||
+ try:
|
||||
+ tbl_v6.add_row([route_id, r['destination'],
|
||||
+ r['gateway'], r['iface'], r['flags']])
|
||||
+ except KeyError as e:
|
||||
+ util.logexc(LOG, "Route info formatting error: %s" % e)
|
||||
route_s = tbl_v6.get_string()
|
||||
max_len = len(max(route_s.splitlines(), key=len))
|
||||
header = util.center("Route IPv6 info", "+", max_len)
|
||||
diff --git a/tests/data/netinfo/route-formatted-output-missing-gateway b/tests/data/netinfo/route-formatted-output-missing-gateway
|
||||
new file mode 100644
|
||||
index 0000000..8bbd819
|
||||
--- /dev/null
|
||||
+++ b/tests/data/netinfo/route-formatted-output-missing-gateway
|
||||
@@ -0,0 +1,19 @@
|
||||
++++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++
|
||||
++-------+-------------+---------+---------------+-----------+-------+
|
||||
+| Route | Destination | Gateway | Genmask | Interface | Flags |
|
||||
++-------+-------------+---------+---------------+-----------+-------+
|
||||
+| 0 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | enp0s25 | U |
|
||||
++-------+-------------+---------+---------------+-----------+-------+
|
||||
+++++++++++++++++++++++++++Route IPv6 info++++++++++++++++++++++++++
|
||||
++-------+---------------------------+---------+-----------+-------+
|
||||
+| Route | Destination | Gateway | Interface | Flags |
|
||||
++-------+---------------------------+---------+-----------+-------+
|
||||
+| 0 | 2a00:abcd:82ae:cd33::657 | :: | enp0s25 | Ue |
|
||||
+| 1 | 2a00:abcd:82ae:cd33::/64 | :: | enp0s25 | U |
|
||||
+| 2 | 2a00:abcd:82ae:cd33::/56 | :: | enp0s25 | U |
|
||||
+| 3 | fd81:123f:654::657 | :: | enp0s25 | U |
|
||||
+| 4 | fd81:123f:654::/64 | :: | enp0s25 | U |
|
||||
+| 5 | fd81:123f:654::/48 | :: | enp0s25 | U |
|
||||
+| 6 | fe80::abcd:ef12:bc34:da21 | :: | enp0s25 | U |
|
||||
+| 7 | fe80::/64 | :: | enp0s25 | U |
|
||||
++-------+---------------------------+---------+-----------+-------+
|
||||
diff --git a/tests/data/netinfo/sample-iproute-output-v4-missing-gateway b/tests/data/netinfo/sample-iproute-output-v4-missing-gateway
|
||||
new file mode 100644
|
||||
index 0000000..c1e0b3c
|
||||
--- /dev/null
|
||||
+++ b/tests/data/netinfo/sample-iproute-output-v4-missing-gateway
|
||||
@@ -0,0 +1 @@
|
||||
+192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100
|
||||
diff --git a/tests/data/netinfo/sample-iproute-output-v6-missing-gateway b/tests/data/netinfo/sample-iproute-output-v6-missing-gateway
|
||||
new file mode 100644
|
||||
index 0000000..ffab1fa
|
||||
--- /dev/null
|
||||
+++ b/tests/data/netinfo/sample-iproute-output-v6-missing-gateway
|
||||
@@ -0,0 +1,10 @@
|
||||
+2a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium
|
||||
+2a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium
|
||||
+2a00:abcd:82ae:cd33::/56 dev enp0s25 proto ra metric 100 pref medium
|
||||
+fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium
|
||||
+fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium
|
||||
+fd81:123f:654::/48 dev enp0s25 proto ra metric 100 pref medium
|
||||
+fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium
|
||||
+fe80::/64 dev enp0s25 proto kernel metric 256 pref medium
|
||||
+local ::1 dev lo table local proto none metric 0 pref medium
|
||||
+local 2600:1f16:b80:ad00:90a:c915:bca6:5ff2 dev lo table local proto none metric 0 pref medium
|
||||
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
|
||||
index e44b16d..6619e0d 100644
|
||||
--- a/cloudinit/tests/test_netinfo.py
|
||||
+++ b/cloudinit/tests/test_netinfo.py
|
||||
@@ -15,10 +15,19 @@ SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
|
||||
SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
|
||||
SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
|
||||
SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
|
||||
+SAMPLE_IPROUTE_OUT_V6_MISSING_GATEWAY = readResource(
|
||||
+ "netinfo/sample-iproute-output-v6-missing-gateway"
|
||||
+)
|
||||
+SAMPLE_IPROUTE_OUT_V4_MISSING_GATEWAY = readResource(
|
||||
+ "netinfo/sample-iproute-output-v4-missing-gateway"
|
||||
+)
|
||||
SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
|
||||
SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
|
||||
NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
|
||||
ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
|
||||
+ROUTE_FORMATTED_OUT_MISSING_GATEWAY = readResource(
|
||||
+ "netinfo/route-formatted-output-missing-gateway"
|
||||
+)
|
||||
FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output")
|
||||
|
||||
|
||||
@@ -165,6 +174,24 @@ class TestNetInfo(CiTestCase):
|
||||
content = route_pformat()
|
||||
self.assertEqual(ROUTE_FORMATTED_OUT, content)
|
||||
|
||||
+ @mock.patch("cloudinit.netinfo.subp.which")
|
||||
+ @mock.patch("cloudinit.netinfo.subp.subp")
|
||||
+ def test_route_iproute_pformat_missing_gateway(self, m_subp, m_which):
|
||||
+ """route_pformat properly rendering info with missing gateway."""
|
||||
+
|
||||
+ def subp_iproute_selector(*args, **kwargs):
|
||||
+ if ["ip", "-o", "route", "list"] == args[0]:
|
||||
+ return (SAMPLE_IPROUTE_OUT_V4_MISSING_GATEWAY, "")
|
||||
+ v6cmd = ["ip", "--oneline", "-6", "route", "list", "table", "all"]
|
||||
+ if v6cmd == args[0]:
|
||||
+ return (SAMPLE_IPROUTE_OUT_V6_MISSING_GATEWAY, "")
|
||||
+ raise RuntimeError("Unexpected subp call %s" % args[0])
|
||||
+
|
||||
+ m_subp.side_effect = subp_iproute_selector
|
||||
+ m_which.side_effect = lambda x: x if x == "ip" else None
|
||||
+ content = route_pformat()
|
||||
+ assert ROUTE_FORMATTED_OUT_MISSING_GATEWAY == content
|
||||
+
|
||||
@mock.patch('cloudinit.netinfo.subp.which')
|
||||
@mock.patch('cloudinit.netinfo.subp.subp')
|
||||
def test_route_warn_on_missing_commands(self, m_subp, m_which):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
291
backport-Fix-KeyError-when-rendering-sysconfig-IPv6-routes.patch
Normal file
291
backport-Fix-KeyError-when-rendering-sysconfig-IPv6-routes.patch
Normal file
@ -0,0 +1,291 @@
|
||||
From 9fabfff5aac6e1b398e18429e6dfad54f59e7f75 Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Thu, 14 Apr 2022 09:31:17 -0500
|
||||
Subject: [PATCH] Fix KeyError when rendering sysconfig IPv6 routes (#1380)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/9fabfff5aac6e1b398e18429e6dfad54f59e7f75
|
||||
Conflict:(1)do not delete ipv6_mask_to_net_prefix.
|
||||
(2)do not add "NM_CONTROLLED=no" in test because of cloud-init-20.4-nm-controlled.patch.
|
||||
(3)format diffs.
|
||||
|
||||
Route rendering code was expecting a netmask rather than using the
|
||||
prefix. A prefix is provided to the renderer, but was being hidden
|
||||
from the route rendering code. This commit exposes the prefix and
|
||||
prefers it for IPv6, given how uncommon netmasks are for IPv6.
|
||||
|
||||
LP: #1958506
|
||||
---
|
||||
cloudinit/net/network_state.py | 4 +-
|
||||
cloudinit/net/renderer.py | 4 +-
|
||||
cloudinit/net/sysconfig.py | 41 +++++++-------
|
||||
tests/unittests/test_net.py | 100 +++++++++++++++++++++++++++++++++
|
||||
4 files changed, 124 insertions(+), 25 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
|
||||
index 0a4e0c6..0d81291 100644
|
||||
--- a/cloudinit/net/network_state.py
|
||||
+++ b/cloudinit/net/network_state.py
|
||||
@@ -885,12 +885,12 @@ def _normalize_net_keys(network, address_keys=()):
|
||||
else:
|
||||
# this supports input of 255.255.255.0
|
||||
prefix = ipv4_mask_to_net_prefix(maybe_prefix)
|
||||
+ elif "prefix" in net:
|
||||
+ prefix = int(net["prefix"])
|
||||
elif netmask and not ipv6:
|
||||
prefix = ipv4_mask_to_net_prefix(netmask)
|
||||
elif netmask and ipv6:
|
||||
prefix = ipv6_mask_to_net_prefix(netmask)
|
||||
- elif 'prefix' in net:
|
||||
- prefix = int(net['prefix'])
|
||||
else:
|
||||
prefix = 64 if ipv6 else 24
|
||||
|
||||
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
|
||||
index 54a83b5..e387173 100644
|
||||
--- a/cloudinit/net/renderer.py
|
||||
+++ b/cloudinit/net/renderer.py
|
||||
@@ -8,7 +8,7 @@
|
||||
import abc
|
||||
import io
|
||||
|
||||
-from cloudinit.net.network_state import parse_net_config_data
|
||||
+from cloudinit.net.network_state import NetworkState, parse_net_config_data
|
||||
from cloudinit.net.udev import generate_udev_rule
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class Renderer(object):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
- def _render_persistent_net(network_state):
|
||||
+ def _render_persistent_net(network_state: NetworkState):
|
||||
"""Given state, emit udev rules to map mac to ifname."""
|
||||
# TODO(harlowja): this seems shared between eni renderer and
|
||||
# this, so move it to a shared location.
|
||||
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
|
||||
index 4c6caef..6d17d69 100644
|
||||
--- a/cloudinit/net/sysconfig.py
|
||||
+++ b/cloudinit/net/sysconfig.py
|
||||
@@ -4,6 +4,7 @@ import copy
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
+from typing import Mapping
|
||||
|
||||
from configobj import ConfigObj
|
||||
|
||||
@@ -17,6 +18,7 @@ from cloudinit.net import network_state
|
||||
from . import renderer
|
||||
from .network_state import (
|
||||
is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES)
|
||||
+from cloudinit.net.network_state import NetworkState
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
KNOWN_DISTROS = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora',
|
||||
@@ -178,7 +180,6 @@ class Route(ConfigMap):
|
||||
|
||||
index = key.replace("ADDRESS", "")
|
||||
address_value = str(self._conf[key])
|
||||
- netmask_value = str(self._conf["NETMASK" + index])
|
||||
gateway_value = str(self._conf["GATEWAY" + index])
|
||||
|
||||
# only accept combinations:
|
||||
@@ -188,6 +189,7 @@ class Route(ConfigMap):
|
||||
# do not add ipv4 routes if proto is ipv6
|
||||
# (this array will contain a mix of ipv4 and ipv6)
|
||||
if proto == "ipv4" and not self.is_ipv6_route(address_value):
|
||||
+ netmask_value = str(self._conf["NETMASK" + index])
|
||||
# increase IPv4 index
|
||||
reindex = reindex + 1
|
||||
buf.write(
|
||||
@@ -209,9 +211,7 @@ class Route(ConfigMap):
|
||||
% ("METRIC" + str(reindex), _quote_value(metric_value))
|
||||
)
|
||||
elif proto == "ipv6" and self.is_ipv6_route(address_value):
|
||||
- prefix_value = network_state.ipv6_mask_to_net_prefix(
|
||||
- netmask_value
|
||||
- )
|
||||
+ prefix_value = str(self._conf[f"PREFIX{index}"])
|
||||
metric_value = (
|
||||
"metric " + str(self._conf["METRIC" + index])
|
||||
if "METRIC" + index in self._conf
|
||||
@@ -604,12 +604,9 @@ class Renderer(renderer.Renderer):
|
||||
raise ValueError("Duplicate declaration of default "
|
||||
"route found for interface '%s'"
|
||||
% (iface_cfg.name))
|
||||
- # NOTE(harlowja): ipv6 and ipv4 default gateways
|
||||
- gw_key = 'GATEWAY0'
|
||||
- nm_key = 'NETMASK0'
|
||||
- addr_key = 'ADDRESS0'
|
||||
- # The owning interface provides the default route.
|
||||
- #
|
||||
+ # NOTE that instead of defining the route0 settings,
|
||||
+ # the owning interface provides the default route.
|
||||
+
|
||||
# TODO(harlowja): add validation that no other iface has
|
||||
# also provided the default route?
|
||||
iface_cfg['DEFROUTE'] = True
|
||||
@@ -626,19 +623,19 @@ class Renderer(renderer.Renderer):
|
||||
iface_cfg['METRIC'] = route['metric']
|
||||
|
||||
else:
|
||||
- gw_key = 'GATEWAY%s' % route_cfg.last_idx
|
||||
- nm_key = 'NETMASK%s' % route_cfg.last_idx
|
||||
- addr_key = 'ADDRESS%s' % route_cfg.last_idx
|
||||
- metric_key = 'METRIC%s' % route_cfg.last_idx
|
||||
- route_cfg.last_idx += 1
|
||||
# add default routes only to ifcfg files, not
|
||||
# to route-* or route6-*
|
||||
- for (old_key, new_key) in [('gateway', gw_key),
|
||||
- ('metric', metric_key),
|
||||
- ('netmask', nm_key),
|
||||
- ('network', addr_key)]:
|
||||
+ for old_key, new_name in [
|
||||
+ ("gateway", "GATEWAY"),
|
||||
+ ("metric", "METRIC"),
|
||||
+ ("prefix", "PREFIX"),
|
||||
+ ("netmask", "NETMASK"),
|
||||
+ ("network", "ADDRESS"),
|
||||
+ ]:
|
||||
if old_key in route:
|
||||
+ new_key = f"{new_name}{route_cfg.last_idx}"
|
||||
route_cfg[new_key] = route[old_key]
|
||||
+ route_cfg.last_idx += 1
|
||||
|
||||
@classmethod
|
||||
def _render_bonding_opts(cls, iface_cfg, iface, flavor):
|
||||
@@ -890,7 +887,7 @@ class Renderer(renderer.Renderer):
|
||||
'''Given state, return /etc/sysconfig files + contents'''
|
||||
if not templates:
|
||||
templates = cls.templates
|
||||
- iface_contents = {}
|
||||
+ iface_contents: Mapping[str, NetInterface] = {}
|
||||
for iface in network_state.iter_interfaces():
|
||||
if iface['type'] == "loopback":
|
||||
continue
|
||||
@@ -922,7 +919,9 @@ class Renderer(renderer.Renderer):
|
||||
contents[cpath] = iface_cfg.routes.to_string(proto)
|
||||
return contents
|
||||
|
||||
- def render_network_state(self, network_state, templates=None, target=None):
|
||||
+ def render_network_state(
|
||||
+ self, network_state: NetworkState, templates=None, target=None
|
||||
+ ):
|
||||
if not templates:
|
||||
templates = self.templates
|
||||
file_mode = 0o644
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index 8737a76..a698c65 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -3871,6 +3871,106 @@ USERCTL=no
|
||||
self._compare_files_to_expected(
|
||||
expected, self._render_and_read(network_config=v2data))
|
||||
|
||||
+ def test_from_v2_routes(self):
|
||||
+ """verify routes (including IPv6) get rendered using v2 config.
|
||||
+ LP: #1958506
|
||||
+ """
|
||||
+ v2_data = {
|
||||
+ "version": 2,
|
||||
+ "ethernets": {
|
||||
+ "eth0": {
|
||||
+ "addresses": [
|
||||
+ "10.54.2.19/21",
|
||||
+ "2a00:1730:fff9:100::52/128",
|
||||
+ ],
|
||||
+ "gateway4": "10.54.0.1",
|
||||
+ "gateway6": "2a00:1730:fff9:100::1",
|
||||
+ "match": {"macaddress": "52:54:00:3f:fc:f7"},
|
||||
+ "mtu": 1400,
|
||||
+ "nameservers": {
|
||||
+ "addresses": [
|
||||
+ "10.52.1.1",
|
||||
+ "10.52.1.71",
|
||||
+ "2001:4860:4860::8888",
|
||||
+ "2001:4860:4860::8844",
|
||||
+ ]
|
||||
+ },
|
||||
+ "routes": [
|
||||
+ {
|
||||
+ "scope": "link",
|
||||
+ "to": "10.54.0.1/32",
|
||||
+ "via": "0.0.0.0",
|
||||
+ },
|
||||
+ {
|
||||
+ "scope": "link",
|
||||
+ "to": "0.0.0.0/0",
|
||||
+ "via": "10.54.0.1",
|
||||
+ },
|
||||
+ {
|
||||
+ "scope": "link",
|
||||
+ "to": "2a00:1730:fff9:100::1/128",
|
||||
+ "via": "::0",
|
||||
+ },
|
||||
+ {
|
||||
+ "scope": "link",
|
||||
+ "to": "::0/0",
|
||||
+ "via": "2a00:1730:fff9:100::1",
|
||||
+ },
|
||||
+ ],
|
||||
+ "set-name": "eth0",
|
||||
+ }
|
||||
+ },
|
||||
+ }
|
||||
+
|
||||
+ expected = {
|
||||
+ "ifcfg-eth0": textwrap.dedent(
|
||||
+ """\
|
||||
+ # Created by cloud-init on instance boot automatically, do not edit.
|
||||
+ #
|
||||
+ BOOTPROTO=none
|
||||
+ DEFROUTE=yes
|
||||
+ DEVICE=eth0
|
||||
+ DNS1=10.52.1.1
|
||||
+ DNS2=10.52.1.71
|
||||
+ DNS3=2001:4860:4860::8888
|
||||
+ GATEWAY=10.54.0.1
|
||||
+ HWADDR=52:54:00:3f:fc:f7
|
||||
+ IPADDR=10.54.2.19
|
||||
+ IPV6ADDR=2a00:1730:fff9:100::52/128
|
||||
+ IPV6INIT=yes
|
||||
+ IPV6_AUTOCONF=no
|
||||
+ IPV6_DEFAULTGW=2a00:1730:fff9:100::1
|
||||
+ IPV6_FORCE_ACCEPT_RA=no
|
||||
+ MTU=1400
|
||||
+ NETMASK=255.255.248.0
|
||||
+ ONBOOT=yes
|
||||
+ TYPE=Ethernet
|
||||
+ USERCTL=no
|
||||
+ """ # noqa: E501
|
||||
+ ),
|
||||
+ "route-eth0": textwrap.dedent(
|
||||
+ """\
|
||||
+ # Created by cloud-init on instance boot automatically, do not edit.
|
||||
+ #
|
||||
+ ADDRESS0=10.54.0.1
|
||||
+ GATEWAY0=0.0.0.0
|
||||
+ NETMASK0=255.255.255.255
|
||||
+ """ # noqa: E501
|
||||
+ ),
|
||||
+ "route6-eth0": textwrap.dedent(
|
||||
+ """\
|
||||
+ # Created by cloud-init on instance boot automatically, do not edit.
|
||||
+ #
|
||||
+ 2a00:1730:fff9:100::1/128 via ::0 dev eth0
|
||||
+ ::0/64 via 2a00:1730:fff9:100::1 dev eth0
|
||||
+ """ # noqa: E501
|
||||
+ ),
|
||||
+ }
|
||||
+
|
||||
+ found = self._render_and_read(network_config=v2_data)
|
||||
+ self._compare_files_to_expected(expected, found)
|
||||
+ self._assert_headers(found)
|
||||
+
|
||||
|
||||
@mock.patch(
|
||||
"cloudinit.net.is_openvswitch_internal_interface",
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
47
backport-Fix-default-route-rendering-on-v2-ipv6-1973.patch
Normal file
47
backport-Fix-default-route-rendering-on-v2-ipv6-1973.patch
Normal file
@ -0,0 +1,47 @@
|
||||
From 7d57fcff6d32fd706dd745315c0f8f72d94385eb Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Tue, 24 Jan 2023 08:54:05 -0600
|
||||
Subject: [PATCH] Fix default route rendering on v2 ipv6 (#1973)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/7d57fcff6d32fd706dd745315c0f8f72d94385eb
|
||||
Conflict:NA
|
||||
|
||||
::/0 would get rendered as ::/64 rather than ::/0 across all renderers
|
||||
using ipv6 in a v2 config.
|
||||
|
||||
LP: #2003562
|
||||
---
|
||||
cloudinit/net/network_state.py | 2 +-
|
||||
tests/unittests/test_net.py | 2 +-
|
||||
2 files changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
|
||||
index 0d81291..857942a 100644
|
||||
--- a/cloudinit/net/network_state.py
|
||||
+++ b/cloudinit/net/network_state.py
|
||||
@@ -857,7 +857,7 @@ def _normalize_net_keys(network, address_keys=()):
|
||||
|
||||
@returns: A dict containing normalized prefix and matching addr_key.
|
||||
"""
|
||||
- net = dict((k, v) for k, v in network.items() if v)
|
||||
+ net = {k: v for k, v in network.items() if v or v == 0}
|
||||
addr_key = None
|
||||
for key in address_keys:
|
||||
if net.get(key):
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index a698c65..2d2cff0 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -3962,7 +3962,7 @@ USERCTL=no
|
||||
# Created by cloud-init on instance boot automatically, do not edit.
|
||||
#
|
||||
2a00:1730:fff9:100::1/128 via ::0 dev eth0
|
||||
- ::0/64 via 2a00:1730:fff9:100::1 dev eth0
|
||||
+ ::0/0 via 2a00:1730:fff9:100::1 dev eth0
|
||||
""" # noqa: E501
|
||||
),
|
||||
}
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
89
backport-Fix-exception-when-no-activator-found-1129.patch
Normal file
89
backport-Fix-exception-when-no-activator-found-1129.patch
Normal file
@ -0,0 +1,89 @@
|
||||
From ffa6fc88249aa080aa31811a45569a45e567418a Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Thu, 2 Dec 2021 22:36:37 -0600
|
||||
Subject: [PATCH] Fix exception when no activator found (#1129)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/ffa6fc88249aa080aa31811a45569a45e567418a
|
||||
Conflict:NA
|
||||
|
||||
Given that there are additional network management tools that we haven't
|
||||
yet supported with activators, we should log a warning and continue
|
||||
without network activation here, especially since this was a no-op for
|
||||
years.
|
||||
|
||||
LP: #1948681
|
||||
---
|
||||
cloudinit/distros/__init__.py | 7 ++++++-
|
||||
cloudinit/net/activators.py | 6 +++++-
|
||||
tests/unittests/test_net_activators.py | 5 +++--
|
||||
3 files changed, 14 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
|
||||
index cf6aad14..fe44f20e 100755
|
||||
--- a/cloudinit/distros/__init__.py
|
||||
+++ b/cloudinit/distros/__init__.py
|
||||
@@ -228,7 +228,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
|
||||
# Now try to bring them up
|
||||
if bring_up:
|
||||
LOG.debug('Bringing up newly configured network interfaces')
|
||||
- network_activator = activators.select_activator()
|
||||
+ try:
|
||||
+ network_activator = activators.select_activator()
|
||||
+ except activators.NoActivatorException:
|
||||
+ LOG.warning("No network activator found, not bringing up "
|
||||
+ "network interfaces")
|
||||
+ return True
|
||||
network_activator.bring_up_all_interfaces(network_state)
|
||||
else:
|
||||
LOG.debug("Not bringing up newly configured network interfaces")
|
||||
diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py
|
||||
index 11149548..137338d8 100644
|
||||
--- a/cloudinit/net/activators.py
|
||||
+++ b/cloudinit/net/activators.py
|
||||
@@ -16,6 +16,10 @@ from cloudinit.net.sysconfig import NM_CFG_FILE
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
+class NoActivatorException(Exception):
|
||||
+ pass
|
||||
+
|
||||
+
|
||||
def _alter_interface(cmd, device_name) -> bool:
|
||||
LOG.debug("Attempting command %s for device %s", cmd, device_name)
|
||||
try:
|
||||
@@ -271,7 +275,7 @@ def select_activator(priority=None, target=None) -> Type[NetworkActivator]:
|
||||
tmsg = ""
|
||||
if target and target != "/":
|
||||
tmsg = " in target=%s" % target
|
||||
- raise RuntimeError(
|
||||
+ raise NoActivatorException(
|
||||
"No available network activators found%s. Searched "
|
||||
"through list: %s" % (tmsg, priority))
|
||||
selected = found[0]
|
||||
diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py
|
||||
index f63a8b74..9da21195 100644
|
||||
--- a/tests/unittests/test_net_activators.py
|
||||
+++ b/tests/unittests/test_net_activators.py
|
||||
@@ -12,7 +12,8 @@ from cloudinit.net.activators import (
|
||||
IfUpDownActivator,
|
||||
NetplanActivator,
|
||||
NetworkManagerActivator,
|
||||
- NetworkdActivator
|
||||
+ NetworkdActivator,
|
||||
+ NoActivatorException,
|
||||
)
|
||||
from cloudinit.net.network_state import parse_net_config_data
|
||||
from cloudinit.safeyaml import load
|
||||
@@ -99,7 +100,7 @@ class TestSearchAndSelect:
|
||||
resp = search_activator()
|
||||
assert resp == []
|
||||
|
||||
- with pytest.raises(RuntimeError):
|
||||
+ with pytest.raises(NoActivatorException):
|
||||
select_activator()
|
||||
|
||||
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
388
backport-Fix-network-v2-metric-rendering-4220.patch
Normal file
388
backport-Fix-network-v2-metric-rendering-4220.patch
Normal file
@ -0,0 +1,388 @@
|
||||
From c68305a91791e28031df8b9ebd33bfe7ffd7e75d Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Tue, 4 Jul 2023 07:00:22 -0500
|
||||
Subject: [PATCH] Fix network v2 metric rendering (#4220)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/c68305a91791e28031df8b9ebd33bfe7ffd7e75d
|
||||
Conflict:(1)change 'small' to 'small_v1'
|
||||
(2)do not change TestNetworkManagerRendering.
|
||||
(3)do not add "NM_CONTROLLED=no" in test because of cloud-init-20.4-nm-controlled.patch.
|
||||
(4)format diffs.
|
||||
|
||||
Metric info was not being included in v2-based routes.
|
||||
|
||||
Fixes GH-4217
|
||||
---
|
||||
cloudinit/net/network_state.py | 4 +-
|
||||
tests/unittests/test_net.py | 232 ++++++++++++++++++++++++++++++---
|
||||
2 files changed, 216 insertions(+), 20 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
|
||||
index 4862bf9..ac44304 100644
|
||||
--- a/cloudinit/net/network_state.py
|
||||
+++ b/cloudinit/net/network_state.py
|
||||
@@ -187,7 +187,6 @@ class NetworkState(object):
|
||||
|
||||
|
||||
class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
|
||||
-
|
||||
initial_network_state = {
|
||||
'interfaces': {},
|
||||
'routes': [],
|
||||
@@ -582,7 +581,6 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
|
||||
self._handle_bond_bridge(command, cmd_type='bond')
|
||||
|
||||
def handle_bridges(self, command):
|
||||
-
|
||||
'''
|
||||
v2_command = {
|
||||
br0: {
|
||||
@@ -815,7 +813,7 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
|
||||
routes = []
|
||||
for route in cfg.get('routes', []):
|
||||
routes.append(_normalize_route(
|
||||
- {'destination': route.get('to'), 'gateway': route.get('via')}))
|
||||
+ {'destination': route.get('to'), 'gateway': route.get('via'), "metric": route.get("metric"),}))
|
||||
|
||||
# v2 routes are bound to the interface, in v1 we add them under
|
||||
# the first subnet since there isn't an equivalent interface level.
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index 7cde102..764e1c7 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -818,7 +818,7 @@ iface eth1 inet static
|
||||
""".lstrip()
|
||||
|
||||
NETWORK_CONFIGS = {
|
||||
- 'small': {
|
||||
+ 'small_v1': {
|
||||
'expected_networkd_eth99': textwrap.dedent("""\
|
||||
[Match]
|
||||
Name=eth99
|
||||
@@ -959,6 +959,164 @@ NETWORK_CONFIGS = {
|
||||
- wark.maas
|
||||
"""),
|
||||
},
|
||||
+ # We test a separate set of configs here because v2 doesn't support
|
||||
+ # generic nameservers, so that aspect needs to be modified
|
||||
+ "small_v2": {
|
||||
+ "expected_networkd_eth99": textwrap.dedent(
|
||||
+ """\
|
||||
+ [Match]
|
||||
+ Name=eth99
|
||||
+ MACAddress=c0:d6:9f:2c:e8:80
|
||||
+ [Address]
|
||||
+ Address=192.168.21.3/24
|
||||
+ [Network]
|
||||
+ DHCP=ipv4
|
||||
+ Domains=barley.maas sach.maas
|
||||
+ DNS=8.8.8.8 8.8.4.4
|
||||
+ [Route]
|
||||
+ Gateway=65.61.151.37
|
||||
+ Destination=0.0.0.0/0
|
||||
+ Metric=10000
|
||||
+ """
|
||||
+ ).rstrip(" "),
|
||||
+ "expected_networkd_eth1": textwrap.dedent(
|
||||
+ """\
|
||||
+ [Match]
|
||||
+ Name=eth1
|
||||
+ MACAddress=cf:d6:af:48:e8:80
|
||||
+ [Network]
|
||||
+ DHCP=no
|
||||
+ """
|
||||
+ ).rstrip(" "),
|
||||
+ "expected_eni": textwrap.dedent(
|
||||
+ """\
|
||||
+ auto lo
|
||||
+ iface lo inet loopback
|
||||
+ dns-nameservers 8.8.8.8 8.8.4.4
|
||||
+ dns-search wark.maas
|
||||
+ iface eth1 inet manual
|
||||
+ auto eth99
|
||||
+ iface eth99 inet dhcp
|
||||
+ # control-alias eth99
|
||||
+ iface eth99 inet static
|
||||
+ address 192.168.21.3/24
|
||||
+ dns-nameservers 8.8.8.8 8.8.4.4
|
||||
+ dns-search barley.maas sach.maas
|
||||
+ post-up route add default gw 65.61.151.37 metric 10000 || true
|
||||
+ pre-down route del default gw 65.61.151.37 metric 10000 || true
|
||||
+ """
|
||||
+ ).rstrip(" "),
|
||||
+ "expected_sysconfig_opensuse": {
|
||||
+ "ifcfg-eth1": textwrap.dedent(
|
||||
+ """\
|
||||
+ BOOTPROTO=static
|
||||
+ LLADDR=cf:d6:af:48:e8:80
|
||||
+ STARTMODE=auto"""
|
||||
+ ),
|
||||
+ "ifcfg-eth99": textwrap.dedent(
|
||||
+ """\
|
||||
+ BOOTPROTO=dhcp4
|
||||
+ LLADDR=c0:d6:9f:2c:e8:80
|
||||
+ IPADDR=192.168.21.3
|
||||
+ NETMASK=255.255.255.0
|
||||
+ STARTMODE=auto"""
|
||||
+ ),
|
||||
+ },
|
||||
+ "expected_sysconfig_rhel": {
|
||||
+ "ifcfg-eth1": textwrap.dedent(
|
||||
+ """\
|
||||
+ BOOTPROTO=none
|
||||
+ DEVICE=eth1
|
||||
+ HWADDR=cf:d6:af:48:e8:80
|
||||
+ ONBOOT=yes
|
||||
+ TYPE=Ethernet
|
||||
+ USERCTL=no"""
|
||||
+ ),
|
||||
+ "ifcfg-eth99": textwrap.dedent(
|
||||
+ """\
|
||||
+ BOOTPROTO=dhcp
|
||||
+ DEFROUTE=yes
|
||||
+ DEVICE=eth99
|
||||
+ DHCLIENT_SET_DEFAULT_ROUTE=yes
|
||||
+ DNS1=8.8.8.8
|
||||
+ DNS2=8.8.4.4
|
||||
+ DOMAIN="barley.maas sach.maas"
|
||||
+ GATEWAY=65.61.151.37
|
||||
+ HWADDR=c0:d6:9f:2c:e8:80
|
||||
+ IPADDR=192.168.21.3
|
||||
+ NETMASK=255.255.255.0
|
||||
+ METRIC=10000
|
||||
+ ONBOOT=yes
|
||||
+ TYPE=Ethernet
|
||||
+ USERCTL=no"""
|
||||
+ ),
|
||||
+ },
|
||||
+ "expected_network_manager": {
|
||||
+ "cloud-init-eth1.nmconnection": textwrap.dedent(
|
||||
+ """\
|
||||
+ # Generated by cloud-init. Changes will be lost.
|
||||
+ [connection]
|
||||
+ id=cloud-init eth1
|
||||
+ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
|
||||
+ autoconnect-priority=120
|
||||
+ type=ethernet
|
||||
+ [user]
|
||||
+ org.freedesktop.NetworkManager.origin=cloud-init
|
||||
+ [ethernet]
|
||||
+ mac-address=CF:D6:AF:48:E8:80
|
||||
+ """
|
||||
+ ),
|
||||
+ "cloud-init-eth99.nmconnection": textwrap.dedent(
|
||||
+ """\
|
||||
+ # Generated by cloud-init. Changes will be lost.
|
||||
+ [connection]
|
||||
+ id=cloud-init eth99
|
||||
+ uuid=b1b88000-1f03-5360-8377-1a2205efffb4
|
||||
+ autoconnect-priority=120
|
||||
+ type=ethernet
|
||||
+ [user]
|
||||
+ org.freedesktop.NetworkManager.origin=cloud-init
|
||||
+ [ethernet]
|
||||
+ mac-address=C0:D6:9F:2C:E8:80
|
||||
+ [ipv4]
|
||||
+ method=auto
|
||||
+ may-fail=false
|
||||
+ route1=0.0.0.0/0,65.61.151.37
|
||||
+ address1=192.168.21.3/24
|
||||
+ dns=8.8.8.8;8.8.4.4;
|
||||
+ dns-search=barley.maas;sach.maas;
|
||||
+ """
|
||||
+ ),
|
||||
+ },
|
||||
+ "yaml": textwrap.dedent(
|
||||
+ """
|
||||
+ version: 2
|
||||
+ ethernets:
|
||||
+ eth1:
|
||||
+ match:
|
||||
+ macaddress: cf:d6:af:48:e8:80
|
||||
+ set-name: eth1
|
||||
+ eth99:
|
||||
+ addresses:
|
||||
+ - 192.168.21.3/24
|
||||
+ dhcp4: true
|
||||
+ match:
|
||||
+ macaddress: c0:d6:9f:2c:e8:80
|
||||
+ nameservers:
|
||||
+ addresses:
|
||||
+ - 8.8.8.8
|
||||
+ - 8.8.4.4
|
||||
+ search:
|
||||
+ - barley.maas
|
||||
+ - sach.maas
|
||||
+ routes:
|
||||
+ - metric: 10000
|
||||
+ to: 0.0.0.0/0
|
||||
+ via: 65.61.151.37
|
||||
+ set-name: eth99
|
||||
+ """
|
||||
+ ),
|
||||
+ },
|
||||
'v4_and_v6': {
|
||||
'expected_networkd': textwrap.dedent("""\
|
||||
[Match]
|
||||
@@ -2965,7 +3123,6 @@ iface eth1 inet dhcp
|
||||
mock.Mock(return_value=False)
|
||||
)
|
||||
class TestRhelSysConfigRendering(CiTestCase):
|
||||
-
|
||||
with_logs = True
|
||||
|
||||
nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf"
|
||||
@@ -3286,8 +3443,14 @@ USERCTL=no
|
||||
'WARNING: Network config: ignoring eth0.101 device-level mtu',
|
||||
self.logs.getvalue())
|
||||
|
||||
- def test_small_config(self):
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ def test_small_config_v1(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v1"]
|
||||
+ found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
|
||||
+ self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
+ self._assert_headers(found)
|
||||
+
|
||||
+ def test_small_config_v2(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v2"]
|
||||
found = self._render_and_read(network_config=yaml.load(entry['yaml']))
|
||||
self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
self._assert_headers(found)
|
||||
@@ -3429,7 +3592,7 @@ USERCTL=no
|
||||
self.assertTrue(os.path.exists(nm_cfg))
|
||||
|
||||
# render and read
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ entry = NETWORK_CONFIGS['small_v1']
|
||||
found = self._render_and_read(network_config=yaml.load(entry['yaml']),
|
||||
dir=render_dir)
|
||||
self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
@@ -3450,7 +3613,7 @@ USERCTL=no
|
||||
util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\nplugins=foo\n')
|
||||
|
||||
# render and read
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ entry = NETWORK_CONFIGS['small_v1']
|
||||
found = self._render_and_read(network_config=yaml.load(entry['yaml']),
|
||||
dir=render_dir)
|
||||
self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
@@ -3476,7 +3639,7 @@ USERCTL=no
|
||||
self.assertTrue(os.path.exists(nm_cfg))
|
||||
|
||||
# render and read
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ entry = NETWORK_CONFIGS['small_v1']
|
||||
found = self._render_and_read(network_config=yaml.load(entry['yaml']),
|
||||
dir=render_dir)
|
||||
self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
@@ -3644,7 +3807,6 @@ USERCTL=no
|
||||
mock.Mock(return_value=False)
|
||||
)
|
||||
class TestOpenSuseSysConfigRendering(CiTestCase):
|
||||
-
|
||||
with_logs = True
|
||||
|
||||
scripts_dir = '/etc/sysconfig/network'
|
||||
@@ -3916,8 +4078,14 @@ STARTMODE=auto
|
||||
'WARNING: Network config: ignoring eth0.101 device-level mtu',
|
||||
self.logs.getvalue())
|
||||
|
||||
- def test_small_config(self):
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ def test_small_config_v1(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v1"]
|
||||
+ found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
|
||||
+ self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
+ self._assert_headers(found)
|
||||
+
|
||||
+ def test_small_config_v2(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v1"]
|
||||
found = self._render_and_read(network_config=yaml.load(entry['yaml']))
|
||||
self._compare_files_to_expected(entry[self.expected_name], found)
|
||||
self._assert_headers(found)
|
||||
@@ -4539,7 +4707,6 @@ class TestReadInitramfsConfig(CiTestCase):
|
||||
|
||||
|
||||
class TestNetplanRoundTrip(CiTestCase):
|
||||
-
|
||||
NETPLAN_INFO_OUT = textwrap.dedent("""
|
||||
netplan.io:
|
||||
features:
|
||||
@@ -4596,7 +4763,7 @@ class TestNetplanRoundTrip(CiTestCase):
|
||||
files['/etc/netplan/50-cloud-init.yaml'].splitlines())
|
||||
|
||||
def testsimple_render_small_netplan(self):
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ entry = NETWORK_CONFIGS['small_v1']
|
||||
files = self._render_and_read(network_config=yaml.load(entry['yaml']))
|
||||
self.assertEqual(
|
||||
entry['expected_netplan'].splitlines(),
|
||||
@@ -4781,8 +4948,17 @@ class TestEniRoundTrip(CiTestCase):
|
||||
entry['expected_eni'].splitlines(),
|
||||
files['/etc/network/interfaces'].splitlines())
|
||||
|
||||
- def testsimple_render_small(self):
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ def testsimple_render_small_v1(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v1"]
|
||||
+ files = self._render_and_read(network_config=yaml.load(entry["yaml"]))
|
||||
+ self.assertEqual(
|
||||
+ entry["expected_eni"].splitlines(),
|
||||
+ files["/etc/network/interfaces"].splitlines(),
|
||||
+ )
|
||||
+
|
||||
+ @pytest.mark.xfail(reason="GH-4219")
|
||||
+ def testsimple_render_small_v2(self):
|
||||
+ entry = NETWORK_CONFIGS["small_v2"]
|
||||
files = self._render_and_read(network_config=yaml.load(entry['yaml']))
|
||||
self.assertEqual(
|
||||
entry['expected_eni'].splitlines(),
|
||||
@@ -5105,10 +5281,33 @@ class TestNetworkdRoundTrip(CiTestCase):
|
||||
return dir2dict(dir)
|
||||
|
||||
@mock.patch("cloudinit.net.util.chownbyname", return_value=True)
|
||||
- def testsimple_render_small_networkd(self, m_chown):
|
||||
+ def testsimple_render_small_networkd_v1(self, m_chown):
|
||||
+ nwk_fn1 = "/etc/systemd/network/10-cloud-init-eth99.network"
|
||||
+ nwk_fn2 = "/etc/systemd/network/10-cloud-init-eth1.network"
|
||||
+ entry = NETWORK_CONFIGS["small_v1"]
|
||||
+ files = self._render_and_read(network_config=yaml.load(entry["yaml"]))
|
||||
+
|
||||
+ actual = files[nwk_fn1].splitlines()
|
||||
+ actual = self.create_conf_dict(actual)
|
||||
+
|
||||
+ expected = entry["expected_networkd_eth99"].splitlines()
|
||||
+ expected = self.create_conf_dict(expected)
|
||||
+
|
||||
+ self.compare_dicts(actual, expected)
|
||||
+
|
||||
+ actual = files[nwk_fn2].splitlines()
|
||||
+ actual = self.create_conf_dict(actual)
|
||||
+
|
||||
+ expected = entry["expected_networkd_eth1"].splitlines()
|
||||
+ expected = self.create_conf_dict(expected)
|
||||
+
|
||||
+ self.compare_dicts(actual, expected)
|
||||
+
|
||||
+ @mock.patch("cloudinit.net.util.chownbyname", return_value=True)
|
||||
+ def testsimple_render_small_networkd_v2(self, m_chown):
|
||||
nwk_fn1 = '/etc/systemd/network/10-cloud-init-eth99.network'
|
||||
nwk_fn2 = '/etc/systemd/network/10-cloud-init-eth1.network'
|
||||
- entry = NETWORK_CONFIGS['small']
|
||||
+ entry = NETWORK_CONFIGS['small_v2']
|
||||
files = self._render_and_read(network_config=yaml.load(entry['yaml']))
|
||||
|
||||
actual = files[nwk_fn1].splitlines()
|
||||
@@ -5735,7 +5934,6 @@ class TestInterfacesSorting(CiTestCase):
|
||||
mock.Mock(return_value=False)
|
||||
)
|
||||
class TestGetIBHwaddrsByInterface(CiTestCase):
|
||||
-
|
||||
_ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56'
|
||||
_ib_addr_eth_format = '00:11:22:33:44:56'
|
||||
_data = {'devices': ['enp0s1', 'enp0s2', 'bond1', 'bridge1',
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
From 3634678465e7b8f8608bcb9a1f5773ae7837cbe9 Mon Sep 17 00:00:00 2001
|
||||
From: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com>
|
||||
Date: Fri, 17 Mar 2023 03:01:22 +0530
|
||||
Subject: [PATCH] Handle non existent ca-cert-config situation (#2073)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/3634678465e7b8f8608bcb9a1f5773ae7837cbe9
|
||||
Conflict:only check cert file
|
||||
|
||||
Currently if a cert file doesn't exist, cc_ca_certs module crashes
|
||||
This fix makes it possible to handle it gracefully.
|
||||
|
||||
Also, out_lines variable may not be available if os.stat returns 0.
|
||||
This issue is also taken care of.
|
||||
|
||||
Added tests for the same.
|
||||
---
|
||||
cloudinit/config/cc_ca_certs.py | 3 ++-
|
||||
1 file changed, 2 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py
|
||||
index bd7bead..2b3210b 100644
|
||||
--- a/cloudinit/config/cc_ca_certs.py
|
||||
+++ b/cloudinit/config/cc_ca_certs.py
|
||||
@@ -110,7 +110,8 @@ def update_cert_config(distro_cfg):
|
||||
|
||||
@param distro_cfg: A hash providing _distro_ca_certs_configs function.
|
||||
"""
|
||||
- if distro_cfg['ca_cert_config'] is None:
|
||||
+ ca_cert_cfg_fn = distro_cfg["ca_cert_config"]
|
||||
+ if not ca_cert_cfg_fn or not os.path.exists(ca_cert_cfg_fn):
|
||||
return
|
||||
if os.stat(distro_cfg['ca_cert_config']).st_size == 0:
|
||||
# If the CA_CERT_CONFIG file is empty (i.e. all existing
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
From 4610833d1e9a0839321f84bbc3c8d27ff19a17f2 Mon Sep 17 00:00:00 2001
|
||||
From: James Falcon <james.falcon@canonical.com>
|
||||
Date: Thu, 2 Feb 2023 10:13:08 -0600
|
||||
Subject: [PATCH] Ignore duplicate macs from mscc_felix and fsl_enetc
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/4610833d1e9a0839321f84bbc3c8d27ff19a17f2
|
||||
Conflict:import does_not_raise diffs.
|
||||
|
||||
mscc_felix and fsl_enetc are drivers representing a switch that is
|
||||
expected to have duplicate macs. If we encounter either of these
|
||||
drivers, we should not raise the duplicate mac exception.
|
||||
|
||||
LP: #1997922
|
||||
---
|
||||
cloudinit/net/__init__.py | 16 ++++++++++++++++
|
||||
tests/unittests/test_net.py | 20 ++++++++++++++++++++
|
||||
2 files changed, 36 insertions(+)
|
||||
|
||||
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
||||
index 96ce6f5..a308c98 100644
|
||||
--- a/cloudinit/net/__init__.py
|
||||
+++ b/cloudinit/net/__init__.py
|
||||
@@ -929,6 +929,22 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict:
|
||||
% (ret[mac], driver_map[mac], name)
|
||||
)
|
||||
|
||||
+ # This is intended to be a short-term fix of LP: #1997922
|
||||
+ # Long term, we should better handle configuration of virtual
|
||||
+ # devices where duplicate MACs are expected early in boot if
|
||||
+ # cloud-init happens to enumerate network interfaces before drivers
|
||||
+ # have fully initialized the leader/subordinate relationships for
|
||||
+ # those devices or switches.
|
||||
+ if driver == "mscc_felix" or driver == "fsl_enetc":
|
||||
+ LOG.debug(
|
||||
+ "Ignoring duplicate macs from '%s' and '%s' due to "
|
||||
+ "driver '%s'.",
|
||||
+ name,
|
||||
+ ret[mac],
|
||||
+ driver,
|
||||
+ )
|
||||
+ continue
|
||||
+
|
||||
if raise_duplicate_mac_error:
|
||||
raise RuntimeError(msg)
|
||||
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index 0db4442..e5dd979 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -11,6 +11,7 @@ from cloudinit import temp_utils
|
||||
from cloudinit import subp
|
||||
from cloudinit import util
|
||||
from cloudinit import safeyaml as yaml
|
||||
+from contextlib import ExitStack as does_not_raise
|
||||
|
||||
from cloudinit.tests.helpers import (
|
||||
CiTestCase, FilesystemMockingTestCase, dir2dict, mock, populate_dir)
|
||||
@@ -5515,6 +5516,7 @@ class TestInterfaceHasOwnMac(CiTestCase):
|
||||
mock.Mock(return_value=False)
|
||||
)
|
||||
class TestGetInterfacesByMac(CiTestCase):
|
||||
+ with_logs = True
|
||||
_data = {'bonds': ['bond1'],
|
||||
'bridges': ['bridge1'],
|
||||
'vlans': ['bond1.101'],
|
||||
@@ -5680,6 +5682,24 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
ib_addr_eth_format: 'ib0', ib_addr: 'ib0'}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
+ def test_duplicate_ignored_macs(self):
|
||||
+ # LP: #199792
|
||||
+ self._data = copy.deepcopy(self._data)
|
||||
+ self._data["macs"]["swp0"] = "9a:57:7d:78:47:c0"
|
||||
+ self._data["macs"]["swp1"] = "9a:57:7d:78:47:c0"
|
||||
+ self._data["own_macs"].append("swp0")
|
||||
+ self._data["own_macs"].append("swp1")
|
||||
+ self._data["drivers"]["swp0"] = "mscc_felix"
|
||||
+ self._data["drivers"]["swp1"] = "mscc_felix"
|
||||
+ self._mock_setup()
|
||||
+ with does_not_raise():
|
||||
+ net.get_interfaces_by_mac()
|
||||
+ pattern = (
|
||||
+ "Ignoring duplicate macs from 'swp[0-1]' and 'swp[0-1]' due to "
|
||||
+ "driver 'mscc_felix'."
|
||||
+ )
|
||||
+ assert re.search(pattern, self.logs.getvalue())
|
||||
+
|
||||
|
||||
class TestInterfacesSorting(CiTestCase):
|
||||
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
From 15a6e0868097ec8a6ef97b9fde59a9486270fc37 Mon Sep 17 00:00:00 2001
|
||||
From: Jack <jack4zhang@gmail.com>
|
||||
Date: Tue, 21 Feb 2023 23:39:43 +0800
|
||||
Subject: [PATCH] Set ownership for new folders in Write Files Module
|
||||
(#1980)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/15a6e0868097ec8a6ef97b9fde59a9486270fc37
|
||||
Conflict:(1)cloudinit/config/schemas/schema-cloud-config-v1.json not
|
||||
change.
|
||||
(2)tools/.github-cla-signers not change.
|
||||
(3)tests/integration_tests/modules/test_write_files.py not change.
|
||||
Integration tests are current Ubuntu-only.
|
||||
https://github.com/canonical/cloud-init/issues/4290#issuecomment-1660200921
|
||||
|
||||
The parent directory would be created automatically if it does not
|
||||
exist. But the ownership of newly-created parent
|
||||
directory would always be root.
|
||||
|
||||
With this change, it would be set the same as .
|
||||
|
||||
LP: #1990513
|
||||
---
|
||||
cloudinit/config/cc_write_files.py | 4 +++-
|
||||
cloudinit/util.py | 38 ++++++++++++++++++++++++++++--
|
||||
tests/unittests/test_util.py | 20 ++++++++++++++++
|
||||
3 files changed, 59 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py
|
||||
index b1678b8..6edaa85 100644
|
||||
--- a/cloudinit/config/cc_write_files.py
|
||||
+++ b/cloudinit/config/cc_write_files.py
|
||||
@@ -241,7 +241,9 @@ def write_files(name, files):
|
||||
(u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER))
|
||||
perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS)
|
||||
omode = 'ab' if util.get_cfg_option_bool(f_info, 'append') else 'wb'
|
||||
- util.write_file(path, contents, omode=omode, mode=perms)
|
||||
+ util.write_file(
|
||||
+ path, contents, omode=omode, mode=perms, user=u, group=g
|
||||
+ )
|
||||
util.chownbyname(path, u, g)
|
||||
|
||||
|
||||
diff --git a/cloudinit/util.py b/cloudinit/util.py
|
||||
index 2b8adf3..d4a6eed 100644
|
||||
--- a/cloudinit/util.py
|
||||
+++ b/cloudinit/util.py
|
||||
@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode
|
||||
from errno import ENOENT
|
||||
from collections import namedtuple
|
||||
from functools import lru_cache, total_ordering
|
||||
+from pathlib import Path
|
||||
from urllib import parse
|
||||
from typing import List
|
||||
|
||||
@@ -1648,12 +1649,41 @@ def json_dumps(data):
|
||||
raise
|
||||
|
||||
|
||||
-def ensure_dir(path, mode=None):
|
||||
+def get_non_exist_parent_dir(path):
|
||||
+ """Get the last directory in a path that does not exist.
|
||||
+
|
||||
+ Example: when path=/usr/a/b and /usr/a does not exis but /usr does,
|
||||
+ return /usr/a
|
||||
+ """
|
||||
+ p_path = os.path.dirname(path)
|
||||
+ # Check if parent directory of path is root
|
||||
+ if p_path == os.path.dirname(p_path):
|
||||
+ return path
|
||||
+ else:
|
||||
+ if os.path.isdir(p_path):
|
||||
+ return path
|
||||
+ else:
|
||||
+ return get_non_exist_parent_dir(p_path)
|
||||
+
|
||||
+
|
||||
+def ensure_dir(path, mode=None, user=None, group=None):
|
||||
if not os.path.isdir(path):
|
||||
+ # Get non existed parent dir first before they are created.
|
||||
+ non_existed_parent_dir = get_non_exist_parent_dir(path)
|
||||
# Make the dir and adjust the mode
|
||||
with SeLinuxGuard(os.path.dirname(path), recursive=True):
|
||||
os.makedirs(path)
|
||||
chmod(path, mode)
|
||||
+ # Change the ownership
|
||||
+ if user or group:
|
||||
+ chownbyname(non_existed_parent_dir, user, group)
|
||||
+ # if path=/usr/a/b/c and non_existed_parent_dir=/usr,
|
||||
+ # then sub_relative_dir=PosixPath('a/b/c')
|
||||
+ sub_relative_dir = Path(path.split(non_existed_parent_dir)[1][1:])
|
||||
+ sub_path = Path(non_existed_parent_dir)
|
||||
+ for part in sub_relative_dir.parts:
|
||||
+ sub_path = sub_path.joinpath(part)
|
||||
+ chownbyname(sub_path, user, group)
|
||||
else:
|
||||
# Just adjust the mode
|
||||
chmod(path, mode)
|
||||
@@ -1972,6 +2002,8 @@ def write_file(
|
||||
preserve_mode=False,
|
||||
*,
|
||||
ensure_dir_exists=True,
|
||||
+ user=None,
|
||||
+ group=None,
|
||||
):
|
||||
"""
|
||||
Writes a file with the given content and sets the file mode as specified.
|
||||
@@ -1986,6 +2018,8 @@ def write_file(
|
||||
@param ensure_dir_exists: If True (the default), ensure that the directory
|
||||
containing `filename` exists before writing to
|
||||
the file.
|
||||
+ @param user: The user to set on the file.
|
||||
+ @param group: The group to set on the file.
|
||||
"""
|
||||
|
||||
if preserve_mode:
|
||||
@@ -1995,7 +2029,7 @@ def write_file(
|
||||
pass
|
||||
|
||||
if ensure_dir_exists:
|
||||
- ensure_dir(os.path.dirname(filename))
|
||||
+ ensure_dir(os.path.dirname(filename), user=user, group=group)
|
||||
if 'b' in omode.lower():
|
||||
content = encode_text(content)
|
||||
write_type = 'bytes'
|
||||
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
|
||||
index 3fa5059..2ab3bad 100644
|
||||
--- a/tests/unittests/test_util.py
|
||||
+++ b/tests/unittests/test_util.py
|
||||
@@ -10,6 +10,7 @@ import tempfile
|
||||
import pytest
|
||||
import yaml
|
||||
from unittest import mock
|
||||
+from pathlib import Path
|
||||
|
||||
from cloudinit import subp
|
||||
from cloudinit import importer, util
|
||||
@@ -99,6 +100,25 @@ class TestWriteFile(helpers.TestCase):
|
||||
self.assertTrue(os.path.isdir(dirname))
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
+ def test_dir_ownership(self):
|
||||
+ """Verifiy that directories is created with appropriate ownership."""
|
||||
+ dirname = os.path.join(self.tmp, "subdir", "subdir2")
|
||||
+ path = os.path.join(dirname, "NewFile.txt")
|
||||
+ contents = "Hey there"
|
||||
+ user = "foo"
|
||||
+ group = "foo"
|
||||
+
|
||||
+ with mock.patch.object(
|
||||
+ util, "chownbyname", return_value=None
|
||||
+ ) as mockobj:
|
||||
+ util.write_file(path, contents, user=user, group=group)
|
||||
+
|
||||
+ calls = [
|
||||
+ mock.call(os.path.join(self.tmp, "subdir"), user, group),
|
||||
+ mock.call(Path(dirname), user, group),
|
||||
+ ]
|
||||
+ mockobj.assert_has_calls(calls, any_order=False)
|
||||
+
|
||||
def test_dir_is_not_created_if_ensure_dir_false(self):
|
||||
"""Verify directories are not created if ensure_dir_exists is False."""
|
||||
dirname = os.path.join(self.tmp, "subdir")
|
||||
--
|
||||
2.27.0
|
||||
|
||||
142
backport-cc_ubuntu_advantage-Redact-token-from-logs-1726.patch
Normal file
142
backport-cc_ubuntu_advantage-Redact-token-from-logs-1726.patch
Normal file
@ -0,0 +1,142 @@
|
||||
From 3a32188398675793d87f19363af0497035ea9263 Mon Sep 17 00:00:00 2001
|
||||
From: Alberto Contreras <alberto.contreras@canonical.com>
|
||||
Date: Thu, 15 Sep 2022 22:19:38 +0200
|
||||
Subject: [PATCH] cc_ubuntu_advantage: Redact token from logs (#1726)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/3a32188398675793d87f19363af0497035ea9263
|
||||
Conflict:test diffs.
|
||||
---
|
||||
cloudinit/config/cc_ubuntu_advantage.py | 11 ++++----
|
||||
.../config/tests/test_ubuntu_advantage.py | 26 +++++++++----------
|
||||
2 files changed, 19 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py
|
||||
index e55772a..033d4d2 100644
|
||||
--- a/cloudinit/config/cc_ubuntu_advantage.py
|
||||
+++ b/cloudinit/config/cc_ubuntu_advantage.py
|
||||
@@ -85,7 +85,7 @@ schema = {
|
||||
__doc__ = get_schema_doc(schema) # Supplement python help()
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
-
|
||||
+REDACTED = "REDACTED"
|
||||
|
||||
def configure_ua(token=None, enable=None):
|
||||
"""Call ua commandline client to attach or enable services."""
|
||||
@@ -108,12 +108,13 @@ def configure_ua(token=None, enable=None):
|
||||
enable = []
|
||||
|
||||
attach_cmd = ['ua', 'attach', token]
|
||||
- LOG.debug('Attaching to Ubuntu Advantage. %s', ' '.join(attach_cmd))
|
||||
+ redacted_cmd = attach_cmd[:-1] + [REDACTED]
|
||||
+ LOG.debug("Attaching to Ubuntu Advantage. %s", " ".join(redacted_cmd))
|
||||
try:
|
||||
- subp.subp(attach_cmd)
|
||||
+ subp.subp(attach_cmd, logstring=redacted_cmd)
|
||||
except subp.ProcessExecutionError as e:
|
||||
- msg = 'Failure attaching Ubuntu Advantage:\n{error}'.format(
|
||||
- error=str(e))
|
||||
+ error = str(e).replace(token, REDACTED)
|
||||
+ msg = f"Failure attaching Ubuntu Advantage:\n{error}"
|
||||
util.logexc(LOG, msg)
|
||||
raise RuntimeError(msg) from e
|
||||
enable_errors = []
|
||||
diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py
|
||||
index db7fb72..eadeddb 100644
|
||||
--- a/cloudinit/config/tests/test_ubuntu_advantage.py
|
||||
+++ b/cloudinit/config/tests/test_ubuntu_advantage.py
|
||||
@@ -36,23 +36,23 @@ class TestConfigureUA(CiTestCase):
|
||||
self.assertEqual(
|
||||
'Failure attaching Ubuntu Advantage:\nUnexpected error while'
|
||||
' running command.\nCommand: -\nExit code: -\nReason: -\n'
|
||||
- 'Stdout: Invalid token SomeToken\nStderr: -',
|
||||
+ 'Stdout: Invalid token REDACTED\nStderr: -',
|
||||
str(context_manager.exception))
|
||||
|
||||
@mock.patch('%s.subp.subp' % MPATH)
|
||||
def test_configure_ua_attach_with_token(self, m_subp):
|
||||
"""When token is provided, attach the machine to ua using the token."""
|
||||
configure_ua(token='SomeToken')
|
||||
- m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'])
|
||||
+ m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],)
|
||||
self.assertEqual(
|
||||
- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
|
||||
+ 'DEBUG: Attaching to Ubuntu Advantage. ua attach REDACTED\n',
|
||||
self.logs.getvalue())
|
||||
|
||||
@mock.patch('%s.subp.subp' % MPATH)
|
||||
def test_configure_ua_attach_on_service_error(self, m_subp):
|
||||
"""all services should be enabled and then any failures raised"""
|
||||
|
||||
- def fake_subp(cmd, capture=None):
|
||||
+ def fake_subp(cmd, capture=None, logstring=None):
|
||||
fail_cmds = [['ua', 'enable', svc] for svc in ['esm', 'cc']]
|
||||
if cmd in fail_cmds and capture:
|
||||
svc = cmd[-1]
|
||||
@@ -65,7 +65,7 @@ class TestConfigureUA(CiTestCase):
|
||||
configure_ua(token='SomeToken', enable=['esm', 'cc', 'fips'])
|
||||
self.assertEqual(
|
||||
m_subp.call_args_list,
|
||||
- [mock.call(['ua', 'attach', 'SomeToken']),
|
||||
+ [mock.call(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],),
|
||||
mock.call(['ua', 'enable', 'esm'], capture=True),
|
||||
mock.call(['ua', 'enable', 'cc'], capture=True),
|
||||
mock.call(['ua', 'enable', 'fips'], capture=True)])
|
||||
@@ -87,9 +87,9 @@ class TestConfigureUA(CiTestCase):
|
||||
def test_configure_ua_attach_with_empty_services(self, m_subp):
|
||||
"""When services is an empty list, do not auto-enable attach."""
|
||||
configure_ua(token='SomeToken', enable=[])
|
||||
- m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'])
|
||||
+ m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],)
|
||||
self.assertEqual(
|
||||
- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
|
||||
+ 'DEBUG: Attaching to Ubuntu Advantage. ua attach REDACTED\n',
|
||||
self.logs.getvalue())
|
||||
|
||||
@mock.patch('%s.subp.subp' % MPATH)
|
||||
@@ -98,10 +98,10 @@ class TestConfigureUA(CiTestCase):
|
||||
configure_ua(token='SomeToken', enable=['fips'])
|
||||
self.assertEqual(
|
||||
m_subp.call_args_list,
|
||||
- [mock.call(['ua', 'attach', 'SomeToken']),
|
||||
+ [mock.call(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],),
|
||||
mock.call(['ua', 'enable', 'fips'], capture=True)])
|
||||
self.assertEqual(
|
||||
- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
|
||||
+ 'DEBUG: Attaching to Ubuntu Advantage. ua attach REDACTED\n',
|
||||
self.logs.getvalue())
|
||||
|
||||
@mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock())
|
||||
@@ -111,12 +111,12 @@ class TestConfigureUA(CiTestCase):
|
||||
configure_ua(token='SomeToken', enable='fips')
|
||||
self.assertEqual(
|
||||
m_subp.call_args_list,
|
||||
- [mock.call(['ua', 'attach', 'SomeToken']),
|
||||
+ [mock.call(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],),
|
||||
mock.call(['ua', 'enable', 'fips'], capture=True)])
|
||||
self.assertEqual(
|
||||
'WARNING: ubuntu_advantage: enable should be a list, not a'
|
||||
' string; treating as a single enable\n'
|
||||
- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
|
||||
+ 'DEBUG: Attaching to Ubuntu Advantage. ua attach REDACTED\n',
|
||||
self.logs.getvalue())
|
||||
|
||||
@mock.patch('%s.subp.subp' % MPATH)
|
||||
@@ -125,11 +125,11 @@ class TestConfigureUA(CiTestCase):
|
||||
configure_ua(token='SomeToken', enable={'deffo': 'wont work'})
|
||||
self.assertEqual(
|
||||
m_subp.call_args_list,
|
||||
- [mock.call(['ua', 'attach', 'SomeToken'])])
|
||||
+ [mock.call(['ua', 'attach', 'SomeToken'], logstring=["ua", "attach", "REDACTED"],)])
|
||||
self.assertEqual(
|
||||
'WARNING: ubuntu_advantage: enable should be a list, not a'
|
||||
' dict; skipping enabling services\n'
|
||||
- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
|
||||
+ 'DEBUG: Attaching to Ubuntu Advantage. ua attach REDACTED\n',
|
||||
self.logs.getvalue())
|
||||
|
||||
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
From ceb66d4552e6c4c0da60cb08fdf208fb90c34660 Mon Sep 17 00:00:00 2001
|
||||
From: Chad Smith <chad.smith@canonical.com>
|
||||
Date: Thu, 23 Mar 2023 06:08:12 -0600
|
||||
Subject: [PATCH] macs: ignore duplicate MAC for devs with driver driver
|
||||
qmi_wwan (#2090)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/ceb66d4552e6c4c0da60cb08fdf208fb90c34660
|
||||
Conflict:NA
|
||||
|
||||
Another physical modem which has duplicate MAC addresses.
|
||||
Cloud-init needs to ignore the subordinate devices which are
|
||||
associated with the qmi_wwan driver.
|
||||
|
||||
Fixes network rendering for the following modems:
|
||||
Quectel EG25
|
||||
Quectel RM510Q-GLHA
|
||||
Sierra Wireless MC7455
|
||||
|
||||
LP: #2008888
|
||||
---
|
||||
cloudinit/net/__init__.py | 2 +-
|
||||
tests/unittests/test_net.py | 40 ++++++++++++++++++++++++++-----------
|
||||
2 files changed, 29 insertions(+), 13 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
||||
index a308c98..a503210 100644
|
||||
--- a/cloudinit/net/__init__.py
|
||||
+++ b/cloudinit/net/__init__.py
|
||||
@@ -935,7 +935,7 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict:
|
||||
# cloud-init happens to enumerate network interfaces before drivers
|
||||
# have fully initialized the leader/subordinate relationships for
|
||||
# those devices or switches.
|
||||
- if driver == "mscc_felix" or driver == "fsl_enetc":
|
||||
+ if driver in ("fsl_enetc", "mscc_felix", "qmi_wwan"):
|
||||
LOG.debug(
|
||||
"Ignoring duplicate macs from '%s' and '%s' due to "
|
||||
"driver '%s'.",
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index e5dd979..7cde102 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -5682,23 +5682,39 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
ib_addr_eth_format: 'ib0', ib_addr: 'ib0'}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
- def test_duplicate_ignored_macs(self):
|
||||
- # LP: #199792
|
||||
- self._data = copy.deepcopy(self._data)
|
||||
- self._data["macs"]["swp0"] = "9a:57:7d:78:47:c0"
|
||||
- self._data["macs"]["swp1"] = "9a:57:7d:78:47:c0"
|
||||
- self._data["own_macs"].append("swp0")
|
||||
- self._data["own_macs"].append("swp1")
|
||||
- self._data["drivers"]["swp0"] = "mscc_felix"
|
||||
- self._data["drivers"]["swp1"] = "mscc_felix"
|
||||
- self._mock_setup()
|
||||
+
|
||||
+@pytest.mark.parametrize("driver", ("mscc_felix", "fsl_enetc", "qmi_wwan"))
|
||||
+@mock.patch("cloudinit.net.get_sys_class_path")
|
||||
+@mock.patch("cloudinit.util.system_info", return_value={"variant": "ubuntu"})
|
||||
+class TestDuplicateMac:
|
||||
+ def test_duplicate_ignored_macs(
|
||||
+ self, _get_system_info, get_sys_class_path, driver, tmpdir, caplog
|
||||
+ ):
|
||||
+ # Create sysfs representation of network devices and drivers in tmpdir
|
||||
+ sys_net_path = tmpdir.join("class/net")
|
||||
+ get_sys_class_path.return_value = sys_net_path.strpath + "/"
|
||||
+ net_data = {
|
||||
+ "swp0/address": "9a:57:7d:78:47:c0",
|
||||
+ "swp0/addr_assign_type": "0",
|
||||
+ "swp0/device/dev_id": "something",
|
||||
+ "swp1/address": "9a:57:7d:78:47:c0",
|
||||
+ "swp1/addr_assign_type": "0",
|
||||
+ "swp1/device/dev_id": "something else",
|
||||
+ }
|
||||
+ populate_dir(sys_net_path.strpath, net_data)
|
||||
+ # Symlink for device driver
|
||||
+ driver_path = tmpdir.join(f"module/{driver}")
|
||||
+ driver_path.ensure_dir()
|
||||
+ sys_net_path.join("swp0/device/driver").mksymlinkto(driver_path)
|
||||
+ sys_net_path.join("swp1/device/driver").mksymlinkto(driver_path)
|
||||
+
|
||||
with does_not_raise():
|
||||
net.get_interfaces_by_mac()
|
||||
pattern = (
|
||||
"Ignoring duplicate macs from 'swp[0-1]' and 'swp[0-1]' due to "
|
||||
- "driver 'mscc_felix'."
|
||||
+ f"driver '{driver}'."
|
||||
)
|
||||
- assert re.search(pattern, self.logs.getvalue())
|
||||
+ assert re.search(pattern, caplog.text)
|
||||
|
||||
|
||||
class TestInterfacesSorting(CiTestCase):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
From c82ace920a743c6e6797536416018d9680b8fa7e Mon Sep 17 00:00:00 2001
|
||||
From: Chris Patterson <cpatterson@microsoft.com>
|
||||
Date: Wed, 29 Mar 2023 17:30:13 -0400
|
||||
Subject: [PATCH] net/dhcp: catch dhclient failures and raise NoDHCPLeaseError
|
||||
(#2083)
|
||||
|
||||
Some variants of dhclient will exit with non-zero codes on lease
|
||||
failure. For example, on RHEL 8.7:
|
||||
```
|
||||
[cpatterson@test-rhel87 ~]$ sudo /usr/sbin/dhclient -1 -v -lf
|
||||
/tmp/my.lease -pf /tmp/my.pid bridge2nowhere -sf /bin/
|
||||
true
|
||||
Internet Systems Consortium DHCP Client 4.3.6
|
||||
Copyright 2004-2017 Internet Systems Consortium.
|
||||
All rights reserved.
|
||||
For info, please visit https://www.isc.org/software/dhcp/
|
||||
|
||||
Listening on LPF/bridge2nowhere/42:ef:d5:38:1d:19
|
||||
Sending on LPF/bridge2nowhere/42:ef:d5:38:1d:19
|
||||
Sending on Socket/fallback
|
||||
Created duid "\000\004E<\225X\232\304J\337\243\026T\324\243O\270\177".
|
||||
DHCPDISCOVER on bridge2nowhere to 255.255.255.255 port 67 interval 4
|
||||
(xid=0x777bc142)
|
||||
DHCPDISCOVER on bridge2nowhere to 255.255.255.255 port 67 interval 7
|
||||
(xid=0x777bc142)
|
||||
DHCPDISCOVER on bridge2nowhere to 255.255.255.255 port 67 interval 13
|
||||
(xid=0x777bc142)
|
||||
DHCPDISCOVER on bridge2nowhere to 255.255.255.255 port 67 interval 6
|
||||
(xid=0x777bc142)
|
||||
No DHCPOFFERS received.
|
||||
Unable to obtain a lease on first try. Exiting.
|
||||
|
||||
[cpatterson@test-rhel87 ~]$ echo $?
|
||||
2
|
||||
```
|
||||
|
||||
This results in an unhandled subp.ProcessExecutionError exception.
|
||||
Catch these failures and re-raise as NoDHCPLeaseError.
|
||||
|
||||
Signed-off-by: Chris Patterson <cpatterson@microsoft.com>
|
||||
---
|
||||
cloudinit/net/dhcp.py | 11 ++++++++++-
|
||||
cloudinit/net/tests/test_dhcp.py | 23 +++++++++++++++++++++--
|
||||
2 files changed, 31 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
|
||||
index 3f4b041..e5f36e1 100644
|
||||
--- a/cloudinit/net/dhcp.py
|
||||
+++ b/cloudinit/net/dhcp.py
|
||||
@@ -239,7 +239,16 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None):
|
||||
subp.subp(['ip', 'link', 'set', 'dev', interface, 'up'], capture=True)
|
||||
cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,
|
||||
'-pf', pid_file, interface, '-sf', '/bin/true']
|
||||
- out, err = subp.subp(cmd, capture=True)
|
||||
+ try:
|
||||
+ out, err = subp.subp(cmd, capture=True)
|
||||
+ except subp.ProcessExecutionError as error:
|
||||
+ LOG.debug(
|
||||
+ "dhclient exited with code: %s stderr: %r stdout: %r",
|
||||
+ error.exit_code,
|
||||
+ error.stderr,
|
||||
+ error.stdout,
|
||||
+ )
|
||||
+ raise NoDHCPLeaseError from error
|
||||
|
||||
# Wait for pid file and lease file to appear, and for the process
|
||||
# named by the pid file to daemonize (have pid 1 as its parent). If we
|
||||
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
|
||||
index 28b4ecf..de4b461 100644
|
||||
--- a/cloudinit/net/tests/test_dhcp.py
|
||||
+++ b/cloudinit/net/tests/test_dhcp.py
|
||||
@@ -3,14 +3,15 @@
|
||||
import httpretty
|
||||
import os
|
||||
import signal
|
||||
+import pytest
|
||||
from textwrap import dedent
|
||||
|
||||
import cloudinit.net as net
|
||||
from cloudinit.net.dhcp import (
|
||||
- InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
|
||||
+ InvalidDHCPLeaseFileError, NoDHCPLeaseError, maybe_perform_dhcp_discovery,
|
||||
parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases,
|
||||
parse_static_routes)
|
||||
-from cloudinit.util import ensure_file, write_file
|
||||
+from cloudinit.util import ensure_file, subp, write_file
|
||||
from cloudinit.tests.helpers import (
|
||||
CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
|
||||
|
||||
@@ -268,6 +269,24 @@ class TestDHCPDiscoveryClean(CiTestCase):
|
||||
'Skip dhcp_discovery: Unable to find fallback nic.',
|
||||
self.logs.getvalue())
|
||||
|
||||
+ @mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
|
||||
+ @mock.patch("cloudinit.net.dhcp.os.remove")
|
||||
+ @mock.patch("cloudinit.net.dhcp.subp.subp")
|
||||
+ def test_dhclient_exits_with_error(self, m_subp, m_remove, m_fallback):
|
||||
+ """Log and do nothing when nic is absent and no fallback is found."""
|
||||
+ m_subp.side_effect = [
|
||||
+ ("", ""),
|
||||
+ subp.ProcessExecutionError(exit_code=-5),
|
||||
+ ]
|
||||
+
|
||||
+ with pytest.raises(NoDHCPLeaseError):
|
||||
+ maybe_perform_dhcp_discovery()
|
||||
+
|
||||
+ self.assertIn(
|
||||
+ "dhclient exited with code: -5",
|
||||
+ self.logs.getvalue(),
|
||||
+ )
|
||||
+
|
||||
def test_provided_nic_does_not_exist(self):
|
||||
"""When the provided nic doesn't exist, log a message and no-op."""
|
||||
self.assertEqual([], maybe_perform_dhcp_discovery('idontexist'))
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
From 24bf6147712655fc36a5d714a081853ea37e0312 Mon Sep 17 00:00:00 2001
|
||||
From: Anh Vo <anhvo@microsoft.com>
|
||||
Date: Fri, 18 Nov 2022 14:31:27 -0500
|
||||
Subject: [PATCH] net: skip duplicate mac check for netvsc nic and its VF
|
||||
(#1853)
|
||||
|
||||
Reference:https://github.com/canonical/cloud-init/commit/24bf6147712655fc36a5d714a081853ea37e0312
|
||||
Conflict:format diff.
|
||||
|
||||
When accelerated network is enabled on Azure, the host presents
|
||||
two network interfaces with the same mac address to the VM:
|
||||
a synthetic nic (netvsc) and a VF nic, which is enslaved to the synthetic
|
||||
nic.
|
||||
|
||||
The net module is already excluding slave nics when enumerating
|
||||
interfaces. However, if cloud-init starts enumerating after the kernel
|
||||
makes the VF visible to userspace, but before the enslaving has finished,
|
||||
cloud-init will see two nics with duplicate mac.
|
||||
|
||||
This change will skip the duplicate mac error if one of the two nics
|
||||
with duplicate mac is a netvsc nic
|
||||
|
||||
LP: #1844191
|
||||
---
|
||||
cloudinit/net/__init__.py | 39 +++++++++++++++++++++++++++++++++----
|
||||
tests/unittests/test_net.py | 32 ++++++++++++++++++++++++++----
|
||||
2 files changed, 63 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
||||
index fba133e..96ce6f5 100644
|
||||
--- a/cloudinit/net/__init__.py
|
||||
+++ b/cloudinit/net/__init__.py
|
||||
@@ -896,13 +896,44 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict:
|
||||
|
||||
Bridges and any devices that have a 'stolen' mac are excluded."""
|
||||
ret = {}
|
||||
- for name, mac, _driver, _devid in get_interfaces(
|
||||
+ driver_map: dict = {}
|
||||
+ for name, mac, driver, _devid in get_interfaces(
|
||||
blacklist_drivers=blacklist_drivers):
|
||||
if mac in ret:
|
||||
- raise RuntimeError(
|
||||
- "duplicate mac found! both '%s' and '%s' have mac '%s'" %
|
||||
- (name, ret[mac], mac))
|
||||
+ raise_duplicate_mac_error = True
|
||||
+ msg = "duplicate mac found! both '%s' and '%s' have mac '%s'." % (
|
||||
+ name,
|
||||
+ ret[mac],
|
||||
+ mac,
|
||||
+ )
|
||||
+ # Hyper-V netvsc driver will register a VF with the same mac
|
||||
+ #
|
||||
+ # The VF will be enslaved to the master nic shortly after
|
||||
+ # registration. If cloud-init starts enumerating the interfaces
|
||||
+ # before the completion of the enslaving process, it will see
|
||||
+ # two different nics with duplicate mac. Cloud-init should ignore
|
||||
+ # the slave nic (which does not have hv_netvsc driver).
|
||||
+ if driver != driver_map[mac]:
|
||||
+ if driver_map[mac] == "hv_netvsc":
|
||||
+ LOG.warning(
|
||||
+ msg + " Ignoring '%s' due to driver '%s' and "
|
||||
+ "'%s' having driver hv_netvsc."
|
||||
+ % (name, driver, ret[mac])
|
||||
+ )
|
||||
+ continue
|
||||
+ if driver == "hv_netvsc":
|
||||
+ raise_duplicate_mac_error = False
|
||||
+ LOG.warning(
|
||||
+ msg + " Ignoring '%s' due to driver '%s' and "
|
||||
+ "'%s' having driver hv_netvsc."
|
||||
+ % (ret[mac], driver_map[mac], name)
|
||||
+ )
|
||||
+
|
||||
+ if raise_duplicate_mac_error:
|
||||
+ raise RuntimeError(msg)
|
||||
+
|
||||
ret[mac] = name
|
||||
+ driver_map[mac] = driver
|
||||
|
||||
# Pretend that an Infiniband GUID is an ethernet address for Openstack
|
||||
# configuration purposes
|
||||
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
||||
index ce19498..0db4442 100644
|
||||
--- a/tests/unittests/test_net.py
|
||||
+++ b/tests/unittests/test_net.py
|
||||
@@ -5519,7 +5519,8 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
'bridges': ['bridge1'],
|
||||
'vlans': ['bond1.101'],
|
||||
'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1',
|
||||
- 'bond1.101', 'lo'],
|
||||
+ 'bond1.101', 'lo',
|
||||
+ "netvsc0-vf", "netvsc0", "netvsc1","netvsc1-vf"],
|
||||
'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01',
|
||||
'enp0s2': 'aa:aa:aa:aa:aa:02',
|
||||
'bond1': 'aa:aa:aa:aa:aa:01',
|
||||
@@ -5528,12 +5529,27 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
'bridge1-nic': 'aa:aa:aa:aa:aa:03',
|
||||
'lo': '00:00:00:00:00:00',
|
||||
'greptap0': '00:00:00:00:00:00',
|
||||
- 'tun0': None}}
|
||||
+ "greptap0": "00:00:00:00:00:00",
|
||||
+ "netvsc0-vf": "aa:aa:aa:aa:aa:04",
|
||||
+ "netvsc0": "aa:aa:aa:aa:aa:04",
|
||||
+ "netvsc1-vf": "aa:aa:aa:aa:aa:05",
|
||||
+ "netvsc1": "aa:aa:aa:aa:aa:05",
|
||||
+ 'tun0': None},
|
||||
+ "drivers": {
|
||||
+ "netvsc0": "hv_netvsc",
|
||||
+ "netvsc0-vf": "foo",
|
||||
+ "netvsc1": "hv_netvsc",
|
||||
+ "netvsc1-vf": "bar",
|
||||
+ },
|
||||
+ }
|
||||
data = {}
|
||||
|
||||
def _se_get_devicelist(self):
|
||||
return list(self.data['devices'])
|
||||
|
||||
+ def _se_device_driver(self, name):
|
||||
+ return self.data["drivers"].get(name, None)
|
||||
+
|
||||
def _se_get_interface_mac(self, name):
|
||||
return self.data['macs'][name]
|
||||
|
||||
@@ -5553,7 +5569,7 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
def _mock_setup(self):
|
||||
self.data = copy.deepcopy(self._data)
|
||||
self.data['devices'] = set(list(self.data['macs'].keys()))
|
||||
- mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge',
|
||||
+ mocks = ('get_devicelist', "device_driver", 'get_interface_mac', 'is_bridge',
|
||||
'interface_has_own_mac', 'is_vlan', 'get_ib_interface_hwaddr')
|
||||
self.mocks = {}
|
||||
for n in mocks:
|
||||
@@ -5567,6 +5583,11 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
self.data['macs']['bridge1-nic'] = self.data['macs']['enp0s1']
|
||||
self.assertRaises(RuntimeError, net.get_interfaces_by_mac)
|
||||
|
||||
+ def test_raise_exception_on_duplicate_netvsc_macs(self):
|
||||
+ self._mock_setup()
|
||||
+ self.data["macs"]["netvsc0"] = self.data["macs"]["netvsc1"]
|
||||
+ self.assertRaises(RuntimeError, net.get_interfaces_by_mac)
|
||||
+
|
||||
def test_excludes_any_without_mac_address(self):
|
||||
self._mock_setup()
|
||||
ret = net.get_interfaces_by_mac()
|
||||
@@ -5580,7 +5601,10 @@ class TestGetInterfacesByMac(CiTestCase):
|
||||
[mock.call('enp0s1'), mock.call('bond1')], any_order=True)
|
||||
self.assertEqual(
|
||||
{'aa:aa:aa:aa:aa:01': 'enp0s1', 'aa:aa:aa:aa:aa:02': 'enp0s2',
|
||||
- 'aa:aa:aa:aa:aa:03': 'bridge1-nic', '00:00:00:00:00:00': 'lo'},
|
||||
+ 'aa:aa:aa:aa:aa:03': 'bridge1-nic', '00:00:00:00:00:00': 'lo',
|
||||
+ "aa:aa:aa:aa:aa:04": "netvsc0",
|
||||
+ "aa:aa:aa:aa:aa:05": "netvsc1",
|
||||
+ },
|
||||
ret)
|
||||
|
||||
def test_excludes_bridges(self):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Name: cloud-init
|
||||
Version: 21.4
|
||||
Release: 19
|
||||
Release: 20
|
||||
Summary: the defacto multi-distribution package that handles early initialization of a cloud instance.
|
||||
License: ASL 2.0 or GPLv3
|
||||
URL: http://launchpad.net/cloud-init
|
||||
@ -47,6 +47,25 @@ Patch6016: backport-util-add-Version-class.patch
|
||||
|
||||
Patch9000: Fix-the-error-level-logs-displayed-for-the-cloud-init-local-service.patch
|
||||
|
||||
Patch6017: backport-cc_ubuntu_advantage-Redact-token-from-logs-1726.patch
|
||||
Patch6018: backport-Do-not-silently-ignore-integer-uid-1280.patch
|
||||
Patch6019: backport-Ensure-network-ready-before-cloud-init-service-runs.patch
|
||||
Patch6020: backport-Fix-exception-when-no-activator-found-1129.patch
|
||||
Patch6021: backport-Fix-KeyError-in-iproute-pformat-3287.patch
|
||||
Patch6022: backport-Handle-non-existent-ca-cert-config-situation-2073.patch
|
||||
Patch6023: backport-net-dhcp-catch-dhclient-failures-and-raise-NoDHCPLea.patch
|
||||
Patch6024: backport-Don-t-fail-if-IB-and-Ethernet-devices-collide-1411.patch
|
||||
Patch6025: backport-net-skip-duplicate-mac-check-for-netvsc-nic-and-its-.patch
|
||||
Patch6026: backport-Ignore-duplicate-macs-from-mscc_felix-and-fsl_enetc.patch
|
||||
Patch6027: backport-macs-ignore-duplicate-MAC-for-devs-with-driver-drive.patch
|
||||
Patch6028: backport-Set-ownership-for-new-folders-in-Write-Files-Module-.patch
|
||||
Patch6029: backport-Fix-network-v2-metric-rendering-4220.patch
|
||||
Patch6030: backport-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
|
||||
Patch6031: backport-Fix-KeyError-when-rendering-sysconfig-IPv6-routes.patch
|
||||
Patch6032: backport-Fix-default-route-rendering-on-v2-ipv6-1973.patch
|
||||
Patch6033: backport-Drop-support-of-sk-keys-in-cc_ssh-1451.patch
|
||||
Patch6034: backport-Do-not-generate-dsa-and-ed25519-key-types-when-crypt.patch
|
||||
|
||||
BuildRequires: pkgconfig(systemd) python3-devel python3-setuptools systemd
|
||||
BuildRequires: iproute python3-configobj python3-httpretty >= 0.8.14-2
|
||||
BuildRequires: python3-jinja2 python3-jsonpatch python3-jsonschema
|
||||
@ -155,6 +174,28 @@ fi
|
||||
%exclude /usr/share/doc/*
|
||||
|
||||
%changelog
|
||||
* Mon Aug 21 2023 shixuantong <shixuantong1@huawei.com> - 21.4-20
|
||||
- Type:bugfix
|
||||
- CVE:NA
|
||||
- SUG:NA
|
||||
- DESC:Do not generate dsa and ed25519 key types when crypto FIPS mode is enabled
|
||||
Don't fail if IB and Ethernet devices 'collide'
|
||||
Fix default route rendering on v2 ipv6
|
||||
Fix IPv6 netmask format for sysconfig
|
||||
Fix KeyError when rendering sysconfig IPv6 routes
|
||||
Fix network v2 metric rendering
|
||||
Ignore duplicate macs from mscc_felix and fsl_enetc
|
||||
macs: ignore duplicate MAC for devs with driver driver qmi_wwan
|
||||
net/dhcp: catch dhclient failures and raise NoDHCPLeaseError
|
||||
net: skip duplicate mac check for netvsc nic and its VF
|
||||
Set ownership for new folders in Write Files Module
|
||||
cc_ubuntu_advantage: Redact token from logs
|
||||
Do not silently ignore integer uid
|
||||
Ensure network ready before cloud-init service runs
|
||||
Fix exception when no activator found
|
||||
Fix KeyError in iproute pformat
|
||||
Handle non existent ca-cert-config situation
|
||||
|
||||
* Fri Aug 18 2023 shixuantong <shixuantong1@huawei.com> - 21.4-19
|
||||
- backport upstream patches
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user