c1c26e
From 86bd1e20fc802edfb920fa53bd611d469f83250b Mon Sep 17 00:00:00 2001
c1c26e
From: Eduardo Otubo <otubo@redhat.com>
c1c26e
Date: Fri, 18 Jan 2019 16:55:36 +0100
c1c26e
Subject: net: Make sysconfig renderer compatible with Network Manager.
c1c26e
c1c26e
RH-Author: Eduardo Otubo <otubo@redhat.com>
c1c26e
Message-id: <20190118165536.25963-1-otubo@redhat.com>
c1c26e
Patchwork-id: 84052
c1c26e
O-Subject: [RHEL-8.0 cloud-init PATCH] net: Make sysconfig renderer compatible with Network Manager.
c1c26e
Bugzilla: 1602784
c1c26e
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
c1c26e
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
c1c26e
c1c26e
Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=1602784
c1c26e
Brew: https://brewweb.engineering.redhat.com/brew/taskinfo?taskID=19877292
c1c26e
Tested by: upstream maintainers and me
c1c26e
c1c26e
commit 3861102fcaf47a882516d8b6daab518308eb3086
c1c26e
Author: Eduardo Otubo <otubo@redhat.com>
c1c26e
Date:   Fri Jan 18 15:36:19 2019 +0000
c1c26e
c1c26e
    net: Make sysconfig renderer compatible with Network Manager.
c1c26e
c1c26e
    The 'sysconfig' renderer is activated if, and only if, there's ifup and
c1c26e
    ifdown commands present in its search dictonary or the network-scripts
c1c26e
    configuration files are found. This patch adds a check for Network-
c1c26e
    Manager configuration file as well.
c1c26e
c1c26e
    This solution is based on the use of the plugin 'ifcfg-rh' present in
c1c26e
    Network-Manager and is designed to support Fedora 29 or other
c1c26e
    distributions that also replaced network-scripts by Network-Manager.
c1c26e
c1c26e
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
c1c26e
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
c1c26e
Signed-off-by: Danilo C. L. de Paula <ddepaula@redhat.com>
c1c26e
---
c1c26e
 cloudinit/net/sysconfig.py  | 36 +++++++++++++++++++
c1c26e
 tests/unittests/test_net.py | 71 +++++++++++++++++++++++++++++++++++++
c1c26e
 2 files changed, 107 insertions(+)
c1c26e
c1c26e
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
c1c26e
index ae0554ef..dc1815d9 100644
c1c26e
--- a/cloudinit/net/sysconfig.py
c1c26e
+++ b/cloudinit/net/sysconfig.py
c1c26e
@@ -10,11 +10,14 @@ from cloudinit.distros.parsers import resolv_conf
c1c26e
 from cloudinit import log as logging
c1c26e
 from cloudinit import util
c1c26e
 
c1c26e
+from configobj import ConfigObj
c1c26e
+
c1c26e
 from . import renderer
c1c26e
 from .network_state import (
c1c26e
     is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6)
c1c26e
 
c1c26e
 LOG = logging.getLogger(__name__)
c1c26e
+NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf"
c1c26e
 
c1c26e
 
c1c26e
 def _make_header(sep='#'):
c1c26e
@@ -46,6 +49,24 @@ def _quote_value(value):
c1c26e
         return value
c1c26e
 
c1c26e
 
c1c26e
+def enable_ifcfg_rh(path):
c1c26e
+    """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present"""
c1c26e
+    config = ConfigObj(path)
c1c26e
+    if 'main' in config:
c1c26e
+        if 'plugins' in config['main']:
c1c26e
+            if 'ifcfg-rh' in config['main']['plugins']:
c1c26e
+                return
c1c26e
+        else:
c1c26e
+            config['main']['plugins'] = []
c1c26e
+
c1c26e
+        if isinstance(config['main']['plugins'], list):
c1c26e
+            config['main']['plugins'].append('ifcfg-rh')
c1c26e
+        else:
c1c26e
+            config['main']['plugins'] = [config['main']['plugins'], 'ifcfg-rh']
c1c26e
+        config.write()
c1c26e
+        LOG.debug('Enabled ifcfg-rh NetworkManager plugins')
c1c26e
+
c1c26e
+
c1c26e
 class ConfigMap(object):
c1c26e
     """Sysconfig like dictionary object."""
c1c26e
 
c1c26e
@@ -656,6 +677,8 @@ class Renderer(renderer.Renderer):
c1c26e
             netrules_content = self._render_persistent_net(network_state)
c1c26e
             netrules_path = util.target_path(target, self.netrules_path)
c1c26e
             util.write_file(netrules_path, netrules_content, file_mode)
c1c26e
+        if available_nm(target=target):
c1c26e
+            enable_ifcfg_rh(util.target_path(target, path=NM_CFG_FILE))
c1c26e
 
c1c26e
         sysconfig_path = util.target_path(target, templates.get('control'))
c1c26e
         # Distros configuring /etc/sysconfig/network as a file e.g. Centos
c1c26e
@@ -670,6 +693,13 @@ class Renderer(renderer.Renderer):
c1c26e
 
c1c26e
 
c1c26e
 def available(target=None):
c1c26e
+    sysconfig = available_sysconfig(target=target)
c1c26e
+    nm = available_nm(target=target)
c1c26e
+
c1c26e
+    return any([nm, sysconfig])
c1c26e
+
c1c26e
+
c1c26e
+def available_sysconfig(target=None):
c1c26e
     expected = ['ifup', 'ifdown']
c1c26e
     search = ['/sbin', '/usr/sbin']
c1c26e
     for p in expected:
c1c26e
@@ -685,4 +715,10 @@ def available(target=None):
c1c26e
     return True
