Просмотр исходного кода

Merge pull request #50 from gabriel-samfira/refactor-network-morphing

Refactor network morphing
Nashwan Azhari 6 лет назад
Родитель
Сommit
a56943a3a6

+ 3 - 0
coriolis/osmorphing/coreos.py

@@ -13,6 +13,9 @@ class BaseCoreOSMorphingTools(base.BaseLinuxOSMorphingTools):
             version = os_release.get("VERSION_ID")
             return (name, version)
 
+    def disable_predictable_nic_names(self):
+        pass
+
     def pre_packages_install(self, package_names):
         pass
 

+ 95 - 6
coriolis/osmorphing/debian.py

@@ -2,9 +2,22 @@
 # All Rights Reserved.
 
 import os
+from io import StringIO
 
+import yaml
+
+from coriolis import utils
 from coriolis.osmorphing import base
 
+LO_NIC_TPL = """
+auto lo
+iface lo inet loopback
+"""
+
+INTERFACES_NIC_TPL = """
+auto %(device_name)s
+iface %(device_name)s inet dhcp
+"""
 
 class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
     def _check_os(self):
@@ -21,13 +34,89 @@ class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
                 debian_version_path).decode().split('\n')[0]
             return ('Debian', release)
 
+    def disable_predictable_nic_names(self):
+        grub_cfg = os.path.join(
+            self._os_root_dir,
+            "etc/default/grub")
+        if self._test_path(grub_cfg) is False:
+            return
+        contents = self._read_file(grub_cfg).decode()
+        cfg = utils.Grub2ConfigEditor(contents)
+        cfg.append_to_option(
+            "GRUB_CMDLINE_LINUX_DEFAULT",
+            {"opt_type": "key_val", "opt_key": "net.ifnames", "opt_val": 0})
+        cfg.append_to_option(
+            "GRUB_CMDLINE_LINUX_DEFAULT",
+            {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0})
+        cfg.append_to_option(
+            "GRUB_CMDLINE_LINUX",
+            {"opt_type": "key_val", "opt_key": "net.ifnames", "opt_val": 0})
+        cfg.append_to_option(
+            "GRUB_CMDLINE_LINUX",
+            {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0})
+        self._write_file_sudo("etc/default/grub", cfg.dump())
+        self._exec_cmd_chroot("/usr/sbin/update-grub")
+
+    def _compose_interfaces_config(self, nics_info):
+        fp = StringIO()
+        fp.write(LO_NIC_TPL)
+        fp.write("\n\n")
+        for idx,_ in enumerate(nics_info):
+            dev_name = "eth%d" % idx
+            cfg = INTERFACES_NIC_TPL % {
+                "device_name": dev_name,
+            }
+            fp.write(cfg)
+            fp.write("\n\n")
+        fp.seek(0)
+        return fp.read()
+
+    def _compose_netplan_cfg(self, nics_info):
+        cfg = {
+            "network":{
+                "version": 2,
+                "ethernets": {
+                    "lo": {
+                        "match": {
+                            "name": "lo"
+                        },
+                        "addresses": ["127.0.0.1/8"]
+                    }
+                }
+            }
+        }
+        for idx,_ in enumerate(nics_info):
+            cfg["network"]["ethernets"]["eth%d" % idx] = {
+                "dhcp4": True,
+                "dhcp6": True,
+            }
+        return yaml.dump(cfg, default_flow_style=False)
+
     def set_net_config(self, nics_info, dhcp):
-        if dhcp:
-            # NOTE: doesn't work with chroot
-            interfaces_path = os.path.join(
-                self._os_root_dir, "etc/network/interfaces")
-            self._exec_cmd('sudo sed -i.bak "s/static/dhcp/g" %s' %
-                           interfaces_path)
+        if not dhcp:
+            return
+
+        self.disable_predictable_nic_names()
+        if self._test_path("etc/network"):
+            ifaces_file = "etc/network/interfaces"
+            contents = self._compose_interfaces_config(nics_info)
+            if self._test_path(ifaces_file):
+                self._exec_cmd_chroot(
+                    "cp %s %s.bak" % (ifaces_file, ifaces_file))
+            self._write_file_sudo(ifaces_file, contents)
+
+        netplan_base = "etc/netplan"
+        if self._test_path(netplan_base):
+            curr_files = self._list_dir(netplan_base)
+            for cnf in curr_files:
+                if cnf.endswith(".yaml") or cnf.endswith(".yml"):
+                    pth = "%s/%s" % (netplan_base, cnf)
+                    self._exec_cmd_chroot(
+                        "cp %s %s.bak" % (pth, pth)
+                    )
+            new_cfg = self._compose_netplan_cfg(nics_info)
+            cfg_name = "%s/coriolis_netplan.yaml" % netplan_base
+            self._write_file_sudo(cfg_name, new_cfg)
 
     def pre_packages_install(self, package_names):
         super(BaseDebianMorphingTools, self).pre_packages_install(

+ 3 - 0
coriolis/osmorphing/openwrt.py

@@ -14,6 +14,9 @@ class BaseOpenWRTMorphingTools(base.BaseLinuxOSMorphingTools):
             version = openwrt_release.get("DISTRIB_RELEASE")
             return (name, version)
 
+    def disable_predictable_nic_names(self):
+        pass
+
     def pre_packages_install(self, package_names):
         pass
 

+ 42 - 5
coriolis/osmorphing/redhat.py

@@ -17,6 +17,21 @@ RELEASE_CENTOS = "CentOS Linux"
 RELEASE_FEDORA = "Fedora"
 
 
+IFCFG_TEMPLATE = """
+TYPE=Ethernet
+BOOTPROTO=dhcp
+DEFROUTE=yes
+IPV4_FAILURE_FATAL=no
+IPV6INIT=yes
+IPV6_AUTOCONF=yes
+IPV6_DEFROUTE=yes
+IPV6_FAILURE_FATAL=no
+NAME=%(device_name)s
+DEVICE=%(device_name)s
+ONBOOT=yes
+NM_CONTROLLED=no
+"""
+
 class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
     _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts"
 
@@ -34,9 +49,16 @@ class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
                 redhat_release_path).decode().split('\n')[0].strip()
             m = re.match(r"^(.*) release ([0-9].*) \((.*)\).*$", release_info)
             if m:
