From c3f9da8081ee324e97bc09e73f024bb2b3b06e3f Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Dec 15 2020 16:05:55 +0000 Subject: import cloud-init-19.4-11.el8_3.1 --- diff --git a/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch b/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch new file mode 100644 index 0000000..2fecf7f --- /dev/null +++ b/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch @@ -0,0 +1,488 @@ +From ec14b8ed9cb4264333b80b4361171b1b529c58f3 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Tue, 3 Nov 2020 12:11:45 +0100 +Subject: [PATCH 3/5] Add config modules for controlling IBM PowerVM RMC. + (#584) + +RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) +RH-MergeRequest: 16: Add config modules for controlling IBM PowerVM RMC. (#584) +RH-Commit: [1/1] 734e2c48d323af31aa36abefae346ef62ba3ef5d (eterrell/cloud-init) +RH-Bugzilla: 1894014 + +commit f99d4f96b00a9cfec1c721d364cbfd728674e5dc +Author: Aman306 <45781773+Aman306@users.noreply.github.com> +Date: Wed Oct 28 23:36:09 2020 +0530 + + Add config modules for controlling IBM PowerVM RMC. (#584) + + Reliable Scalable Cluster Technology (RSCT) is a set of software + components that together provide a comprehensive clustering + environment(RAS features) for IBM PowerVM based virtual machines. RSCT + includes the Resource Monitoring and Control (RMC) subsystem. RMC is a + generalized framework used for managing, monitoring, and manipulating + resources. RMC runs as a daemon process on individual machines and needs + creation of unique node id and restarts during VM boot. + + LP: #1895979 + + Co-authored-by: Scott Moser + +Conflicts: +* Calls to module subp.* are replaced by old calls to util.* since the +patch that groups subp.* calls into its own module are introduced after +19.4 release - and it's a huge reafctoring not worth the cherry-pick. + +Signed-off-by: Eduardo Otubo +--- + cloudinit/config/cc_refresh_rmc_and_interface.py | 158 +++++++++++++++++++++ + cloudinit/config/cc_reset_rmc.py | 142 ++++++++++++++++++ + config/cloud.cfg.tmpl | 2 + + .../test_handler_refresh_rmc_and_interface.py | 109 ++++++++++++++ + 4 files changed, 411 insertions(+) + create mode 100644 cloudinit/config/cc_refresh_rmc_and_interface.py + create mode 100644 cloudinit/config/cc_reset_rmc.py + create mode 100644 tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py + +diff --git a/cloudinit/config/cc_refresh_rmc_and_interface.py b/cloudinit/config/cc_refresh_rmc_and_interface.py +new file mode 100644 +index 0000000..07050c4 +--- /dev/null ++++ b/cloudinit/config/cc_refresh_rmc_and_interface.py +@@ -0,0 +1,158 @@ ++# (c) Copyright IBM Corp. 2020 All Rights Reserved ++# ++# Author: Aman Kumar Sinha ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++""" ++Refresh IPv6 interface and RMC ++------------------------------ ++**Summary:** Ensure Network Manager is not managing IPv6 interface ++ ++This module is IBM PowerVM Hypervisor specific ++ ++Reliable Scalable Cluster Technology (RSCT) is a set of software components ++that together provide a comprehensive clustering environment(RAS features) ++for IBM PowerVM based virtual machines. RSCT includes the Resource ++Monitoring and Control (RMC) subsystem. RMC is a generalized framework used ++for managing, monitoring, and manipulating resources. RMC runs as a daemon ++process on individual machines and needs creation of unique node id and ++restarts during VM boot. ++More details refer ++https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm ++ ++This module handles ++- Refreshing RMC ++- Disabling NetworkManager from handling IPv6 interface, as IPv6 interface ++ is used for communication between RMC daemon and PowerVM hypervisor. ++ ++**Internal name:** ``cc_refresh_rmc_and_interface`` ++ ++**Module frequency:** per always ++ ++**Supported distros:** RHEL ++ ++""" ++ ++from cloudinit import log as logging ++from cloudinit.settings import PER_ALWAYS ++from cloudinit import util ++from cloudinit import netinfo ++ ++import errno ++ ++frequency = PER_ALWAYS ++ ++LOG = logging.getLogger(__name__) ++# Ensure that /opt/rsct/bin has been added to standard PATH of the ++# distro. The symlink to rmcctrl is /usr/sbin/rsct/bin/rmcctrl . ++RMCCTRL = 'rmcctrl' ++ ++ ++def handle(name, _cfg, _cloud, _log, _args): ++ if not util.which(RMCCTRL): ++ LOG.debug("No '%s' in path, disabled", RMCCTRL) ++ return ++ ++ LOG.debug( ++ 'Making the IPv6 up explicitly. ' ++ 'Ensuring IPv6 interface is not being handled by NetworkManager ' ++ 'and it is restarted to re-establish the communication with ' ++ 'the hypervisor') ++ ++ ifaces = find_ipv6_ifaces() ++ ++ # Setting NM_CONTROLLED=no for IPv6 interface ++ # making it down and up ++ ++ if len(ifaces) == 0: ++ LOG.debug("Did not find any interfaces with ipv6 addresses.") ++ else: ++ for iface in ifaces: ++ refresh_ipv6(iface) ++ disable_ipv6(sysconfig_path(iface)) ++ restart_network_manager() ++ ++ ++def find_ipv6_ifaces(): ++ info = netinfo.netdev_info() ++ ifaces = [] ++ for iface, data in info.items(): ++ if iface == "lo": ++ LOG.debug('Skipping localhost interface') ++ if len(data.get("ipv4", [])) != 0: ++ # skip this interface, as it has ipv4 addrs ++ continue ++ ifaces.append(iface) ++ return ifaces ++ ++ ++def refresh_ipv6(interface): ++ # IPv6 interface is explicitly brought up, subsequent to which the ++ # RMC services are restarted to re-establish the communication with ++ # the hypervisor. ++ util.subp(['ip', 'link', 'set', interface, 'down']) ++ util.subp(['ip', 'link', 'set', interface, 'up']) ++ ++ ++def sysconfig_path(iface): ++ return '/etc/sysconfig/network-scripts/ifcfg-' + iface ++ ++ ++def restart_network_manager(): ++ util.subp(['systemctl', 'restart', 'NetworkManager']) ++ ++ ++def disable_ipv6(iface_file): ++ # Ensuring that the communication b/w the hypervisor and VM is not ++ # interrupted due to NetworkManager. For this purpose, as part of ++ # this function, the NM_CONTROLLED is explicitly set to No for IPV6 ++ # interface and NetworkManager is restarted. ++ try: ++ contents = util.load_file(iface_file) ++ except IOError as e: ++ if e.errno == errno.ENOENT: ++ LOG.debug("IPv6 interface file %s does not exist\n", ++ iface_file) ++ else: ++ raise e ++ ++ if 'IPV6INIT' not in contents: ++ LOG.debug("Interface file %s did not have IPV6INIT", iface_file) ++ return ++ ++ LOG.debug("Editing interface file %s ", iface_file) ++ ++ # Dropping any NM_CONTROLLED or IPV6 lines from IPv6 interface file. ++ lines = contents.splitlines() ++ lines = [line for line in lines if not search(line)] ++ lines.append("NM_CONTROLLED=no") ++ ++ with open(iface_file, "w") as fp: ++ fp.write("\n".join(lines) + "\n") ++ ++ ++def search(contents): ++ # Search for any NM_CONTROLLED or IPV6 lines in IPv6 interface file. ++ return( ++ contents.startswith("IPV6ADDR") or ++ contents.startswith("IPADDR6") or ++ contents.startswith("IPV6INIT") or ++ contents.startswith("NM_CONTROLLED")) ++ ++ ++def refresh_rmc(): ++ # To make a healthy connection between RMC daemon and hypervisor we ++ # refresh RMC. With refreshing RMC we are ensuring that making IPv6 ++ # down and up shouldn't impact communication between RMC daemon and ++ # hypervisor. ++ # -z : stop Resource Monitoring & Control subsystem and all resource ++ # managers, but the command does not return control to the user ++ # until the subsystem and all resource managers are stopped. ++ # -s : start Resource Monitoring & Control subsystem. ++ try: ++ util.subp([RMCCTRL, '-z']) ++ util.subp([RMCCTRL, '-s']) ++ except Exception: ++ util.logexc(LOG, 'Failed to refresh the RMC subsystem.') ++ raise +diff --git a/cloudinit/config/cc_reset_rmc.py b/cloudinit/config/cc_reset_rmc.py +new file mode 100644 +index 0000000..68373ad +--- /dev/null ++++ b/cloudinit/config/cc_reset_rmc.py +@@ -0,0 +1,142 @@ ++# (c) Copyright IBM Corp. 2020 All Rights Reserved ++# ++# Author: Aman Kumar Sinha ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++ ++""" ++Reset RMC ++------------ ++**Summary:** reset rsct node id ++ ++Reset RMC module is IBM PowerVM Hypervisor specific ++ ++Reliable Scalable Cluster Technology (RSCT) is a set of software components, ++that together provide a comprehensive clustering environment (RAS features) ++for IBM PowerVM based virtual machines. RSCT includes the Resource monitoring ++and control (RMC) subsystem. RMC is a generalized framework used for managing, ++monitoring, and manipulating resources. RMC runs as a daemon process on ++individual machines and needs creation of unique node id and restarts ++during VM boot. ++More details refer ++https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm ++ ++This module handles ++- creation of the unique RSCT node id to every instance/virtual machine ++ and ensure once set, it isn't changed subsequently by cloud-init. ++ In order to do so, it restarts RSCT service. ++ ++Prerequisite of using this module is to install RSCT packages. ++ ++**Internal name:** ``cc_reset_rmc`` ++ ++**Module frequency:** per instance ++ ++**Supported distros:** rhel, sles and ubuntu ++ ++""" ++import os ++ ++from cloudinit import log as logging ++from cloudinit.settings import PER_INSTANCE ++from cloudinit import util ++ ++frequency = PER_INSTANCE ++ ++# RMCCTRL is expected to be in system PATH (/opt/rsct/bin) ++# The symlink for RMCCTRL and RECFGCT are ++# /usr/sbin/rsct/bin/rmcctrl and ++# /usr/sbin/rsct/install/bin/recfgct respectively. ++RSCT_PATH = '/opt/rsct/install/bin' ++RMCCTRL = 'rmcctrl' ++RECFGCT = 'recfgct' ++ ++LOG = logging.getLogger(__name__) ++ ++NODE_ID_FILE = '/etc/ct_node_id' ++ ++ ++def handle(name, _cfg, cloud, _log, _args): ++ # Ensuring node id has to be generated only once during first boot ++ if cloud.datasource.platform_type == 'none': ++ LOG.debug('Skipping creation of new ct_node_id node') ++ return ++ ++ if not os.path.isdir(RSCT_PATH): ++ LOG.debug("module disabled, RSCT_PATH not present") ++ return ++ ++ orig_path = os.environ.get('PATH') ++ try: ++ add_path(orig_path) ++ reset_rmc() ++ finally: ++ if orig_path: ++ os.environ['PATH'] = orig_path ++ else: ++ del os.environ['PATH'] ++ ++ ++def reconfigure_rsct_subsystems(): ++ # Reconfigure the RSCT subsystems, which includes removing all RSCT data ++ # under the /var/ct directory, generating a new node ID, and making it ++ # appear as if the RSCT components were just installed ++ try: ++ out = util.subp([RECFGCT])[0] ++ LOG.debug(out.strip()) ++ return out ++ except util.ProcessExecutionError: ++ util.logexc(LOG, 'Failed to reconfigure the RSCT subsystems.') ++ raise ++ ++ ++def get_node_id(): ++ try: ++ fp = util.load_file(NODE_ID_FILE) ++ node_id = fp.split('\n')[0] ++ return node_id ++ except Exception: ++ util.logexc(LOG, 'Failed to get node ID from file %s.' % NODE_ID_FILE) ++ raise ++ ++ ++def add_path(orig_path): ++ # Adding the RSCT_PATH to env standard path ++ # So thet cloud init automatically find and ++ # run RECFGCT to create new node_id. ++ suff = ":" + orig_path if orig_path else "" ++ os.environ['PATH'] = RSCT_PATH + suff ++ return os.environ['PATH'] ++ ++ ++def rmcctrl(): ++ # Stop the RMC subsystem and all resource managers so that we can make ++ # some changes to it ++ try: ++ return util.subp([RMCCTRL, '-z']) ++ except Exception: ++ util.logexc(LOG, 'Failed to stop the RMC subsystem.') ++ raise ++ ++ ++def reset_rmc(): ++ LOG.debug('Attempting to reset RMC.') ++ ++ node_id_before = get_node_id() ++ LOG.debug('Node ID at beginning of module: %s', node_id_before) ++ ++ # Stop the RMC subsystem and all resource managers so that we can make ++ # some changes to it ++ rmcctrl() ++ reconfigure_rsct_subsystems() ++ ++ node_id_after = get_node_id() ++ LOG.debug('Node ID at end of module: %s', node_id_after) ++ ++ # Check if new node ID is generated or not ++ # by comparing old and new node ID ++ if node_id_after == node_id_before: ++ msg = 'New node ID did not get generated.' ++ LOG.error(msg) ++ raise Exception(msg) +diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl +index 87c37ba..52a259c 100644 +--- a/config/cloud.cfg.tmpl ++++ b/config/cloud.cfg.tmpl +@@ -121,6 +121,8 @@ cloud_final_modules: + - mcollective + {% endif %} + - salt-minion ++ - reset_rmc ++ - refresh_rmc_and_interface + - rightscale_userdata + - scripts-vendor + - scripts-per-once +diff --git a/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py +new file mode 100644 +index 0000000..0c35710 +--- /dev/null ++++ b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py +@@ -0,0 +1,109 @@ ++from cloudinit.config import cc_refresh_rmc_and_interface as ccrmci ++ ++from cloudinit import util ++ ++from cloudinit.tests import helpers as t_help ++from cloudinit.tests.helpers import mock ++ ++from textwrap import dedent ++import logging ++ ++LOG = logging.getLogger(__name__) ++MPATH = "cloudinit.config.cc_refresh_rmc_and_interface" ++NET_INFO = { ++ 'lo': {'ipv4': [{'ip': '127.0.0.1', ++ 'bcast': '', 'mask': '255.0.0.0', ++ 'scope': 'host'}], ++ 'ipv6': [{'ip': '::1/128', ++ 'scope6': 'host'}], 'hwaddr': '', ++ 'up': 'True'}, ++ 'env2': {'ipv4': [{'ip': '8.0.0.19', ++ 'bcast': '8.0.0.255', 'mask': '255.255.255.0', ++ 'scope': 'global'}], ++ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8220/64', ++ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:20', ++ 'up': 'True'}, ++ 'env3': {'ipv4': [{'ip': '90.0.0.14', ++ 'bcast': '90.0.0.255', 'mask': '255.255.255.0', ++ 'scope': 'global'}], ++ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8221/64', ++ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:21', ++ 'up': 'True'}, ++ 'env4': {'ipv4': [{'ip': '9.114.23.7', ++ 'bcast': '9.114.23.255', 'mask': '255.255.255.0', ++ 'scope': 'global'}], ++ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8222/64', ++ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:22', ++ 'up': 'True'}, ++ 'env5': {'ipv4': [], ++ 'ipv6': [{'ip': 'fe80::9c26:c3ff:fea4:62c8/64', ++ 'scope6': 'link'}], 'hwaddr': '42:20:86:df:fa:4c', ++ 'up': 'True'}} ++ ++ ++class TestRsctNodeFile(t_help.CiTestCase): ++ def test_disable_ipv6_interface(self): ++ """test parsing of iface files.""" ++ fname = self.tmp_path("iface-eth5") ++ util.write_file(fname, dedent("""\ ++ BOOTPROTO=static ++ DEVICE=eth5 ++ HWADDR=42:20:86:df:fa:4c ++ IPV6INIT=yes ++ IPADDR6=fe80::9c26:c3ff:fea4:62c8/64 ++ IPV6ADDR=fe80::9c26:c3ff:fea4:62c8/64 ++ NM_CONTROLLED=yes ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """)) ++ ++ ccrmci.disable_ipv6(fname) ++ self.assertEqual(dedent("""\ ++ BOOTPROTO=static ++ DEVICE=eth5 ++ HWADDR=42:20:86:df:fa:4c ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ NM_CONTROLLED=no ++ """), util.load_file(fname)) ++ ++ @mock.patch(MPATH + '.refresh_rmc') ++ @mock.patch(MPATH + '.restart_network_manager') ++ @mock.patch(MPATH + '.disable_ipv6') ++ @mock.patch(MPATH + '.refresh_ipv6') ++ @mock.patch(MPATH + '.netinfo.netdev_info') ++ @mock.patch(MPATH + '.util.which') ++ def test_handle(self, m_refresh_rmc, ++ m_netdev_info, m_refresh_ipv6, m_disable_ipv6, ++ m_restart_nm, m_which): ++ """Basic test of handle.""" ++ m_netdev_info.return_value = NET_INFO ++ m_which.return_value = '/opt/rsct/bin/rmcctrl' ++ ccrmci.handle( ++ "refresh_rmc_and_interface", None, None, None, None) ++ self.assertEqual(1, m_netdev_info.call_count) ++ m_refresh_ipv6.assert_called_with('env5') ++ m_disable_ipv6.assert_called_with( ++ '/etc/sysconfig/network-scripts/ifcfg-env5') ++ self.assertEqual(1, m_restart_nm.call_count) ++ self.assertEqual(1, m_refresh_rmc.call_count) ++ ++ @mock.patch(MPATH + '.netinfo.netdev_info') ++ def test_find_ipv6(self, m_netdev_info): ++ """find_ipv6_ifaces parses netdev_info returning those with ipv6""" ++ m_netdev_info.return_value = NET_INFO ++ found = ccrmci.find_ipv6_ifaces() ++ self.assertEqual(['env5'], found) ++ ++ @mock.patch(MPATH + '.util.subp') ++ def test_refresh_ipv6(self, m_subp): ++ """refresh_ipv6 should ip down and up the interface.""" ++ iface = "myeth0" ++ ccrmci.refresh_ipv6(iface) ++ m_subp.assert_has_calls([ ++ mock.call(['ip', 'link', 'set', iface, 'down']), ++ mock.call(['ip', 'link', 'set', iface, 'up'])]) +-- +1.8.3.1 + diff --git a/SOURCES/ci-DHCP-sandboxing-failing-on-noexec-mounted-var-tmp-52.patch b/SOURCES/ci-DHCP-sandboxing-failing-on-noexec-mounted-var-tmp-52.patch new file mode 100644 index 0000000..672b882 --- /dev/null +++ b/SOURCES/ci-DHCP-sandboxing-failing-on-noexec-mounted-var-tmp-52.patch @@ -0,0 +1,115 @@ +From 94753da021d0849f4858e2c2cb98b3276842b665 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 24 Aug 2020 15:34:24 +0200 +Subject: [PATCH 1/5] DHCP sandboxing failing on noexec mounted /var/tmp (#521) + +RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) +RH-MergeRequest: 1: DHCP sandboxing failing on noexec mounted /var/tmp (#521) +RH-Commit: [1/1] 4971d742aa1de27dff61b07ef9d6d478c0889ded (eterrell/cloud-init) +RH-Bugzilla: 1879989 + +commit db86753f81af73826158c9522f2521f210300e2b +Author: Eduardo Otubo +Date: Mon Aug 24 15:34:24 2020 +0200 + + DHCP sandboxing failing on noexec mounted /var/tmp (#521) + + * DHCP sandboxing failing on noexec mounted /var/tmp + + If /var/tmp is mounted with noexec option the DHCP sandboxing will fail + with Permission Denied. This patch simply avoids this error by checking + the exec permission updating the dhcp path in negative case. + + rhbz: https://bugzilla.redhat.com/show_bug.cgi?id=1879989 + + Signed-off-by: Eduardo Otubo + + * Replacing with os.* calls + + * Adding test and removing isfile() useless call. + + Co-authored-by: Rick Harding + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/dhcp.py | 6 ++++++ + cloudinit/net/tests/test_dhcp.py | 46 ++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 52 insertions(+) + +diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py +index c033cc8..841e72e 100644 +--- a/cloudinit/net/dhcp.py ++++ b/cloudinit/net/dhcp.py +@@ -215,6 +215,12 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): + pid_file = os.path.join(cleandir, 'dhclient.pid') + lease_file = os.path.join(cleandir, 'dhcp.leases') + ++ # In some cases files in /var/tmp may not be executable, launching dhclient ++ # from there will certainly raise 'Permission denied' error. Try launching ++ # the original dhclient instead. ++ if not os.access(sandbox_dhclient_cmd, os.X_OK): ++ sandbox_dhclient_cmd = dhclient_cmd_path ++ + # ISC dhclient needs the interface up to send initial discovery packets. + # Generally dhclient relies on dhclient-script PREINIT action to bring the + # link up before attempting discovery. Since we are using -sf /bin/true, +diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py +index c3fa1e0..08e2cfb 100644 +--- a/cloudinit/net/tests/test_dhcp.py ++++ b/cloudinit/net/tests/test_dhcp.py +@@ -406,6 +406,52 @@ class TestDHCPDiscoveryClean(CiTestCase): + 'eth9', '-sf', '/bin/true'], capture=True)]) + m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) + ++ @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid') ++ @mock.patch('cloudinit.net.dhcp.os.kill') ++ @mock.patch('cloudinit.net.dhcp.subp.subp') ++ def test_dhcp_discovery_outside_sandbox(self, m_subp, m_kill, m_getppid): ++ """dhcp_discovery brings up the interface and runs dhclient. ++ ++ It also returns the parsed dhcp.leases file generated in the sandbox. ++ """ ++ m_subp.return_value = ('', '') ++ tmpdir = self.tmp_dir() ++ dhclient_script = os.path.join(tmpdir, 'dhclient.orig') ++ script_content = '#!/bin/bash\necho fake-dhclient' ++ write_file(dhclient_script, script_content, mode=0o755) ++ lease_content = dedent(""" ++ lease { ++ interface "eth9"; ++ fixed-address 192.168.2.74; ++ option subnet-mask 255.255.255.0; ++ option routers 192.168.2.1; ++ } ++ """) ++ lease_file = os.path.join(tmpdir, 'dhcp.leases') ++ write_file(lease_file, lease_content) ++ pid_file = os.path.join(tmpdir, 'dhclient.pid') ++ my_pid = 1 ++ write_file(pid_file, "%d\n" % my_pid) ++ m_getppid.return_value = 1 # Indicate that dhclient has daemonized ++ ++ with mock.patch('os.access', return_value=False): ++ self.assertCountEqual( ++ [{'interface': 'eth9', 'fixed-address': '192.168.2.74', ++ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], ++ dhcp_discovery(dhclient_script, 'eth9', tmpdir)) ++ # dhclient script got copied ++ with open(os.path.join(tmpdir, 'dhclient.orig')) as stream: ++ self.assertEqual(script_content, stream.read()) ++ # Interface was brought up before dhclient called from sandbox ++ m_subp.assert_has_calls([ ++ mock.call( ++ ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True), ++ mock.call( ++ [os.path.join(tmpdir, 'dhclient.orig'), '-1', '-v', '-lf', ++ lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), ++ 'eth9', '-sf', '/bin/true'], capture=True)]) ++ m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) ++ + + class TestSystemdParseLeases(CiTestCase): + +-- +1.8.3.1 + diff --git a/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch b/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch new file mode 100644 index 0000000..c69e974 --- /dev/null +++ b/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch @@ -0,0 +1,303 @@ +From 02924179d423c919d0d46e6149da5bb8d26dd0d5 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Tue, 3 Nov 2020 12:16:37 +0100 +Subject: [PATCH 4/5] Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on + static6 (#634) + +RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) +RH-MergeRequest: 17: Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634) +RH-Commit: [1/2] ba604c675f7c54a3e1768945a9ba77918ca4a57b (eterrell/cloud-init) +RH-Bugzilla: 1894015 + +commit b46e4a8cff667c8441622089cf7d57aeb88220cd +Author: Eduardo Otubo +Date: Thu Oct 29 15:05:42 2020 +0100 + + Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634) + + The static and static6 subnet types for network_data.json were + being ignored by the Openstack handler, this would cause the code to + break and not function properly. + + As of today, if a static6 configuration is chosen, the interface will + still eventually be available to receive router advertisements or be set + from NetworkManager to wait for them and cycle the interface in negative + case. + + It is safe to assume that if the interface is manually configured to use + static ipv6 address, there's no need to wait for router advertisements. + This patch will set automatically IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA + both to "no" in this case. + + This patch fixes the specific behavior only for RHEL flavor and + sysconfig renderer. It also introduces new unit tests for the specific + case as well as adjusts some existent tests to be compatible with the + new options. This patch also addresses this problem by assigning the + appropriate subnet type for each case on the openstack handler. + + rhbz: #1889635 + rhbz: #1889635 + + Signed-off-by: Eduardo Otubo otubo@redhat.com + +Conflicts: +* The context of the patches are slightly different from upstream since +the there is more code added around the changes. But nothing interfering +on the patches. +* One minor conflict, removed the "flavor == 'rhel'" check because the +commit that introduced this change is after the 19.4 release. No harm +done since this commit is intended to be shipped to RHEL only anyways. + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/network_state.py | 3 +- + cloudinit/net/sysconfig.py | 4 + + cloudinit/sources/helpers/openstack.py | 8 +- + tests/unittests/test_distros/test_netconfig.py | 2 + + tests/unittests/test_net.py | 100 +++++++++++++++++++++++++ + 5 files changed, 115 insertions(+), 2 deletions(-) + +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index f3e8e25..2525fc9 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -822,7 +822,8 @@ def _normalize_subnet(subnet): + + if subnet.get('type') in ('static', 'static6'): + normal_subnet.update( +- _normalize_net_keys(normal_subnet, address_keys=('address',))) ++ _normalize_net_keys(normal_subnet, address_keys=( ++ 'address', 'ip_address',))) + normal_subnet['routes'] = [_normalize_route(r) + for r in subnet.get('routes', [])] + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 4b4ed09..4210544 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -401,6 +401,10 @@ class Renderer(renderer.Renderer): + ' because ipv4 subnet-level mtu:%s provided.', + iface_cfg.name, iface_cfg[mtu_key], subnet['mtu']) + iface_cfg[mtu_key] = subnet['mtu'] ++ ++ if subnet_is_ipv6(subnet): ++ iface_cfg['IPV6_FORCE_ACCEPT_RA'] = False ++ iface_cfg['IPV6_AUTOCONF'] = False + elif subnet_type == 'manual': + # If the subnet has an MTU setting, then ONBOOT=True + # to apply the setting +diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py +index 0778f45..6ef4f90 100644 +--- a/cloudinit/sources/helpers/openstack.py ++++ b/cloudinit/sources/helpers/openstack.py +@@ -592,11 +592,17 @@ def convert_net_json(network_json=None, known_macs=None): + elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless', + 'ipv6_dhcpv6-stateful']: + subnet.update({'type': network['type']}) +- elif network['type'] in ['ipv4', 'ipv6']: ++ elif network['type'] in ['ipv4', 'static']: + subnet.update({ + 'type': 'static', + 'address': network.get('ip_address'), + }) ++ elif network['type'] in ['ipv6', 'static6']: ++ cfg.update({'accept-ra': False}) ++ subnet.update({ ++ 'type': 'static6', ++ 'address': network.get('ip_address'), ++ }) + + # Enable accept_ra for stateful and legacy ipv6_dhcp types + if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']: +diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py +index 4ea4203..b85a333 100644 +--- a/tests/unittests/test_distros/test_netconfig.py ++++ b/tests/unittests/test_distros/test_netconfig.py +@@ -673,7 +673,9 @@ class TestNetCfgDistroOpensuse(TestNetCfgDistroBase): + IPADDR6=2607:f0d0:1002:0011::2/64 + IPV6ADDR=2607:f0d0:1002:0011::2/64 + IPV6INIT=yes ++ IPV6_AUTOCONF=no + IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 ++ IPV6_FORCE_ACCEPT_RA=no + NM_CONTROLLED=no + ONBOOT=yes + STARTMODE=auto +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 2eedb12..b2b7c4b 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -768,7 +768,9 @@ IPADDR6_2=2001:DB10::10/64 + IPV6ADDR=2001:DB8::10/64 + IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64" + IPV6INIT=yes ++IPV6_AUTOCONF=no + IPV6_DEFAULTGW=2001:DB8::1 ++IPV6_FORCE_ACCEPT_RA=no + NETMASK=255.255.252.0 + ONBOOT=yes + STARTMODE=auto +@@ -1016,6 +1018,8 @@ NETWORK_CONFIGS = { + IPADDR6=2001:1::1/64 + IPV6ADDR=2001:1::1/64 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no + NETMASK=255.255.255.0 + ONBOOT=yes + STARTMODE=auto +@@ -1201,6 +1205,33 @@ NETWORK_CONFIGS = { + """), + }, + }, ++ 'static6': { ++ 'yaml': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ accept-ra: 'no' ++ subnets: ++ - type: 'static6' ++ address: 2001:1::1/64 ++ """).rstrip(' '), ++ 'expected_sysconfig_rhel': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ IPV6ADDR=2001:1::1/64 ++ IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, + 'dhcpv6_stateless': { + 'expected_eni': textwrap.dedent("""\ + auto lo +@@ -1507,6 +1538,8 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + IPADDR6=2001:1::1/64 + IPV6ADDR=2001:1::1/64 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no + IPV6_DEFAULTGW=2001:4800:78ff:1b::1 + MACADDR=bb:bb:bb:bb:bb:aa + NETMASK=255.255.255.0 +@@ -2067,6 +2100,8 @@ iface bond0 inet6 static + IPADDR6=2001:1::1/92 + IPV6ADDR=2001:1::1/92 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no + MTU=9000 + NETMASK=255.255.255.0 + NETMASK1=255.255.255.0 +@@ -2154,6 +2189,8 @@ iface bond0 inet6 static + IPADDR6=2001:1::bbbb/96 + IPV6ADDR=2001:1::bbbb/96 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no + IPV6_DEFAULTGW=2001:1::1 + MTU=2222 + NETMASK=255.255.255.0 +@@ -2213,6 +2250,9 @@ iface bond0 inet6 static + IPADDR6=2001:1::100/96 + IPV6ADDR=2001:1::100/96 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no ++ NM_CONTROLLED=no + ONBOOT=yes + STARTMODE=auto + TYPE=Ethernet +@@ -2226,6 +2266,9 @@ iface bond0 inet6 static + IPADDR6=2001:1::101/96 + IPV6ADDR=2001:1::101/96 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no ++ NM_CONTROLLED=no + ONBOOT=yes + STARTMODE=auto + TYPE=Ethernet +@@ -3015,6 +3058,61 @@ USERCTL=no + self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + ++ def test_stattic6_from_json(self): ++ net_json = { ++ "services": [{"type": "dns", "address": "172.19.0.12"}], ++ "networks": [{ ++ "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4", ++ "type": "ipv4", "netmask": "255.255.252.0", ++ "link": "tap1a81968a-79", ++ "routes": [{ ++ "netmask": "0.0.0.0", ++ "network": "0.0.0.0", ++ "gateway": "172.19.3.254", ++ }, { ++ "netmask": "0.0.0.0", # A second default gateway ++ "network": "0.0.0.0", ++ "gateway": "172.20.3.254", ++ }], ++ "ip_address": "172.19.1.34", "id": "network0" ++ }, { ++ "network_id": "mgmt", ++ "netmask": "ffff:ffff:ffff:ffff::", ++ "link": "interface1", ++ "mode": "link-local", ++ "routes": [], ++ "ip_address": "fe80::c096:67ff:fe5c:6e84", ++ "type": "static6", ++ "id": "network1", ++ "services": [], ++ "accept-ra": "false" ++ }], ++ "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'} ++ render_dir = self.tmp_dir() ++ network_cfg = openstack.convert_net_json(net_json, known_macs=macs) ++ ns = network_state.parse_net_config_data(network_cfg, ++ skip_broken=False) ++ renderer = self._get_renderer() ++ with self.assertRaises(ValueError): ++ renderer.render_network_state(ns, target=render_dir) ++ self.assertEqual([], os.listdir(render_dir)) ++ ++ def test_static6_from_yaml(self): ++ entry = NETWORK_CONFIGS['static6'] ++ 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_dhcpv6_reject_ra_config_v2(self): + entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] + found = self._render_and_read(network_config=yaml.load( +@@ -3133,6 +3231,8 @@ USERCTL=no + IPADDR6=2001:db8::100/32 + IPV6ADDR=2001:db8::100/32 + IPV6INIT=yes ++ IPV6_AUTOCONF=no ++ IPV6_FORCE_ACCEPT_RA=no + IPV6_DEFAULTGW=2001:db8::1 + NETMASK=255.255.255.0 + NM_CONTROLLED=no +-- +1.8.3.1 + diff --git a/SOURCES/ci-net-fix-rendering-of-static6-in-network-config-77.patch b/SOURCES/ci-net-fix-rendering-of-static6-in-network-config-77.patch new file mode 100644 index 0000000..efa65cb --- /dev/null +++ b/SOURCES/ci-net-fix-rendering-of-static6-in-network-config-77.patch @@ -0,0 +1,203 @@ +From 3ee8f2f5dde1bb27e682c5985bffe6fb9f9e5e0b Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Thu, 5 Nov 2020 12:42:26 +0100 +Subject: [PATCH 5/5] net: fix rendering of 'static6' in network config (#77) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) +RH-MergeRequest: 17: Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634) +RH-Commit: [2/2] 30eb756aceb37761d50c70eb4f684662a11afa3f (eterrell/cloud-init) +RH-Bugzilla: 1894015 + +commit dacdd30080bd8183d1f1c1dc9dbcbc8448301529 +Author: Ryan Harper +Date: Wed Jan 8 11:30:17 2020 -0600 + + net: fix rendering of 'static6' in network config (#77) + + * net: fix rendering of 'static6' in network config + + A V1 static6 network typo was misrendered in eni, it's not valid. + It was ignored in sysconfig and netplan. This branch fixes eni, + updates sysconfig, netplan to render it correctly and adds unittests + for all cases. + + Reported-by: Raphaƫl Enrici + + LP: #1850988 + + * net: add comment about static6 type in subnet_is_ipv6 + + Co-authored-by: Chad Smith + Co-authored-by: Daniel Watkins + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/eni.py | 4 +- + cloudinit/net/netplan.py | 2 +- + cloudinit/net/network_state.py | 2 +- + cloudinit/net/sysconfig.py | 4 +- + tests/unittests/test_distros/test_netconfig.py | 55 +++++++++++++++++++++++++- + 5 files changed, 61 insertions(+), 6 deletions(-) + +diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py +index 7077106..2f71456 100644 +--- a/cloudinit/net/eni.py ++++ b/cloudinit/net/eni.py +@@ -429,7 +429,9 @@ class Renderer(renderer.Renderer): + iface['mode'] = 'auto' + # Use stateless DHCPv6 (0=off, 1=on) + iface['dhcp'] = '0' +- elif subnet_is_ipv6(subnet) and subnet['type'] == 'static': ++ elif subnet_is_ipv6(subnet): ++ # mode might be static6, eni uses 'static' ++ iface['mode'] = 'static' + if accept_ra is not None: + # Accept router advertisements (0=off, 1=on) + iface['accept_ra'] = '1' if accept_ra else '0' +diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py +index 14d3999..8985527 100644 +--- a/cloudinit/net/netplan.py ++++ b/cloudinit/net/netplan.py +@@ -98,7 +98,7 @@ def _extract_addresses(config, entry, ifname, features=None): + entry.update({sn_type: True}) + elif sn_type in IPV6_DYNAMIC_TYPES: + entry.update({'dhcp6': True}) +- elif sn_type in ['static']: ++ elif sn_type in ['static', 'static6']: + addr = "%s" % subnet.get('address') + if 'prefix' in subnet: + addr += "/%d" % subnet.get('prefix') +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index 2525fc9..48e5b6e 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -942,7 +942,7 @@ def subnet_is_ipv6(subnet): + # 'static6', 'dhcp6', 'ipv6_dhcpv6-stateful', 'ipv6_dhcpv6-stateless' or + # 'ipv6_slaac' + if subnet['type'].endswith('6') or subnet['type'] in IPV6_DYNAMIC_TYPES: +- # This is a request for DHCPv6. ++ # This is a request either static6 type or DHCPv6. + return True + elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')): + return True +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 4210544..1989d01 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -378,7 +378,7 @@ class Renderer(renderer.Renderer): + iface_cfg['IPV6_AUTOCONF'] = True + elif subnet_type in ['dhcp4', 'dhcp']: + iface_cfg['BOOTPROTO'] = 'dhcp' +- elif subnet_type == 'static': ++ elif subnet_type in ['static', 'static6']: + # grep BOOTPROTO sysconfig.txt -A2 | head -3 + # BOOTPROTO=none|bootp|dhcp + # 'bootp' or 'dhcp' cause a DHCP client +@@ -434,7 +434,7 @@ class Renderer(renderer.Renderer): + continue + elif subnet_type in IPV6_DYNAMIC_TYPES: + continue +- elif subnet_type == 'static': ++ elif subnet_type in ['static', 'static6']: + if subnet_is_ipv6(subnet): + ipv6_index = ipv6_index + 1 + ipv6_cidr = "%s/%s" % (subnet['address'], subnet['prefix']) +diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py +index b85a333..e277bca 100644 +--- a/tests/unittests/test_distros/test_netconfig.py ++++ b/tests/unittests/test_distros/test_netconfig.py +@@ -109,13 +109,31 @@ auto eth1 + iface eth1 inet dhcp + """ + ++V1_NET_CFG_IPV6_OUTPUT = """\ ++# This file is generated from information provided by the datasource. Changes ++# to it will not persist across an instance reboot. To disable cloud-init's ++# network configuration capabilities, write a file ++# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: ++# network: {config: disabled} ++auto lo ++iface lo inet loopback ++ ++auto eth0 ++iface eth0 inet6 static ++ address 2607:f0d0:1002:0011::2/64 ++ gateway 2607:f0d0:1002:0011::1 ++ ++auto eth1 ++iface eth1 inet dhcp ++""" ++ + V1_NET_CFG_IPV6 = {'config': [{'name': 'eth0', + 'subnets': [{'address': + '2607:f0d0:1002:0011::2', + 'gateway': + '2607:f0d0:1002:0011::1', + 'netmask': '64', +- 'type': 'static'}], ++ 'type': 'static6'}], + 'type': 'physical'}, + {'name': 'eth1', + 'subnets': [{'control': 'auto', +@@ -141,6 +159,23 @@ network: + dhcp4: true + """ + ++V1_TO_V2_NET_CFG_IPV6_OUTPUT = """\ ++# This file is generated from information provided by the datasource. Changes ++# to it will not persist across an instance reboot. To disable cloud-init's ++# network configuration capabilities, write a file ++# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: ++# network: {config: disabled} ++network: ++ version: 2 ++ ethernets: ++ eth0: ++ addresses: ++ - 2607:f0d0:1002:0011::2/64 ++ gateway6: 2607:f0d0:1002:0011::1 ++ eth1: ++ dhcp4: true ++""" ++ + V2_NET_CFG = { + 'ethernets': { + 'eth7': { +@@ -376,6 +411,14 @@ class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): + V1_NET_CFG, + expected_cfgs=expected_cfgs.copy()) + ++ def test_apply_network_config_ipv6_ub(self): ++ expected_cfgs = { ++ self.eni_path(): V1_NET_CFG_IPV6_OUTPUT ++ } ++ self._apply_and_verify_eni(self.distro.apply_network_config, ++ V1_NET_CFG_IPV6, ++ expected_cfgs=expected_cfgs.copy()) ++ + + class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase): + def setUp(self): +@@ -419,6 +462,16 @@ class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase): + V1_NET_CFG, + expected_cfgs=expected_cfgs.copy()) + ++ def test_apply_network_config_v1_ipv6_to_netplan_ub(self): ++ expected_cfgs = { ++ self.netplan_path(): V1_TO_V2_NET_CFG_IPV6_OUTPUT, ++ } ++ ++ # ub_distro.apply_network_config(V1_NET_CFG_IPV6, False) ++ self._apply_and_verify_netplan(self.distro.apply_network_config, ++ V1_NET_CFG_IPV6, ++ expected_cfgs=expected_cfgs.copy()) ++ + def test_apply_network_config_v2_passthrough_ub(self): + expected_cfgs = { + self.netplan_path(): V2_TO_V2_NET_CFG_OUTPUT, +-- +1.8.3.1 + diff --git a/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch b/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch new file mode 100644 index 0000000..be05fe3 --- /dev/null +++ b/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch @@ -0,0 +1,254 @@ +From 2f9d58439c94fe00cee951c213f14ace6da73691 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Tue, 15 Sep 2020 18:00:00 +0200 +Subject: [PATCH 2/5] network: Fix type and respect name when rendering vlan in + sysconfig. (#541) + +RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) +RH-MergeRequest: 10: ifup bond0.504 Error: Connection activation failed: No suitable device found for this connection [rhel-8.3.0.z] +RH-Commit: [1/1] fe8bd8bc184d2391b3f9ac6af80e231649d6019a (eterrell/cloud-init) +RH-Bugzilla: 1890551 + +commit 8439b191ec2f336d544cab86dba2860f969cd5b8 +Author: Eduardo Otubo +Date: Tue Sep 15 18:00:00 2020 +0200 + + network: Fix type and respect name when rendering vlan in sysconfig. (#541) + + Prior to this change, vlans were rendered in sysconfig with + 'TYPE=Ethernet', and incorrectly rendered the PHYSDEV based on + the name of the vlan device rather than the 'link' provided + in the network config. + + The change here fixes: + * rendering of TYPE=Ethernet for a vlan + * adds a warning if the configured device name is not supported + per the RHEL 7 docs "11.5. Naming Scheme for VLAN Interfaces" + + LP: #1788915 + LP: #1826608 + RHBZ: #1861871 + +Conflicts: +* A hunk on cloudinit/net/sysconfig.py could not apply cleanly as it +depends on a verification on the distro flavor, which is not implemented +on cloud-init-19.4. +* Couple of hunks could not apply cleanly on tests/unittests/test_net.py +because the definition of unit test response moved a little bit. + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/sysconfig.py | 32 +++++++++- + tests/unittests/test_distros/test_netconfig.py | 81 ++++++++++++++++++++++++++ + tests/unittests/test_net.py | 4 -- + 3 files changed, 112 insertions(+), 5 deletions(-) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 810b283..4b4ed09 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -95,6 +95,10 @@ class ConfigMap(object): + def __len__(self): + return len(self._conf) + ++ def skip_key_value(self, key, val): ++ """Skip the pair key, value if it matches a certain rule.""" ++ return False ++ + def to_string(self): + buf = six.StringIO() + buf.write(_make_header()) +@@ -102,6 +106,8 @@ class ConfigMap(object): + buf.write("\n") + for key in sorted(self._conf.keys()): + value = self._conf[key] ++ if self.skip_key_value(key, value): ++ continue + if isinstance(value, bool): + value = self._bool_map[value] + if not isinstance(value, six.string_types): +@@ -207,6 +213,7 @@ class NetInterface(ConfigMap): + 'bond': 'Bond', + 'bridge': 'Bridge', + 'infiniband': 'InfiniBand', ++ 'vlan': 'Vlan', + } + + def __init__(self, iface_name, base_sysconf_dir, templates, +@@ -260,6 +267,11 @@ class NetInterface(ConfigMap): + c.routes = self.routes.copy() + return c + ++ def skip_key_value(self, key, val): ++ if key == 'TYPE' and val == 'Vlan': ++ return True ++ return False ++ + + class Renderer(renderer.Renderer): + """Renders network information in a /etc/sysconfig format.""" +@@ -599,7 +611,16 @@ class Renderer(renderer.Renderer): + iface_name = iface['name'] + iface_cfg = iface_contents[iface_name] + iface_cfg['VLAN'] = True +- iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')] ++ iface_cfg.kind = 'vlan' ++ ++ rdev = iface['vlan-raw-device'] ++ supported = _supported_vlan_names(rdev, iface['vlan_id']) ++ if iface_name not in supported: ++ LOG.info( ++ "Name '%s' for vlan '%s' is not officially supported" ++ "by RHEL. Supported: %s", ++ iface_name, rdev, ' '.join(supported)) ++ iface_cfg['PHYSDEV'] = rdev + + iface_subnets = iface.get("subnets", []) + route_cfg = iface_cfg.routes +@@ -771,6 +792,15 @@ class Renderer(renderer.Renderer): + "\n".join(netcfg) + "\n", file_mode) + + ++def _supported_vlan_names(rdev, vid): ++ """Return list of supported names for vlan devices per RHEL doc ++ 11.5. Naming Scheme for VLAN Interfaces.""" ++ return [ ++ v.format(rdev=rdev, vid=int(vid)) ++ for v in ("{rdev}{vid:04}", "{rdev}{vid}", ++ "{rdev}.{vid:04}", "{rdev}.{vid}")] ++ ++ + def available(target=None): + sysconfig = available_sysconfig(target=target) + nm = available_nm(target=target) +diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py +index 6720995..4ea4203 100644 +--- a/tests/unittests/test_distros/test_netconfig.py ++++ b/tests/unittests/test_distros/test_netconfig.py +@@ -526,6 +526,87 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): + V1_NET_CFG_IPV6, + expected_cfgs=expected_cfgs.copy()) + ++ def test_vlan_render_unsupported(self): ++ """Render officially unsupported vlan names.""" ++ cfg = { ++ 'version': 2, ++ 'ethernets': { ++ 'eth0': {'addresses': ["192.10.1.2/24"], ++ 'match': {'macaddress': "00:16:3e:60:7c:df"}}}, ++ 'vlans': { ++ 'infra0': {'addresses': ["10.0.1.2/16"], ++ 'id': 1001, 'link': 'eth0'}}, ++ } ++ expected_cfgs = { ++ self.ifcfg_path('eth0'): dedent("""\ ++ BOOTPROTO=none ++ DEVICE=eth0 ++ HWADDR=00:16:3e:60:7c:df ++ IPADDR=192.10.1.2 ++ NETMASK=255.255.255.0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ self.ifcfg_path('infra0'): dedent("""\ ++ BOOTPROTO=none ++ DEVICE=infra0 ++ IPADDR=10.0.1.2 ++ NETMASK=255.255.0.0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ PHYSDEV=eth0 ++ USERCTL=no ++ VLAN=yes ++ """), ++ self.control_path(): dedent("""\ ++ NETWORKING=yes ++ """), ++ } ++ self._apply_and_verify( ++ self.distro.apply_network_config, cfg, ++ expected_cfgs=expected_cfgs) ++ ++ def test_vlan_render(self): ++ cfg = { ++ 'version': 2, ++ 'ethernets': { ++ 'eth0': {'addresses': ["192.10.1.2/24"]}}, ++ 'vlans': { ++ 'eth0.1001': {'addresses': ["10.0.1.2/16"], ++ 'id': 1001, 'link': 'eth0'}}, ++ } ++ expected_cfgs = { ++ self.ifcfg_path('eth0'): dedent("""\ ++ BOOTPROTO=none ++ DEVICE=eth0 ++ IPADDR=192.10.1.2 ++ NETMASK=255.255.255.0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ self.ifcfg_path('eth0.1001'): dedent("""\ ++ BOOTPROTO=none ++ DEVICE=eth0.1001 ++ IPADDR=10.0.1.2 ++ NETMASK=255.255.0.0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ PHYSDEV=eth0 ++ USERCTL=no ++ VLAN=yes ++ """), ++ self.control_path(): dedent("""\ ++ NETWORKING=yes ++ """), ++ } ++ self._apply_and_verify( ++ self.distro.apply_network_config, cfg, ++ expected_cfgs=expected_cfgs) ++ + + class TestNetCfgDistroOpensuse(TestNetCfgDistroBase): + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index a931a3e..2eedb12 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -1496,7 +1496,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + ONBOOT=yes + PHYSDEV=bond0 + STARTMODE=auto +- TYPE=Ethernet + USERCTL=no + VLAN=yes"""), + 'ifcfg-br0': textwrap.dedent("""\ +@@ -1541,7 +1540,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + ONBOOT=yes + PHYSDEV=eth0 + STARTMODE=auto +- TYPE=Ethernet + USERCTL=no + VLAN=yes"""), + 'ifcfg-eth1': textwrap.dedent("""\ +@@ -2163,7 +2161,6 @@ iface bond0 inet6 static + ONBOOT=yes + PHYSDEV=en0 + STARTMODE=auto +- TYPE=Ethernet + USERCTL=no + VLAN=yes"""), + }, +@@ -3180,7 +3177,6 @@ USERCTL=no + ONBOOT=yes + PHYSDEV=eno1 + STARTMODE=auto +- TYPE=Ethernet + USERCTL=no + VLAN=yes + """) +-- +1.8.3.1 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index 4648182..673efce 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -6,7 +6,7 @@ Name: cloud-init Version: 19.4 -Release: 11%{?dist} +Release: 11%{?dist}.1 Summary: Cloud instance init scripts Group: System Environment/Base @@ -54,6 +54,16 @@ Patch21: ci-Detect-kernel-version-before-swap-file-creation-428.patch Patch22: ci-Changing-notation-of-subp-call.patch # For bz#1794664 - [RHEL8] swapon fails with "swapfile has holes" when created on a xfs filesystem by cloud-init Patch23: ci-cc_mounts-fix-incorrect-format-specifiers-316.patch +# For bz#1879989 - [Azure][RHEL 8] cloud-init Permission denied with the use of mount option noexec [rhel-8.3.0.z] +Patch24: ci-DHCP-sandboxing-failing-on-noexec-mounted-var-tmp-52.patch +# For bz#1890551 - [rhel8][cloud-init] ifup bond0.504 Error: Connection activation failed: No suitable device found for this connection [rhel-8.3.0.z] +Patch25: ci-network-Fix-type-and-respect-name-when-rendering-vla.patch +# For bz#1894014 - Support for cloud-init config modules for PowerVM Hypervisor in Red Hat cloud-init [rhel-8.3.0.z] +Patch26: ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch +# For bz#1894015 - Add support for ipv6_autoconf[rhel-8.3.0.z] +Patch27: ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch +# For bz#1894015 - Add support for ipv6_autoconf[rhel-8.3.0.z] +Patch28: ci-net-fix-rendering-of-static6-in-network-config-77.patch BuildArch: noarch @@ -237,6 +247,21 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Mon Nov 09 2020 Miroslav Rezanina - 19.4-11.el8_3.1 +- ci-DHCP-sandboxing-failing-on-noexec-mounted-var-tmp-52.patch [bz#1879989] +- ci-network-Fix-type-and-respect-name-when-rendering-vla.patch [bz#1890551] +- ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch [bz#1894014] +- ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch [bz#1894015] +- ci-net-fix-rendering-of-static6-in-network-config-77.patch [bz#1894015] +- Resolves: bz#1879989 + ([Azure][RHEL 8] cloud-init Permission denied with the use of mount option noexec [rhel-8.3.0.z]) +- Resolves: bz#1890551 + ([rhel8][cloud-init] ifup bond0.504 Error: Connection activation failed: No suitable device found for this connection [rhel-8.3.0.z]) +- Resolves: bz#1894014 + (Support for cloud-init config modules for PowerVM Hypervisor in Red Hat cloud-init [rhel-8.3.0.z]) +- Resolves: bz#1894015 + (Add support for ipv6_autoconf[rhel-8.3.0.z]) + * Wed Sep 02 2020 Miroslav Rezanina - 19.4-11.el8 - ci-cc_mounts-fix-incorrect-format-specifiers-316.patch [bz#1794664] - Resolves: bz#1794664