c1c26e
 
c1c26e
 
c1c26e
+def available_nm(target=None):
c1c26e
+    if not os.path.isfile(util.target_path(target, path=NM_CFG_FILE)):
c1c26e
+        return False
c1c26e
+    return True
c1c26e
+
c1c26e
+
c1c26e
 # vi: ts=4 expandtab
c1c26e
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
c1c26e
index 8bcafe08..526a30ed 100644
c1c26e
--- a/tests/unittests/test_net.py
c1c26e
+++ b/tests/unittests/test_net.py
c1c26e
@@ -22,6 +22,7 @@ import os
c1c26e
 import textwrap
c1c26e
 import yaml
c1c26e
 
c1c26e
+
c1c26e
 DHCP_CONTENT_1 = """
c1c26e
 DEVICE='eth0'
c1c26e
 PROTO='dhcp'
c1c26e
@@ -1854,6 +1855,7 @@ class TestRhelSysConfigRendering(CiTestCase):
c1c26e
 
c1c26e
     with_logs = True
c1c26e
 
c1c26e
+    nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf"
c1c26e
     scripts_dir = '/etc/sysconfig/network-scripts'
c1c26e
     header = ('# Created by cloud-init on instance boot automatically, '
c1c26e
               'do not edit.\n#\n')
c1c26e
@@ -2497,6 +2499,75 @@ iface eth0 inet dhcp
c1c26e
         self.assertEqual(
c1c26e
             expected, dir2dict(tmp_dir)['/etc/network/interfaces'])
c1c26e
 
c1c26e
+    def test_check_ifcfg_rh(self):
c1c26e
+        """ifcfg-rh plugin is added NetworkManager.conf if conf present."""
c1c26e
+        render_dir = self.tmp_dir()
c1c26e
+        nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
c1c26e
+        util.ensure_dir(os.path.dirname(nm_cfg))
c1c26e
+
c1c26e
+        # write a template nm.conf, note plugins is a list here
c1c26e
+        with open(nm_cfg, 'w') as fh:
c1c26e
+            fh.write('# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n')
c1c26e
+        self.assertTrue(os.path.exists(nm_cfg))
c1c26e
+
c1c26e
+        # render and read
c1c26e
+        entry = NETWORK_CONFIGS['small']
c1c26e
+        found = self._render_and_read(network_config=yaml.load(entry['yaml']),
c1c26e
+                                      dir=render_dir)
c1c26e
+        self._compare_files_to_expected(entry[self.expected_name], found)
c1c26e
+        self._assert_headers(found)
c1c26e
+
c1c26e
+        # check ifcfg-rh is in the 'plugins' list
c1c26e
+        config = sysconfig.ConfigObj(nm_cfg)
c1c26e
+        self.assertIn('ifcfg-rh', config['main']['plugins'])
c1c26e
+
c1c26e
+    def test_check_ifcfg_rh_plugins_string(self):
c1c26e
+        """ifcfg-rh plugin is append when plugins is a string."""
c1c26e
+        render_dir = self.tmp_path("render")
c1c26e
+        os.makedirs(render_dir)
c1c26e
+        nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
c1c26e
+        util.ensure_dir(os.path.dirname(nm_cfg))
c1c26e
+
c1c26e
+        # write a template nm.conf, note plugins is a value here
c1c26e
+        util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\nplugins=foo\n')
c1c26e
+
c1c26e
+        # render and read
c1c26e
+        entry = NETWORK_CONFIGS['small']
c1c26e
+        found = self._render_and_read(network_config=yaml.load(entry['yaml']),
c1c26e
+                                      dir=render_dir)
c1c26e
+        self._compare_files_to_expected(entry[self.expected_name], found)
c1c26e
+        self._assert_headers(found)
c1c26e
+
c1c26e
+        # check raw content has plugin
c1c26e
+        nm_file_content = util.load_file(nm_cfg)
c1c26e
+        self.assertIn('ifcfg-rh', nm_file_content)
c1c26e
+
c1c26e
+        # check ifcfg-rh is in the 'plugins' list
c1c26e
+        config = sysconfig.ConfigObj(nm_cfg)
c1c26e
+        self.assertIn('ifcfg-rh', config['main']['plugins'])
c1c26e
+
c1c26e
+    def test_check_ifcfg_rh_plugins_no_plugins(self):
c1c26e
+        """enable_ifcfg_plugin creates plugins value if missing."""
c1c26e
+        render_dir = self.tmp_path("render")
c1c26e
+        os.makedirs(render_dir)
c1c26e
+        nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
c1c26e
+        util.ensure_dir(os.path.dirname(nm_cfg))
c1c26e
+
c1c26e
+        # write a template nm.conf, note plugins is missing
c1c26e
+        util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\n')
c1c26e
+        self.assertTrue(os.path.exists(nm_cfg))
c1c26e
+
c1c26e
+        # render and read
c1c26e
+        entry = NETWORK_CONFIGS['small']
c1c26e
+        found = self._render_and_read(network_config=yaml.load(entry['yaml']),
c1c26e
+                                      dir=render_dir)
c1c26e
+        self._compare_files_to_expected(entry[self.expected_name], found)
c1c26e
+        self._assert_headers(found)
c1c26e
+
c1c26e
+        # check ifcfg-rh is in the 'plugins' list
c1c26e
+        config = sysconfig.ConfigObj(nm_cfg)
c1c26e
+        self.assertIn('ifcfg-rh', config['main']['plugins'])
c1c26e
+
c1c26e
 
c1c26e
 class TestNetplanNetRendering(CiTestCase):
c1c26e
 
c1c26e
-- 
c1c26e
2.20.1
c1c26e