-                distro, version, codename = m.groups()
+                distro, version, _ = m.groups()
                 return (distro, version)
 
+    def disable_predictable_nic_names(self):
+        kernel_versions = self._list_dir("lib/modules")
+        for version in kernel_versions:
+            cmd = '/sbin/new-kernel-pkg --update --kernel-args="%s" %s'
+            self._exec_cmd_chroot(cmd % (
+                "net.ifnames=0 biosdevname=0", version))
+
     def _get_net_ifaces_info(self, ifcfgs_ethernet, mac_addresses):
         net_ifaces_info = []
 
@@ -105,12 +127,27 @@ class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
                 ifcfgs.append((ifcfg_file, ifcfg))
         return ifcfgs
 
-    def set_net_config(self, nics_info, dhcp):
-        ifcfgs_ethernet = self._get_ifcfgs_by_type("Ethernet")
+    def _write_nic_configs(self, nics_info):
+        for idx,_ in enumerate(nics_info):
+            dev_name = "eth%d" % idx
+            cfg_path = "etc/sysconfig/network-scripts/ifcfg-%s" % dev_name
+            if self._test_path(cfg_path):
+                self._exec_cmd_chroot(
+                    "cp %s %s.bak" % (cfg_path, cfg_path)
+                )
+            self._write_file_sudo(
+                cfg_path,
+                IFCFG_TEMPLATE % {
+                    "device_name": dev_name,
+                })
 
+    def set_net_config(self, nics_info, dhcp):
         if dhcp:
-            self._set_dhcp_net_config(ifcfgs_ethernet)
+            self.disable_predictable_nic_names()
+            self._write_nic_configs(nics_info)
+            return
 
+        ifcfgs_ethernet = self._get_ifcfgs_by_type("Ethernet")
         mac_addresses = [ni.get("mac_address") for ni in nics_info]
         net_ifaces_info = self._get_net_ifaces_info(ifcfgs_ethernet,
                                                     mac_addresses)
@@ -129,7 +166,7 @@ class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
 
     def _yum_clean_all(self):
         self._exec_cmd_chroot("yum clean all")
-        if self._test_path('/var/cache/yum'):
+        if self._test_path('var/cache/yum'):
             self._exec_cmd_chroot("rm -rf /var/cache/yum")
 
     def pre_packages_install(self, package_names):

+ 4 - 0
coriolis/osmorphing/suse.py

@@ -36,6 +36,10 @@ class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools):
             self._version_id = version_id
             return ('SUSE', release)
 
+    def disable_predictable_nic_names(self):
+        # TODO(gsamfira): implement once we have networking support
+        pass
+
     def set_net_config(self, nics_info, dhcp):
         # TODO(alexpilotti): add networking support
         pass

+ 198 - 0
coriolis/utils.py

@@ -13,11 +13,14 @@ import os
 import pickle
 import re
 import socket
+import string
 import subprocess
 import time
 import traceback
 import uuid
 
+from io import StringIO
+
 import OpenSSL
 from oslo_config import cfg
 from oslo_log import log as logging
@@ -606,3 +609,198 @@ def create_service(ssh, cmdline, svcname, run_as=None, start=True):
     else:
         raise exception.CoriolisException(
             "could not determine init system")
+        
+        
+class Grub2ConfigEditor(object):
+    """This class edits GRUB2 configs, normally found in
+    /etc/default/grub. This class tries to preserve commented
+    and empty lines.
+    NOTE: This class does not actually write to file during
+    commit. Rhather, it will mutate it's internal view of the
+    contents of that file with the latest changes made.
+    Use dump() to get the file contents.
+    """
+    def __init__(self, cfg):
+        self._cfg = cfg
+        self._parsed = self._parse_cfg(self._cfg)
+
+    def _parse_cfg(self, cfg):
+        ret = []
+        for line in cfg.split("\n")[:-1]:
+            if line.startswith("#") or len(line.strip()) == 0:
+                ret.append(
+                    {
+                        "type": "raw",
+                        "payload": line
+                    }
+                )
+                continue
+            vals = line.split("=", 1)
+            if len(vals) != 2:
+                ret.append(
+                    {
+                        "type": "raw",
+                        "payload": line
+                    }
+                )
+                continue
+
+            quoted = False
+            # should extend to single quotes
+            if vals[1].startswith('"') and vals[1].endswith('"'):
+                quoted = True
+                vals[1] = vals[1].strip('"')
+
+            if len(vals[1]) == 0 or vals[1][0] in string.punctuation:
+                ret.append(
+                    {
+                        "type": "option",
+                        "payload": line,
+                        "quoted": quoted,
+                        "option_name": vals[0],
+                        "option_value": [
+                            {
+                                "opt_type": "single",
+                                "opt_val": vals[1],
+                            },
+                        ]
+                    }
+                )
+                continue
+            val_sections = vals[1].split()
+            opt_vals = []
+            for sect in val_sections:
+                fields = sect.split("=", 1)
+                if len(fields) == 1:
+                    opt_vals.append(
+                        {
+                            "opt_type": "single",
+                            "opt_val": sect,
+                        }
+                    )
+                else:
+                    opt_vals.append(
+                        {
+                            "opt_type": "key_val",
+                            "opt_val": fields[1],
+                            "opt_key": fields[0],
+                        }
+                    )
+            ret.append(
+                {
+                    "type": "option",
+                    "payload": line,
+                    "quoted": quoted,
+                    "option_name": vals[0],
+                    "option_value": opt_vals,
+                }
+            )
+        return ret
+
+    def _validate_value(self, value):
+        if type(value) is not dict:
+            raise ValueError("value was not dict")
+        opt_type = value.get("opt_type")
+        if opt_type not in ("key_val", "single"):
+            raise ValueError("invalid value type %s" % opt_type)
+        if opt_type == "key_val":
+            if "opt_val" not in value or "opt_key" not in value:
+                raise ValueError(
+                        "key_val option type requires "
+                        "opt_key key and opt_val")
+        elif opt_type == "single":
+            if "opt_val" not in value:
+                raise ValueError(
+                        "single option type requires opt_val")
+        else:
+            raise ValueError("unknown option type: %s" % opt_type)
+
+
+    def set_option(self, option, value):
+        """Replaces the value of an option completely
+        """
+        self._validate_value(value)
+        opt_found = False
+        for opt in self._parsed:
+            if opt.get("option_name") == option:
+                opt_found = True
+                opt["option_value"] = value
+                break
+        if not opt_found:
+            self._parsed.append({
+                "type": "option",
+                "quoted": True,
+                "option_name": option,
+                "option_value": [
+                    value
+                ],
+            })
+
+    def append_to_option(self, option, value):
+        """Appends a value to the specified option. If we're passing
+        in a key_val type and the option already exists, the value
+        will be replaced. Options of type "single", if absent from the
+        list, will be appended. If a single value already exists
+        it will be ignored.
+        """
+        self._validate_value(value)
+        opt_found = False
+        for opt in self._parsed:
+            if opt.get("option_name") == option:
+                opt_found = True
+                found = False
+                for val in opt["option_value"]:
+                    if (val["opt_type"] == "key_val" and 
+                            value["opt_type"] == "key_val"):
+                        if str(val["opt_key"]) == str(value["opt_key"]):
+                            val["opt_val"] = value["opt_val"]
+                            found = True
+                    elif (val["opt_type"] == "single" and 
+                            value["opt_type"] == "single"):
+                        if str(val["opt_val"]) == str(value["opt_val"]):
+                            found = True
+                if not found:
+                    opt["option_value"].append(value)
+                break
+        if not opt_found:
+            self._parsed.append({
+                "type": "option",
+                "quoted": True,
+                "option_name": option,
+                "option_value": [
+                    value
+                ],
+            })
+
+    def dump(self):
+        """dumps the contents of the file"""
+        tmp = StringIO()
+        for line in self._parsed:
+            if line["type"] == "raw":
+                tmp.write("%s\n" % line["payload"])
+                continue
+            vals = line["option_value"]
+            flat = []
+            for val in vals:
+                if val["opt_type"] == "key_val":
+                    flat.append("%s=%s" % (val["opt_key"], val["opt_val"]))
+                else:
+                    flat.append(str(val["opt_val"]))
+
+            if len(flat) == 0:
+                tmp.write("%s=\n" % line["option_name"])
+                continue
+
+            val = " ".join(flat)
+            quoted = line["quoted"]
+            if len(flat) > 1:
+                quoted = True
+
+            fmt = '%s=%s' % (line["option_name"], val)
+            if quoted:
+                fmt = '%s="%s"' % (line["option_name"], val)
+            tmp.write("%s\n" % fmt)
+        tmp.seek(0)
+        return tmp.read()
+
+