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

Overhaul detection and tool selection during OSMorphing.

Nashwan Azhari 6 лет назад
Родитель
Сommit
02123ff7b8

+ 31 - 1
coriolis/exception.py

@@ -148,6 +148,18 @@ class Invalid(CoriolisException):
     code = 400
     code = 400
     safe = True
     safe = True
 
 
+class InvalidCustomOSDetectTools(Invalid):
+    message = _("The provided custom OS detect tools are invalid.")
+
+
+class InvalidOSMorphingTools(Invalid):
+    message = _("Invalid OSMorphing tools received: %(tools_class)s")
+
+
+class InvalidDetectedOSParams(CoriolisException):
+    message = _("One or more detected OS parameters were invalid.")
+    safe = True
+
 
 
 class InvalidResults(Invalid):
 class InvalidResults(Invalid):
     message = _("The results are invalid.")
     message = _("The results are invalid.")
@@ -234,7 +246,25 @@ class NotFound(CoriolisException):
 
 
 
 
 class OSMorphingToolsNotFound(NotFound):
 class OSMorphingToolsNotFound(NotFound):
-    message = _("Couldn't find any morphing tools for this OS.")
+    message = _(
+        'No OSMorphing tools were found for OS type "%(os_type)s" for this VM.'
+        ' This would indicate that it was either not possible to determine the'
+        ' exact OS release, or this OS release is not supported by Coriolis. '
+        'Suggestions include performing any needed OSMorphing steps manually '
+        'within the source VM and then re-syncing with the "Skip OS Morphing" '
+        'option enabled to bypass this stage, or contacting Cloudbase support '
+        'for further assistance.')
+
+
+class OSDetectToolsNotFound(NotFound):
+    message = _(
+        'No "%(os_type)s" OS detect tools were able to identify the OS for this VM. '
+        'This would indicate that it was either not possible to determine the '
+        'exact OS release, or this OS release is not supported by Coriolis. '
+        'Suggestions include performing any needed OSMorphing steps manually '
+        'within the source VM and then re-syncing with the "Skip OS Morphing" '
+        'option enabled to bypass this stage, or contacting Cloudbase support '
+        'for further assistance.')
 
 
 
 
 class FileNotFound(NotFound):
 class FileNotFound(NotFound):

+ 107 - 34
coriolis/osmorphing/base.py

@@ -12,62 +12,101 @@ from six import with_metaclass
 
 
 from coriolis import exception
 from coriolis import exception
 from coriolis import utils
 from coriolis import utils
+from coriolis.osmorphing.osdetect import base as base_os_detect
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
 
 
+# Required OS release fields which are expected from the OSDetect tools.
+# 'schemas.CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA' schema:
+REQUIRED_DETECTED_OS_FIELDS = [
+    "os_type", "distribution_name", "release_version", "friendly_release_name"]
+
+
 class BaseOSMorphingTools(object, with_metaclass(abc.ABCMeta)):
 class BaseOSMorphingTools(object, with_metaclass(abc.ABCMeta)):
 
 
     def __init__(
     def __init__(
             self, conn, os_root_dir, os_root_device, hypervisor,
             self, conn, os_root_dir, os_root_device, hypervisor,
-            event_manager):
+            event_manager, detected_os_info):
+
+        self.check_detected_os_info_parameters(detected_os_info)
+
         self._conn = conn
         self._conn = conn
         self._os_root_dir = os_root_dir
         self._os_root_dir = os_root_dir
         self._os_root_device = os_root_device
         self._os_root_device = os_root_device
-        self._distro = None
-        self._version = None
+        self._distro = detected_os_info['distribution_name']
+        self._version = detected_os_info['release_version']
         self._hypervisor = hypervisor
         self._hypervisor = hypervisor
         self._event_manager = event_manager
         self._event_manager = event_manager
+        self._detected_os_info = detected_os_info
         self._environment = {}
         self._environment = {}
 
 
-    def check_os(self):
-        if not self._distro:
-            os_info = self._check_os()
-            if os_info:
-                self._distro, self._version = os_info
-        if self._distro:
-            return self._distro, self._version
-
-    @abc.abstractmethod
-    def _check_os(self):
-        pass
+    @abc.abstractclassmethod
+    def get_required_detected_os_info_fields(cls):
+        raise NotImplementedError("Required OS params not defined.")
+
+    @classmethod
+    def check_detected_os_info_parameters(cls, detected_os_info):
+        required_fields = cls.get_required_detected_os_info_fields()
+        missing_os_info_fields = [
+            field for field in required_fields
+            if field not in detected_os_info]
+        if missing_os_info_fields:
+            raise exception.InvalidDetectedOSParams(
+                "There are parameters (%s) which are required by %s but "
+                "are missing from the detected OS info: %s" % (
+                    missing_os_info_fields, cls.__name__, detected_os_info))
+
+        extra_os_info_fields = [
+            field for field in detected_os_info
+            if field not in required_fields]
+        if extra_os_info_fields:
+            raise exception.InvalidDetectedOSParams(
+                "There were detected OS info parameters (%s) which were not "
+                "expected by %s: %s" % (
+                    extra_os_info_fields, cls.__name__, detected_os_info))
+        return True
+
+    @abc.abstractclassmethod
+    def check_os_supported(cls, detected_os_info):
+        raise NotImplementedError(
+            "OS compatibility check not implemented for tools class %s" % (
+                cls.__name__))
 
 
     @abc.abstractmethod
     @abc.abstractmethod
     def set_net_config(self, nics_info, dhcp):
     def set_net_config(self, nics_info, dhcp):
         pass
         pass
 
 
+    @abc.abstractmethod
     def get_packages(self):
     def get_packages(self):
         return [], []
         return [], []
 
 
+    @abc.abstractmethod
     def run_user_script(self, user_script):
     def run_user_script(self, user_script):
         pass
         pass
 
 
+    @abc.abstractmethod
     def pre_packages_install(self, package_names):
     def pre_packages_install(self, package_names):
         pass
         pass
 
 
+    @abc.abstractmethod
     def install_packages(self, package_names):
     def install_packages(self, package_names):
         pass
         pass
 
 
+    @abc.abstractmethod
     def post_packages_install(self, package_names):
     def post_packages_install(self, package_names):
         pass
         pass
 
 
+    @abc.abstractmethod
     def pre_packages_uninstall(self, package_names):
     def pre_packages_uninstall(self, package_names):
         pass
         pass
 
 
+    @abc.abstractmethod
     def uninstall_packages(self, package_names):
     def uninstall_packages(self, package_names):
         pass
         pass
 
 
+    @abc.abstractmethod
     def post_packages_uninstall(self, package_names):
     def post_packages_uninstall(self, package_names):
         pass
         pass
 
 
@@ -80,11 +119,56 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
     _packages = {}
     _packages = {}
 
 
     def __init__(self, conn, os_root_dir, os_root_dev, hypervisor,
     def __init__(self, conn, os_root_dir, os_root_dev, hypervisor,
-                 event_manager):
+                 event_manager, detected_os_info):
         super(BaseLinuxOSMorphingTools, self).__init__(
         super(BaseLinuxOSMorphingTools, self).__init__(
-            conn, os_root_dir, os_root_dev, hypervisor, event_manager)
+            conn, os_root_dir, os_root_dev, hypervisor, event_manager,
+            detected_os_info)
         self._ssh = conn
         self._ssh = conn
 
 
+    @classmethod
+    def get_required_detected_os_info_fields(cls):
+        return REQUIRED_DETECTED_OS_FIELDS
+
+    @classmethod
+    def _version_supported_util(cls, version, minimum, maximum=None):
+        """ Parses version strings which are prefixed with a floating point and
+        checks whether the value is between the provided minimum and maximum
+        (excluding the maximum).
+        If a check for specific version is desired, the provided minimum and
+        maximum values should be set to equal.
+        Ex: "18.04LTS" => 18.04
+        """
+        if not version:
+            return False
+
+        float_regex = "([0-9]+(\\.[0-9]+)?)"
+        match = re.match(float_regex, version)
+        if not match:
+            LOG.debug(
+                "Version string '%s' does not contain a float", version)
+            return False
+
+        version_float = None
+        try:
+            version_float = float(match.groups()[0])
+        except ValueError:
+            LOG.debug(
+                "Failed to parse float OS release '%s'", match.groups()[0])
+
+        if version_float < minimum:
+            LOG.debug(
+                "Version '%s' smaller than the minimum of '%s' for "
+                "release: %s", version_float, minimum, version)
+            return False
+
+        if maximum and (maximum != minimum) and version_float >= maximum:
+            LOG.debug(
+                "Version '%s' is larger or equal to the maximum of '%s' for "
+                "release: %s", version_float, maximum, version)
+            return False
+
+        return True
+
     def get_packages(self):
     def get_packages(self):
         k_add = [h for h in self._packages.keys() if
         k_add = [h for h in self._packages.keys() if
                  h is None or h == self._hypervisor]
                  h is None or h == self._hypervisor]
@@ -157,12 +241,13 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
         return utils.list_ssh_dir(self._ssh, path)
         return utils.list_ssh_dir(self._ssh, path)
 
 
     def _exec_cmd(self, cmd):
     def _exec_cmd(self, cmd):
-        return utils.exec_ssh_cmd(self._ssh, cmd, self._environment,
-                                  get_pty=True)
+        return utils.exec_ssh_cmd(
+            self._ssh, cmd, environment=self._environment, get_pty=True)
 
 
     def _exec_cmd_chroot(self, cmd):
     def _exec_cmd_chroot(self, cmd):
         return utils.exec_ssh_cmd_chroot(
         return utils.exec_ssh_cmd_chroot(
-            self._ssh, self._os_root_dir, cmd, self._environment, get_pty=True)
+            self._ssh, self._os_root_dir, cmd,
+            environment=self._environment, get_pty=True)
 
 
     def _check_user_exists(self, username):
     def _check_user_exists(self, username):
         try:
         try:
@@ -187,21 +272,9 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
         return self._read_config_file("etc/os-release", check_exists=True)
         return self._read_config_file("etc/os-release", check_exists=True)
 
 
     def _read_config_file(self, chroot_path, check_exists=False):
     def _read_config_file(self, chroot_path, check_exists=False):
-        if not check_exists or self._test_path(chroot_path):
-            content = self._read_file(chroot_path).decode()
-            return self._get_config(content)
-        else:
-            return {}
-
-    def _get_config(self, config_content):
-        config = {}
-        regex_expr = '(.*[^-\\s])\\s*=\\s*(?:"|\')?([^"\']*)(?:"|\')?\\s*'
-        for config_line in config_content.splitlines():
-            m = re.match(regex_expr, config_line)
-            if m:
-                name, value = m.groups()
-                config[name] = value
-        return config
+        full_path = os.path.join(self._os_root_dir, chroot_path)
+        return utils.read_ssh_ini_config_file(
+            self._ssh, full_path, check_exists=check_exists)
 
 
     def _copy_resolv_conf(self):
     def _copy_resolv_conf(self):
         resolv_conf_path = os.path.join(self._os_root_dir, "etc/resolv.conf")
         resolv_conf_path = os.path.join(self._os_root_dir, "etc/resolv.conf")

+ 20 - 0
coriolis/osmorphing/centos.py

@@ -0,0 +1,20 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+
+from coriolis.osmorphing import redhat
+from coriolis.osmorphing.osdetect import centos as centos_detect
+
+
+CENTOS_DISTRO_IDENTIFIER = centos_detect.CENTOS_DISTRO_IDENTIFIER
+
+
+class BaseCentOSMorphingTools(redhat.BaseRedHatMorphingTools):
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] != (
+                CENTOS_DISTRO_IDENTIFIER):
+            return False
+        return cls._version_supported_util(
+            detected_os_info['release_version'], minimum=7)

+ 8 - 7
coriolis/osmorphing/coreos.py

@@ -2,16 +2,17 @@
 # All Rights Reserved.
 # All Rights Reserved.
 
 
 from coriolis.osmorphing import base
 from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import coreos as coreos_detect
 
 
 
 
 class BaseCoreOSMorphingTools(base.BaseLinuxOSMorphingTools):
 class BaseCoreOSMorphingTools(base.BaseLinuxOSMorphingTools):
-    def _check_os(self):
-        os_release = self._get_os_release()
-        id = os_release.get("ID")
-        if id == "coreos":
-            name = os_release.get("NAME")
-            version = os_release.get("VERSION_ID")
-            return (name, version)
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] == (
+                coreos_detect.COREOS_DISTRO_IDENTIFIER):
+            return True
+        return False
 
 
     def disable_predictable_nic_names(self):
     def disable_predictable_nic_names(self):
         pass
         pass

+ 12 - 14
coriolis/osmorphing/debian.py

@@ -8,6 +8,10 @@ import yaml
 
 
 from coriolis import utils
 from coriolis import utils
 from coriolis.osmorphing import base
 from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import debian as debian_osdetect
+
+
+DEBIAN_DISTRO_IDENTIFIER = debian_osdetect.DEBIAN_DISTRO_IDENTIFIER
 
 
 LO_NIC_TPL = """
 LO_NIC_TPL = """
 auto lo
 auto lo
@@ -21,20 +25,14 @@ iface %(device_name)s inet dhcp
 
 
 
 
 class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
 class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
-    def _check_os(self):
-        lsb_release_path = "etc/lsb-release"
-        debian_version_path = "etc/debian_version"
-        if self._test_path(lsb_release_path):
-            config = self._read_config_file("etc/lsb-release")
-            dist_id = config.get('DISTRIB_ID')
-            if dist_id == 'Debian':
-                release = config.get('DISTRIB_RELEASE')
-                return (dist_id, release)
-        elif self._test_path(debian_version_path):
-            release = self._read_file(
-                debian_version_path).decode().splitlines()
-            if release:
-                return ('Debian', release[0])
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] != (
+                DEBIAN_DISTRO_IDENTIFIER):
+            return False
+        return cls._version_supported_util(
+            detected_os_info['release_version'], minimum=8)
 
 
     def disable_predictable_nic_names(self):
     def disable_predictable_nic_names(self):
         grub_cfg = os.path.join(
         grub_cfg = os.path.join(

+ 156 - 49
coriolis/osmorphing/manager.py

@@ -1,12 +1,17 @@
 # Copyright 2016 Cloudbase Solutions Srl
 # Copyright 2016 Cloudbase Solutions Srl
 # All Rights Reserved.
 # All Rights Reserved.
 
 
+import itertools
+
 from oslo_config import cfg
 from oslo_config import cfg
 from oslo_log import log as logging
 from oslo_log import log as logging
 
 
 from coriolis import events
 from coriolis import events
 from coriolis import exception
 from coriolis import exception
+from coriolis import schemas
+from coriolis.osmorphing import base as base_osmorphing
 from coriolis.osmorphing.osmount import factory as osmount_factory
 from coriolis.osmorphing.osmount import factory as osmount_factory
+from coriolis.osmorphing.osdetect import manager as osdetect_manager
 
 
 proxy_opts = [
 proxy_opts = [
     cfg.StrOpt('url',
     cfg.StrOpt('url',
@@ -38,22 +43,94 @@ def _get_proxy_settings():
     }
     }
 
 
 
 
+def run_os_detect(
+        origin_provider, destination_provider, worker_connection,
+        os_type, os_root_dir, osmorphing_info, tools_environment={}):
+    custom_export_os_detect_tools = (
+        origin_provider.get_custom_os_detect_tools(
+            os_type, osmorphing_info))
+    custom_import_os_detect_tools = (
+        destination_provider.get_custom_os_detect_tools(
+            os_type, osmorphing_info))
+
+    detected_info = osdetect_manager.detect_os(
+        worker_connection, os_type, os_root_dir,
+        tools_environment=tools_environment,
+        custom_os_detect_tools=list(
+            itertools.chain(
+                custom_export_os_detect_tools,
+                custom_import_os_detect_tools)))
+
+    schemas.validate_value(
+        detected_info, schemas.CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA)
+
+    return detected_info
+
+
+def get_osmorphing_tools_class_for_provider(
+        provider, detected_os_info, os_type, osmorphing_info):
+    available_tools_cls = provider.get_os_morphing_tools(
+        os_type, osmorphing_info)
+    LOG.debug(
+        "OSMorphing tools classes returned by provider '%s' for os_type '%s' "
+        "and 'osmorphing_info' %s: %s",
+        type(provider), os_type, osmorphing_info, available_tools_cls)
+
+    osmorphing_base_class = base_osmorphing.BaseOSMorphingTools
+    for toolscls in available_tools_cls:
+        if not issubclass(toolscls, osmorphing_base_class):
+            raise exception.InvalidOSMorphingTools(
+                "Provider class '%s' returned OSMorphing tools which are not "
+                "a subclass of '%s': %s" % (
+                    type(provider), osmorphing_base_class, toolscls))
+
+    detected_toolscls = None
+    LOG.info(
+        "Checking OSMorphing tools classes %s returned by provider '%s' "
+        "for compatibility on detected OS info '%s'",
+        [cls.__name__ for cls in available_tools_cls],
+        type(provider), detected_os_info)
+    for toolscls in available_tools_cls:
+        try:
+            toolscls.check_detected_os_info_parameters(detected_os_info)
+        except exception.InvalidDetectedOSParams as ex:
+            LOG.warn(
+                "OSMorphing tools class %s will be skipped as it is "
+                "incompatbile with the detected OS info params %s. "
+                "Error was: %s" % (
+                    toolscls.__name__, detected_os_info, str(ex)))
+            continue
+
+        if toolscls.check_os_supported(detected_os_info):
+            LOG.info(
+                "Found compatible OSMorphing tools class '%s' from provider "
+                "'%s' for detected OS info: %s",
+                toolscls.__name__, type(provider), detected_os_info)
+            detected_toolscls = toolscls
+            break
+        else:
+            LOG.debug(
+                "OSMorphing tools class '%s' is not compatible with detected "
+                "OS info: %s", toolscls.__name__, detected_os_info)
+
+    return detected_toolscls
+
+
 def morph_image(origin_provider, destination_provider, connection_info,
 def morph_image(origin_provider, destination_provider, connection_info,
                 osmorphing_info, user_script, event_handler):
                 osmorphing_info, user_script, event_handler):
     event_manager = events.EventManager(event_handler)
     event_manager = events.EventManager(event_handler)
 
 
-    event_manager.progress_update("Preparing instance for target platform")
-
     os_type = osmorphing_info.get('os_type')
     os_type = osmorphing_info.get('os_type')
     ignore_devices = osmorphing_info.get('ignore_devices', [])
     ignore_devices = osmorphing_info.get('ignore_devices', [])
 
 
+    # instantiate and run OSMount tools:
     os_mount_tools = osmount_factory.get_os_mount_tools(
     os_mount_tools = osmount_factory.get_os_mount_tools(
         os_type, connection_info, event_manager, ignore_devices)
         os_type, connection_info, event_manager, ignore_devices)
 
 
     proxy_settings = _get_proxy_settings()
     proxy_settings = _get_proxy_settings()
     os_mount_tools.set_proxy(proxy_settings)
     os_mount_tools.set_proxy(proxy_settings)
 
 
-    event_manager.progress_update("Preparing for OS partitions discovery")
+    LOG.info("Preparing for OS partitions discovery")
     os_mount_tools.setup()
     os_mount_tools.setup()
 
 
     event_manager.progress_update("Discovering and mounting OS partitions")
     event_manager.progress_update("Discovering and mounting OS partitions")
@@ -65,19 +142,53 @@ def morph_image(origin_provider, destination_provider, connection_info,
 
 
     environment = os_mount_tools.get_environment()
     environment = os_mount_tools.get_environment()
 
 
+    detected_os_info = run_os_detect(
+        origin_provider, destination_provider, conn,
+        os_type, os_root_dir, osmorphing_info,
+        tools_environment=environment)
+
+    # TODO(aznashwan):
+    # - export the source hypervisor type option in the VM's export info
+    # - automatically detect the target hypervisor type from the worker VM
+    hypervisor_type = osmorphing_info.get(
+        'hypervisor_type', None)
+
+    export_os_morphing_tools = None
     try:
     try:
-        (export_os_morphing_tools, _) = origin_provider.get_os_morphing_tools(
-            conn, osmorphing_info)
-        export_os_morphing_tools.set_environment(environment)
+        export_tools_cls = get_osmorphing_tools_class_for_provider(
+            origin_provider, detected_os_info, os_type, osmorphing_info)
+        if export_tools_cls:
+            LOG.info(
+                "Instantiating OSMorphing tools class '%s' for export provider"
+                " '%s'", export_tools_cls.__name__,
+                type(origin_provider))
+            export_os_morphing_tools = export_tools_cls(
+                conn, os_root_dir, os_root_dev, hypervisor_type,
+                event_manager, detected_os_info)
+            export_os_morphing_tools.set_environment(environment)
+        else:
+            LOG.debug(
+                "No compatible OSMorphing tools class found for export provider "
+                "'%s'", type(origin_provider).__name__)
     except exception.OSMorphingToolsNotFound:
     except exception.OSMorphingToolsNotFound:
         LOG.warn(
         LOG.warn(
             "No tools found for export provider of type: %s",
             "No tools found for export provider of type: %s",
             type(origin_provider))
             type(origin_provider))
-        export_os_morphing_tools = None
 
 
-    (import_os_morphing_tools,
-     os_info) = destination_provider.get_os_morphing_tools(
-         conn, osmorphing_info)
+    import_os_morphing_tools_cls = get_osmorphing_tools_class_for_provider(
+        destination_provider, detected_os_info, os_type, osmorphing_info)
+    if not import_os_morphing_tools_cls:
+        LOG.error(
+            "No compatible OSMorphing tools found from import provider '%s' "
+            "for the given detected OS info %s",
+            type(destination_provider),
+            detected_os_info)
+        raise exception.OSMorphingToolsNotFound(os_type=os_type)
+
+    import_os_morphing_tools = import_os_morphing_tools_cls(
+        conn, os_root_dir, os_root_dev, hypervisor_type,
+        event_manager, detected_os_info)
+    import_os_morphing_tools.set_environment(environment)
 
 
     if user_script:
     if user_script:
         event_manager.progress_update(
         event_manager.progress_update(
@@ -87,53 +198,49 @@ def morph_image(origin_provider, destination_provider, connection_info,
         event_manager.progress_update(
         event_manager.progress_update(
             'No OS morphing user script specified')
             'No OS morphing user script specified')
 
 
-    if not import_os_morphing_tools:
-        event_manager.progress_update(
-            'No OS morphing tools found for this instance')
-    else:
-        import_os_morphing_tools.set_environment(environment)
-        event_manager.progress_update('OS being migrated: %s' % str(os_info))
+    event_manager.progress_update(
+        'OS being migrated: %s' % detected_os_info['friendly_release_name'])
 
 
-        (packages_add, _) = import_os_morphing_tools.get_packages()
+    (packages_add, _) = import_os_morphing_tools.get_packages()
 
 
-        if export_os_morphing_tools:
-            (_, packages_remove) = export_os_morphing_tools.get_packages()
-            # Don't remove packages that need to be installed
-            packages_remove = list(set(packages_remove) - set(packages_add))
+    if export_os_morphing_tools:
+        (_, packages_remove) = export_os_morphing_tools.get_packages()
+        # Don't remove packages that need to be installed
+        packages_remove = list(set(packages_remove) - set(packages_add))
 
 
-            LOG.info("Pre packages uninstall")
-            export_os_morphing_tools.pre_packages_uninstall(packages_remove)
+        LOG.info("Pre packages uninstall")
+        export_os_morphing_tools.pre_packages_uninstall(packages_remove)
 
 
-            if packages_remove:
-                event_manager.progress_update(
-                    "Removing packages: %s" % str(packages_remove))
-                export_os_morphing_tools.uninstall_packages(packages_remove)
+        if packages_remove:
+            event_manager.progress_update(
+                "Removing packages: %s" % str(packages_remove))
+            export_os_morphing_tools.uninstall_packages(packages_remove)
 
 
-            LOG.info("Post packages uninstall")
-            export_os_morphing_tools.post_packages_uninstall(packages_remove)
+        LOG.info("Post packages uninstall")
+        export_os_morphing_tools.post_packages_uninstall(packages_remove)
 
 
-        LOG.info("Pre packages install")
-        import_os_morphing_tools.pre_packages_install(packages_add)
+    LOG.info("Pre packages install")
+    import_os_morphing_tools.pre_packages_install(packages_add)
 
 
-        nics_info = osmorphing_info.get('nics_info')
-        set_dhcp = osmorphing_info.get('nics_set_dhcp', True)
-        import_os_morphing_tools.set_net_config(nics_info, dhcp=set_dhcp)
-        LOG.info("Pre packages")
+    nics_info = osmorphing_info.get('nics_info')
+    set_dhcp = osmorphing_info.get('nics_set_dhcp', True)
+    import_os_morphing_tools.set_net_config(nics_info, dhcp=set_dhcp)
+    LOG.info("Pre packages")
 
 
-        if packages_add:
-            event_manager.progress_update(
-                "Adding packages: %s" % str(packages_add))
-            try:
-                import_os_morphing_tools.install_packages(
-                    packages_add)
-            except Exception as err:
-                raise exception.CoriolisException(
-                    "Failed to install packages: %s. Please review logs"
-                    " for more details." % ", ".join(
-                        packages_add)) from err
-
-        LOG.info("Post packages install")
-        import_os_morphing_tools.post_packages_install(packages_add)
+    if packages_add:
+        event_manager.progress_update(
+            "Adding packages: %s" % str(packages_add))
+        try:
+            import_os_morphing_tools.install_packages(
+                packages_add)
+        except Exception as err:
+            raise exception.CoriolisException(
+                "Failed to install packages: %s. Please review logs"
+                " for more details." % ", ".join(
+                    packages_add)) from err
+
+    LOG.info("Post packages install")
+    import_os_morphing_tools.post_packages_install(packages_add)
 
 
     event_manager.progress_update("Dismounting OS partitions")
     event_manager.progress_update("Dismounting OS partitions")
     os_mount_tools.dismount_os(os_root_dir)
     os_mount_tools.dismount_os(os_root_dir)

+ 11 - 8
coriolis/osmorphing/openwrt.py

@@ -2,17 +2,20 @@
 # All Rights Reserved.
 # All Rights Reserved.
 
 
 from coriolis.osmorphing import base
 from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import openwrt as openwrt_detect
+
+
+OPENWRT_DISTRO_IDENTIFIER = openwrt_detect.OPENWRT_DISTRO_IDENTIFIER
 
 
 
 
 class BaseOpenWRTMorphingTools(base.BaseLinuxOSMorphingTools):
 class BaseOpenWRTMorphingTools(base.BaseLinuxOSMorphingTools):
-    def _check_os(self):
-        openwrt_release = self._read_config_file(
-            "etc/openwrt_release", check_exists=True)
-        distrib_id = openwrt_release.get("DISTRIB_ID")
-        if distrib_id == "OpenWrt":
-            name = openwrt_release.get("DISTRIB_DESCRIPTION", distrib_id)
-            version = openwrt_release.get("DISTRIB_RELEASE")
-            return (name, version)
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] == (
+                OPENWRT_DISTRO_IDENTIFIER):
+            return True
+        return False
 
 
     def disable_predictable_nic_names(self):
     def disable_predictable_nic_names(self):
         pass
         pass

+ 12 - 11
coriolis/osmorphing/oracle.py

@@ -4,20 +4,21 @@
 import re
 import re
 
 
 from coriolis.osmorphing import redhat
 from coriolis.osmorphing import redhat
+from coriolis.osmorphing.osdetect import oracle as oracle_detect
+
+
+ORACLE_DISTRO_IDENTIFIER = oracle_detect.ORACLE_DISTRO_IDENTIFIER
 
 
 
 
 class BaseOracleMorphingTools(redhat.BaseRedHatMorphingTools):
 class BaseOracleMorphingTools(redhat.BaseRedHatMorphingTools):
-    def _check_os(self):
-        oracle_release_path = "etc/oracle-release"
-        if self._test_path(oracle_release_path):
-            release_info = self._read_file(
-                oracle_release_path).decode().splitlines()
-            if release_info:
-                m = re.match(r"^(.*) release ([0-9].*)$",
-                             release_info[0].strip())
-                if m:
-                    distro, version = m.groups()
-                    return (distro, version)
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] != (
+                ORACLE_DISTRO_IDENTIFIER):
+            return False
+        return cls._version_supported_util(
+            detected_os_info['release_version'], minimum=7)
 
 
     def _run_dracut(self):
     def _run_dracut(self):
         self._run_dracut_base('kernel')
         self._run_dracut_base('kernel')

+ 0 - 0
coriolis/osmorphing/osdetect/__init__.py


+ 84 - 0
coriolis/osmorphing/osdetect/base.py

@@ -0,0 +1,84 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+
+import abc
+import os
+
+from six import with_metaclass
+
+from coriolis import utils
+
+
+# Required OS release fields to be returned as declared in the
+# 'schemas.CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA' schema:
+REQUIRED_DETECTED_OS_FIELDS = [
+    "os_type", "distribution_name", "release_version", "friendly_release_name"]
+
+
+class BaseOSDetectTools(object, with_metaclass(abc.ABCMeta)):
+
+    def __init__(self, conn, os_root_dir):
+        self._conn = conn
+        self._os_root_dir = os_root_dir
+        self._environment = {}
+
+    @abc.abstractclassmethod
+    def returned_detected_os_info_fields(cls):
+        raise NotImplementedError(
+            "No returned OS info fields by class '%s'" % cls.__name__)
+
+    @abc.abstractmethod
+    def detect_os(self):
+        """ Attempts to detect the mounted OS and return all relevant
+        release info as a dict.
+
+        Must conform to the 'schemas.CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA'
+        """
+        raise NotImplementedError(
+            "`detect_os` not implemented for OS detection tools '%s'" % (
+                type(self)))
+
+    def set_environment(self, environment):
+        self._environment = environment
+
+    # @abc.abstractmethod
+    # def get_friendly_release_name(self, detailed_release_info):
+    #     """ Returns a friendly name/identifier for the OS based on the
+    #     detailed release info as returned by 'detect_os'. """
+    #     raise NotImplementedError(
+    #         "`get_friendly_release_name` not implemented for OS detection "
+    #         "tools '%s'" % type(self))
+
+
+class BaseLinuxOSDetectTools(BaseOSDetectTools):
+
+    @classmethod
+    def returned_detected_os_info_fields(cls):
+        return REQUIRED_DETECTED_OS_FIELDS
+
+    def _read_file(self, chroot_path):
+        path = os.path.join(self._os_root_dir, chroot_path)
+        return utils.read_ssh_file(self._conn, path)
+
+    def _read_config_file(self, chroot_path, check_exists=False):
+        full_path = os.path.join(self._os_root_dir, chroot_path)
+        return utils.read_ssh_ini_config_file(
+            self._conn, full_path, check_exists=check_exists)
+
+    def _get_os_release(self):
+        return self._read_config_file(
+            "etc/os-release", check_exists=True)
+
+    def _test_path(self, chroot_path):
+        full_path = os.path.join(self._os_root_dir, chroot_path)
+        return utils.test_ssh_path(self._conn, full_path)
+
+    def _exec_cmd(self, cmd):
+        return utils.exec_ssh_cmd(
+            self._conn, cmd, environment=self._environment, get_pty=True)
+
+    def _exec_cmd_chroot(self, cmd):
+        return utils.exec_ssh_cmd_chroot(
+            self._conn, self._os_root_dir, cmd,
+            environment=self._environment, get_pty=True)

+ 40 - 0
coriolis/osmorphing/osdetect/centos.py

@@ -0,0 +1,40 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import re
+
+from oslo_log import log as logging
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+LOG = logging.getLogger(__name__)
+CENTOS_DISTRO_IDENTIFIER = "CentOS"
+
+
+class CentOSOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        redhat_release_path = "etc/redhat-release"
+        if self._test_path(redhat_release_path):
+            release_info = self._read_file(
+                redhat_release_path).decode().splitlines()
+            if release_info:
+                m = re.match(r"^(.*) release ([0-9].*) \((.*)\).*$",
+                             release_info[0].strip())
+                if m:
+                    distro, version, _ = m.groups()
+                    if CENTOS_DISTRO_IDENTIFIER not in distro:
+                        LOG.debug(
+                            "Distro does not appear to be a CentOS: %s", distro)
+                        return {}
+
+                    info = {
+                        "os_type": constants.OS_TYPE_LINUX,
+                        "distribution_name": CENTOS_DISTRO_IDENTIFIER,
+                        "release_version": version,
+                        "friendly_release_name": "%s Version %s" % (
+                            CENTOS_DISTRO_IDENTIFIER, version)}
+        return info

+ 25 - 0
coriolis/osmorphing/osdetect/coreos.py

@@ -0,0 +1,25 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+COREOS_DISTRO_IDENTIFIER = "CoreOS"
+
+
+class CoreOSOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        os_release = self._get_os_release()
+        osid = os_release.get("ID")
+        if osid == "coreos":
+            name = os_release.get("NAME")
+            version = os_release.get("VERSION_ID")
+            info = {
+                "os_type": constants.OS_TYPE_LINUX,
+                "distribution_name": COREOS_DISTRO_IDENTIFIER,
+                "release_version": version,
+                "friendly_release_name": "CoreOS Linux %s" % version}
+        return info

+ 38 - 0
coriolis/osmorphing/osdetect/debian.py

@@ -0,0 +1,38 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+DEBIAN_DISTRO_IDENTIFIER = "Debian"
+
+
+class DebianOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        release = None
+        base_info = {
+            "os_type": constants.OS_TYPE_LINUX,
+            "distribution_name": DEBIAN_DISTRO_IDENTIFIER}
+        lsb_release_path = "etc/lsb-release"
+        debian_version_path = "etc/debian_version"
+        if self._test_path(lsb_release_path):
+            config = self._read_config_file("etc/lsb-release")
+            dist_id = config.get('DISTRIB_ID')
+            if dist_id == 'Debian':
+                release = config.get('DISTRIB_RELEASE')
+        elif self._test_path(debian_version_path):
+            deb_release_info = self._read_file(
+                debian_version_path).decode().splitlines()
+            if deb_release_info:
+                release = deb_release_info[0]
+
+        if not release:
+            return {}
+
+        base_info['release_version'] = release
+        base_info['friendly_release_name'] = (
+            "Debian Linux %s" % release)
+
+        return base_info

+ 114 - 0
coriolis/osmorphing/osdetect/manager.py

@@ -0,0 +1,114 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from oslo_log import log as logging
+
+from coriolis import constants
+from coriolis import exception
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import centos
+from coriolis.osmorphing.osdetect import coreos
+from coriolis.osmorphing.osdetect import debian
+from coriolis.osmorphing.osdetect import openwrt
+from coriolis.osmorphing.osdetect import oracle
+from coriolis.osmorphing.osdetect import redhat
+from coriolis.osmorphing.osdetect import suse
+from coriolis.osmorphing.osdetect import ubuntu
+from coriolis.osmorphing.osdetect import windows
+
+
+LOG = logging.getLogger(__name__)
+
+
+LINUX_OS_DETECTION_TOOLS = [
+    centos.CentOSOSDetectTools,
+    coreos.CoreOSOSDetectTools,
+    debian.DebianOSDetectTools,
+    openwrt.OpenWRTOSDetectTools,
+    oracle.OracleOSDetectTools,
+    redhat.RedHatOSDetectTools,
+    suse.SUSEOSDetectTools,
+    ubuntu.UbuntuOSDetectTools
+]
+
+WINDOWS_OS_DETECTION_TOOLS = [windows.WindowsOSDetectTools]
+
+
+def _check_custom_os_detect_tools(custom_os_detect_tools):
+    if not isinstance(custom_os_detect_tools, list):
+        raise exception.InvalidCustomOSDetectTools(
+            "Custom OS detect tools must be a list, got '%s': %s" % (
+                type(custom_os_detect_tools), custom_os_detect_tools))
+    for detect_tool in custom_os_detect_tools:
+        if not isinstance(detect_tool, base.BaseOSDetectTools):
+            raise exception.InvalidCustomOSDetectTools(
+                "Custom OS tools are of an invalid type which does not "
+                "extend base.BaseOSDetectTools: %s" % type(detect_tool))
+    return True
+
+
+def detect_os(
+        conn, os_type, os_root_dir, tools_environment=None,
+        custom_os_detect_tools=None):
+    """ Iterates through all of the OS detection tools until one successfully
+    identifies the OS/release and returns the release info from it.
+
+    param custom_os_detect_tools: list: list of classes which inherit from
+    coriolis.osmorphing.osdetect.base.BaseOSDetectTools.
+    The custom detect tools will be run before the standard ones.
+    """
+    if not tools_environment:
+        tools_environment = {}
+
+    detect_tools_classes = []
+    if custom_os_detect_tools:
+        _check_custom_os_detect_tools(custom_os_detect_tools)
+        detect_tools_classes.extend(custom_os_detect_tools)
+
+    if os_type == constants.OS_TYPE_LINUX:
+        detect_tools_classes.extend(LINUX_OS_DETECTION_TOOLS)
+    elif os_type == constants.OS_TYPE_WINDOWS:
+        detect_tools_classes.extend(WINDOWS_OS_DETECTION_TOOLS)
+    else:
+        raise exception.OSDetectToolsNotFound(os_type=os_type)
+
+    tools = None
+    detected_info = {}
+    for detectcls in detect_tools_classes:
+        tools = detectcls(conn, os_root_dir)
+        tools.set_environment(tools_environment)
+        LOG.debug(
+            "Running OS detection tools class: %s" % detectcls.__name__)
+        detected_info = tools.detect_os()
+        if detected_info:
+            LOG.info(
+                "Successfully detected OS using tools '%s'. Returned "
+                "OS info is: %s", detectcls.__name__, detected_info)
+            break
+
+    if not detected_info:
+        raise exception.OSDetectToolsNotFound(os_type=os_type)
+
+    if not isinstance(detected_info, dict):
+        raise exception.InvalidDetectedOSParams(
+            "OS detect tools '%s' returned a non-dict value: %s" % (
+                type(tools), detected_info))
+
+    declared_returns = tools.returned_detected_os_info_fields()
+    missing_returns = [
+        field for field in declared_returns if field not in detected_info]
+    if missing_returns:
+        raise exception.InvalidDetectedOSParams(
+            "OS detect tools class '%s' has failed to return some required "
+            "fields (%s) in the detected OS info: %s" % (
+                type(tools), declared_returns, detected_info))
+
+    extra_returns = [
+        field for field in detected_info if field not in declared_returns]
+    if extra_returns:
+        raise exception.InvalidDetectedOSParams(
+            "OS detect tools class '%s' has returned fields (%s) which it had "
+            "declared it would return (%s) in info: %s" % (
+                type(tools), extra_returns, declared_returns, detected_info))
+
+    return detected_info

+ 27 - 0
coriolis/osmorphing/osdetect/openwrt.py

@@ -0,0 +1,27 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+OPENWRT_DISTRO_IDENTIFIER = "OpenWRT"
+
+
+class OpenWRTOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        openwrt_release = self._read_config_file(
+            "etc/openwrt_release", check_exists=True)
+        distrib_id = openwrt_release.get("DISTRIB_ID")
+        if distrib_id == "OpenWrt":
+            name = openwrt_release.get("DISTRIB_DESCRIPTION", distrib_id)
+            version = openwrt_release.get("DISTRIB_RELEASE")
+            info = {
+                "os_type": constants.OS_TYPE_LINUX,
+                "distribution_name": OPENWRT_DISTRO_IDENTIFIER,
+                "release_version": version,
+                "friendly_release_name": "%s Version %s" % (
+                    name, version)}
+        return info

+ 32 - 0
coriolis/osmorphing/osdetect/oracle.py

@@ -0,0 +1,32 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import re
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+ORACLE_DISTRO_IDENTIFIER = "Oracle Linux"
+
+
+class OracleOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        oracle_release_path = "etc/oracle-release"
+        if self._test_path(oracle_release_path):
+            release_info = self._read_file(
+                oracle_release_path).decode().splitlines()
+            if release_info:
+                m = re.match(r"^(.*) release ([0-9].*)$",
+                             release_info[0].strip())
+                if m:
+                    distro, version = m.groups()
+                    info = {
+                        "os_type": constants.OS_TYPE_LINUX,
+                        "distribution_name": ORACLE_DISTRO_IDENTIFIER,
+                        "release_version": version,
+                        "friendly_release_name": "%s Version %s" % (
+                            distro, version)}
+        return info

+ 40 - 0
coriolis/osmorphing/osdetect/redhat.py

@@ -0,0 +1,40 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import re
+
+from oslo_log import log as logging
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+LOG = logging.getLogger(__name__)
+RED_HAT_DISTRO_IDENTIFIER = "Red Hat Enterprise Linux Server"
+
+
+class RedHatOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        redhat_release_path = "etc/redhat-release"
+        if self._test_path(redhat_release_path):
+            release_info = self._read_file(
+                redhat_release_path).decode().splitlines()
+            if release_info:
+                m = re.match(r"^(.*) release ([0-9].*) \((.*)\).*$",
+                             release_info[0].strip())
+                if m:
+                    distro, version, _ = m.groups()
+                    if RED_HAT_DISTRO_IDENTIFIER not in distro:
+                        LOG.debug(
+                            "Distro does not appear to be a RHEL: %s", distro)
+                        return {}
+
+                    info = {
+                        "os_type": constants.OS_TYPE_LINUX,
+                        "distribution_name": RED_HAT_DISTRO_IDENTIFIER,
+                        "release_version": version,
+                        "friendly_release_name": "%s Version %s" % (
+                            RED_HAT_DISTRO_IDENTIFIER, version)}
+        return info

+ 65 - 0
coriolis/osmorphing/osdetect/suse.py

@@ -0,0 +1,65 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import copy
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+DETECTED_SUSE_RELEASE_FIELD_NAME = "suse_release_name"
+SLES_DISTRO_IDENTIFIER = "SLES"
+OPENSUSE_DISTRO_IDENTIFIER = "openSUSE"
+OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER = "Tumbleweed"
+
+
+class SUSEOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    @classmethod
+    def returned_detected_os_info_fields(cls):
+        common_fields = super(
+            SUSEOSDetectTools, cls).returned_detected_os_info_fields()
+        fields = copy.deepcopy(common_fields)
+        fields.append(DETECTED_SUSE_RELEASE_FIELD_NAME)
+        return fields
+
+    def detect_os(self):
+        # TODO(aznashwan): implement separate detection for
+        # SLES and openSUSE when the tools for them are separated.
+        info = {}
+        os_release = self._get_os_release()
+        name = os_release.get("NAME")
+        if name and (name == "SLES" or name.startswith("openSUSE")):
+            distro_name = None
+            if name == "SLES":
+                distro_name = SLES_DISTRO_IDENTIFIER
+            elif name.lower().startswith("opensuse"):
+                distro_name = OPENSUSE_DISTRO_IDENTIFIER
+            version = os_release.get(
+                "VERSION_ID", constants.OS_TYPE_UNKNOWN)
+            if 'tumbleweed' in distro_name.lower():
+                version = OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER
+
+            if distro_name:
+                info = {
+                    "os_type": constants.OS_TYPE_LINUX,
+                    "distribution_name": distro_name,
+                    DETECTED_SUSE_RELEASE_FIELD_NAME: name,
+                    "release_version": version,
+                    "friendly_release_name": "%s %s" % (
+                        distro_name, version)}
+
+        # NOTE: should be redundant as all SUSEs which have a
+        # SuSE-release but no os-release have been deprecated
+        # suse_release_path = "etc/SuSE-release"
+        # if self._test_path(suse_release_path):
+        #     release_info = self._read_config_file(suse_release_path)
+        #     version_id = release_info['VERSION']
+        #     patch_level = release_info.get('PATCHLEVEL', None)
+        #     if patch_level:
+        #         version_id = "%s.%s" % (version_id, patch_level)
+        #     return {
+        #         "name": 'SUSE',
+        #         "version": version_id}
+
+        return info

+ 24 - 0
coriolis/osmorphing/osdetect/ubuntu.py

@@ -0,0 +1,24 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis import constants
+from coriolis.osmorphing.osdetect import base
+
+
+UBUNTU_DISTRO_IDENTIFIER = "Ubuntu"
+
+
+class UbuntuOSDetectTools(base.BaseLinuxOSDetectTools):
+
+    def detect_os(self):
+        info = {}
+        config = self._read_config_file("etc/lsb-release", check_exists=True)
+        dist_id = config.get('DISTRIB_ID')
+        if dist_id == 'Ubuntu':
+            release = config.get('DISTRIB_RELEASE')
+            info = {
+                "os_type": constants.OS_TYPE_LINUX,
+                "distribution_name": UBUNTU_DISTRO_IDENTIFIER,
+                "release_version": release,
+                "friendly_release_name": "Ubuntu %s" % release}
+        return info

+ 125 - 0
coriolis/osmorphing/osdetect/windows.py

@@ -0,0 +1,125 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import copy
+import os
+import re
+import uuid
+
+from distutils import version
+from oslo_log import log as logging
+
+from coriolis import constants
+from coriolis import exception
+from coriolis import utils
+from coriolis.osmorphing.osdetect import base
+
+
+WINDOWS_SERVER_IDENTIFIER = "Server"
+WINDOWS_CLIENT_IDENTIFIER = "Client"
+
+
+LOG = logging.getLogger(__name__)
+
+REQUIRED_DETECTED_WINDOWS_OS_FIELDS = [
+    "version_number", "edition_id", "installation_type", "product_name"]
+
+
+class WindowsOSDetectTools(base.BaseOSDetectTools):
+
+    @classmethod
+    def returned_detected_os_info_fields(cls):
+        base_fields = copy.deepcopy(
+            base.REQUIRED_DETECTED_OS_FIELDS)
+        base_fields.extend(
+            REQUIRED_DETECTED_WINDOWS_OS_FIELDS)
+        return base_fields
+
+    def _load_registry_hive(self, subkey, path):
+        self._conn.exec_command("reg.exe", ["load", subkey, path])
+
+    def _unload_registry_hive(self, subkey):
+        self._conn.exec_command("reg.exe", ["unload", subkey])
+
+    def _get_ps_fl_value(self, data, name):
+        m = re.search(r'^%s\s*: (.*)$' % name, data, re.MULTILINE)
+        if m:
+            return m.groups()[0]
+
+    def _get_image_version_info(self):
+        key_name = str(uuid.uuid4())
+
+        self._load_registry_hive(
+            "HKLM\\%s" % key_name,
+            "%sWindows\\System32\\config\\SOFTWARE" % self._os_root_dir)
+        try:
+            version_info_str = self._conn.exec_ps_command(
+                "Get-ItemProperty "
+                "'HKLM:\\%s\\Microsoft\\Windows NT\\CurrentVersion' "
+                "| select CurrentVersion, CurrentMajorVersionNumber, "
+                "CurrentMinorVersionNumber,  CurrentBuildNumber, "
+                "InstallationType, ProductName, EditionID | FL" %
+                key_name).replace(self._conn.EOL, os.linesep)
+        finally:
+            self._unload_registry_hive("HKLM\\%s" % key_name)
+
+        version_info = {}
+        for n in ["CurrentVersion", "CurrentMajorVersionNumber",
+                  "CurrentMinorVersionNumber", "CurrentBuildNumber",
+                  "InstallationType", "ProductName", "EditionID"]:
+            version_info[n] = self._get_ps_fl_value(version_info_str, n)
+
+        if (not version_info["CurrentMajorVersionNumber"] and
+                not version_info["CurrentVersion"]):
+            raise exception.CoriolisException(
+                "Cannot find Windows version info")
+
+        if version_info["CurrentMajorVersionNumber"]:
+            version_str = "%s.%s.%s" % (
+                version_info["CurrentMajorVersionNumber"],
+                version_info["CurrentMinorVersionNumber"],
+                version_info["CurrentBuildNumber"])
+        else:
+            version_str = "%s.%s" % (
+                version_info["CurrentVersion"],
+                version_info["CurrentBuildNumber"])
+
+        return (version.LooseVersion(version_str),
+                version_info["EditionID"],
+                version_info["InstallationType"],
+                version_info["ProductName"])
+
+    def detect_os(self):
+        version_number = None
+        edition_id = None
+        installation_type = None
+        product_name = None
+        try:
+            (version_number,
+             edition_id,
+             installation_type,
+             product_name) = self._get_image_version_info()
+        except exception.CoriolisException as ex:
+            LOG.debug(
+                "Exception during Windows OS detection: %s",
+                utils.get_exception_details())
+
+        LOG.debug(
+            "Successfully identified Windows release as: Version no.: %s; "
+            "Edition id: %s; Install type: %s; Name: %s",
+            version_number, edition_id,
+            installation_type, product_name)
+
+        typ = WINDOWS_CLIENT_IDENTIFIER
+        if 'server' in edition_id.lower():
+            typ = WINDOWS_SERVER_IDENTIFIER
+
+        return {
+            "version_number": version_number,
+            "edition_id": edition_id,
+            "installation_type": installation_type,
+            "product_name": product_name,
+            "os_type": constants.OS_TYPE_WINDOWS,
+            "distribution_name": typ,
+            "release_version": product_name,
+            "friendly_release_name": "Windows %s" % product_name}

+ 5 - 4
coriolis/osmorphing/osmount/base.py

@@ -67,13 +67,14 @@ class BaseSSHOSMountTools(BaseOSMountTools):
         pkey = connection_info.get("pkey")
         pkey = connection_info.get("pkey")
         password = connection_info.get("password")
         password = connection_info.get("password")
 
 
-        LOG.info("Waiting for connectivity on host: %(ip)s:%(port)s",
-                 {"ip": ip, "port": port})
+        LOG.info(
+            "Waiting for SSH connectivity on OSMorphing host: %(ip)s:%(port)s",
+            {"ip": ip, "port": port})
         utils.wait_for_port_connectivity(ip, port)
         utils.wait_for_port_connectivity(ip, port)
 
 
         self._event_manager.progress_update(
         self._event_manager.progress_update(
-            "Connecting to SSH host: %(ip)s:%(port)s" %
-            {"ip": ip, "port": port})
+            "Connecting through SSH to OSMorphing host on: %(ip)s:%(port)s" % (
+                {"ip": ip, "port": port}))
         ssh = paramiko.SSHClient()
         ssh = paramiko.SSHClient()
         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
         ssh.connect(hostname=ip, port=port, username=username, pkey=pkey,
         ssh.connect(hostname=ip, port=port, username=username, pkey=pkey,

+ 19 - 17
coriolis/osmorphing/redhat.py

@@ -7,13 +7,19 @@ import uuid
 
 
 from oslo_log import log as logging
 from oslo_log import log as logging
 
 
-from coriolis.osmorphing import base
 from coriolis import utils
 from coriolis import utils
+from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import centos as centos_detect
+from coriolis.osmorphing.osdetect import redhat as redhat_detect
+
+
+RED_HAT_DISTRO_IDENTIFIER = redhat_detect.RED_HAT_DISTRO_IDENTIFIER
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
-RELEASE_RHEL = "Red Hat Enterprise Linux Server"
-RELEASE_CENTOS = "CentOS Linux"
+# NOTE: some constants duplicated for backwards-compatibility:
+RELEASE_RHEL = RED_HAT_DISTRO_IDENTIFIER
+RELEASE_CENTOS = centos_detect.CENTOS_DISTRO_IDENTIFIER
 RELEASE_FEDORA = "Fedora"
 RELEASE_FEDORA = "Fedora"
 
 
 
 
@@ -35,25 +41,21 @@ NM_CONTROLLED=no
 class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
 class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
     _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts"
     _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts"
 
 
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] != (
+                RED_HAT_DISTRO_IDENTIFIER):
+            return False
+        return cls._version_supported_util(
+            detected_os_info['release_version'], minimum=7)
+
     def __init__(self, conn, os_root_dir, os_root_dev,
     def __init__(self, conn, os_root_dir, os_root_dev,
-                 hypervisor, event_manager):
+                 hypervisor, event_manager, detected_os_info):
         super(BaseRedHatMorphingTools, self).__init__(
         super(BaseRedHatMorphingTools, self).__init__(
             conn, os_root_dir, os_root_dev,
             conn, os_root_dir, os_root_dev,
-            hypervisor, event_manager)
+            hypervisor, event_manager, detected_os_info)
         self._enable_repos = []
         self._enable_repos = []
 
 
-    def _check_os(self):
-        redhat_release_path = "etc/redhat-release"
-        if self._test_path(redhat_release_path):
-            release_info = self._read_file(
-                redhat_release_path).decode().splitlines()
-            if release_info:
-                m = re.match(r"^(.*) release ([0-9].*) \((.*)\).*$",
-                             release_info[0].strip())
-                if m:
-                    distro, version, _ = m.groups()
-                    return (distro, version)
-
     def disable_predictable_nic_names(self):
     def disable_predictable_nic_names(self):
         kernel_versions = self._list_dir("lib/modules")
         kernel_versions = self._list_dir("lib/modules")
         for version in kernel_versions:
         for version in kernel_versions:

+ 43 - 26
coriolis/osmorphing/suse.py

@@ -1,6 +1,7 @@
 # Copyright 2016 Cloudbase Solutions Srl
 # Copyright 2016 Cloudbase Solutions Srl
 # All Rights Reserved.
 # All Rights Reserved.
 
 
+import copy
 import re
 import re
 
 
 from oslo_log import log as logging
 from oslo_log import log as logging
@@ -8,35 +9,47 @@ from oslo_log import log as logging
 from coriolis import exception
 from coriolis import exception
 from coriolis import utils
 from coriolis import utils
 from coriolis.osmorphing import base
 from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import suse as suse_detect
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
+DETECTED_SUSE_RELEASE_FIELD_NAME = suse_detect.DETECTED_SUSE_RELEASE_FIELD_NAME
+SLES_DISTRO_IDENTIFIER = suse_detect.SLES_DISTRO_IDENTIFIER
+OPENSUSE_DISTRO_IDENTIFIER = suse_detect.OPENSUSE_DISTRO_IDENTIFIER
+OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER = (
+    suse_detect.OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER)
+
 
 
 class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools):
 class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools):
-    def _check_os(self):
-        os_release = self._get_os_release()
-        name = os_release.get("NAME")
-        if name and (name == "SLES" or name.startswith("openSUSE")):
-            pretty_name = os_release.get("PRETTY_NAME")
-            if name == "openSUSE Tumbleweed":
-                self._version_id = None
+
+    @classmethod
+    def get_required_detected_os_info_fields(cls):
+        common_fields = super(
+            BaseSUSEMorphingTools, cls).get_required_detected_os_info_fields()
+        fields = copy.deepcopy(common_fields)
+        fields.append(DETECTED_SUSE_RELEASE_FIELD_NAME)
+        return fields
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        distro = detected_os_info['distribution_name']
+        if distro not in (
+                SLES_DISTRO_IDENTIFIER, OPENSUSE_DISTRO_IDENTIFIER):
+            return False
+
+        version = detected_os_info['release_version']
+        if distro == OPENSUSE_DISTRO_IDENTIFIER:
+            if version == OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER:
+                return True
             else:
             else:
-                self._version_id = os_release.get("VERSION_ID")
-            return (name, pretty_name)
-
-        suse_release_path = "etc/SuSE-release"
-        if self._test_path(suse_release_path):
-            out = self._read_file(suse_release_path).decode()
-            release = out.splitlines()
-            release_info = self._get_config(out)
-            version_id = release_info['VERSION']
-            patch_level = release_info.get('PATCHLEVEL', None)
-            if patch_level:
-                version_id = "%s.%s" % (version_id, patch_level)
-            self._version_id = version_id
-            if release:
-                return ('SUSE', release[0])
+                return cls._version_supported_util(
+                    version, minimum=15)
+        elif distro == SLES_DISTRO_IDENTIFIER:
+            return cls._version_supported_util(
+                version, minimum=12)
+
+        return False
 
 
     def disable_predictable_nic_names(self):
     def disable_predictable_nic_names(self):
         # TODO(gsamfira): implement once we have networking support
         # TODO(gsamfira): implement once we have networking support
@@ -77,7 +90,9 @@ class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools):
                     utils.get_exception_details()))
                     utils.get_exception_details()))
 
 
     def _rebuild_initrds(self):
     def _rebuild_initrds(self):
-        if float(self._version_id) < 12:
+        if self._version_supported_util(
+                self._detected_os_info['release_version'],
+                minimum=0, maximum=12):
             self._run_mkinitrd()
             self._run_mkinitrd()
         else:
         else:
             self._run_dracut()
             self._run_dracut()
@@ -113,10 +128,12 @@ class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools):
 
 
     def _add_cloud_tools_repo(self):
     def _add_cloud_tools_repo(self):
         repo_suffix = ""
         repo_suffix = ""
-        if self._version_id:
-            repo_suffix = "_%s" % self._version_id
+        if self._version:
+            repo_suffix = "_%s" % self._version
         repo = "obs://Cloud:Tools/%s%s" % (
         repo = "obs://Cloud:Tools/%s%s" % (
-            self._distro.replace(" ", "_"), repo_suffix)
+            self._detected_os_info[DETECTED_SUSE_RELEASE_FIELD_NAME].replace(
+                " ", "_"),
+            repo_suffix)
         self._event_manager.progress_update(
         self._event_manager.progress_update(
             "Adding repository: %s" % repo)
             "Adding repository: %s" % repo)
         try:
         try:

+ 19 - 6
coriolis/osmorphing/ubuntu.py

@@ -8,18 +8,31 @@ import yaml
 from oslo_log import log as logging
 from oslo_log import log as logging
 
 
 from coriolis.osmorphing import debian
 from coriolis.osmorphing import debian
+from coriolis.osmorphing.osdetect import ubuntu as ubuntu_osdetect
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
+UBUNTU_DISTRO_IDENTIFIER = ubuntu_osdetect.UBUNTU_DISTRO_IDENTIFIER
+
 
 
 class BaseUbuntuMorphingTools(debian.BaseDebianMorphingTools):
 class BaseUbuntuMorphingTools(debian.BaseDebianMorphingTools):
-    def _check_os(self):
-        config = self._read_config_file("etc/lsb-release", check_exists=True)
-        dist_id = config.get('DISTRIB_ID')
-        if dist_id == 'Ubuntu':
-            release = config.get('DISTRIB_RELEASE')
-            return (dist_id, release)
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        if detected_os_info['distribution_name'] != (
+                UBUNTU_DISTRO_IDENTIFIER):
+            return False
+
+        lts_releases = [12.04, 14.04, 16.04, 18.04]
+        for lts_release in lts_releases:
+            if cls._version_supported_util(
+                    detected_os_info['release_version'],
+                    minimum=lts_release, maximum=lts_release):
+                return True
+
+        return cls._version_supported_util(
+            detected_os_info['release_version'], minimum=20.04)
 
 
     def _set_netplan_ethernet_configs(
     def _set_netplan_ethernet_configs(
             self, nics_info, dhcp=False, iface_name_prefix=None):
             self, nics_info, dhcp=False, iface_name_prefix=None):

+ 54 - 69
coriolis/osmorphing/windows.py

@@ -1,19 +1,25 @@
 # Copyright 2016 Cloudbase Solutions Srl
 # Copyright 2016 Cloudbase Solutions Srl
 # All Rights Reserved.
 # All Rights Reserved.
 
 
-from distutils import version
+import copy
 import os
 import os
 import re
 import re
 import uuid
 import uuid
 
 
 from oslo_log import log as logging
 from oslo_log import log as logging
 
 
+from coriolis import constants
 from coriolis import exception
 from coriolis import exception
 from coriolis import utils
 from coriolis import utils
 from coriolis.osmorphing import base
 from coriolis.osmorphing import base
+from coriolis.osmorphing.osdetect import windows as windows_osdetect
+
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
+WINDOWS_CLIENT_IDENTIFIER = windows_osdetect.WINDOWS_CLIENT_IDENTIFIER
+WINDOWS_SERVER_IDENTIFIER = windows_osdetect.WINDOWS_SERVER_IDENTIFIER
+
 SERVICE_START_AUTO = 2
 SERVICE_START_AUTO = 2
 SERVICE_START_MANUAL = 3
 SERVICE_START_MANUAL = 3
 SERVICE_START_DISABLED = 4
 SERVICE_START_DISABLED = 4
@@ -59,38 +65,36 @@ CLOUDBASE_INIT_DEFAULT_METADATA_SVCS = [
     '.opennebulaservice.OpenNebulaService',
     '.opennebulaservice.OpenNebulaService',
 ]
 ]
 
 
+REQUIRED_DETECTED_WINDOWS_OS_FIELDS = [
+    "version_number", "edition_id", "installation_type", "product_name"]
+
 
 
 class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
 class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
+
+    @classmethod
+    def get_required_detected_os_info_fields(cls):
+        base_fields = copy.deepcopy(base.REQUIRED_DETECTED_OS_FIELDS)
+        base_fields.extend(REQUIRED_DETECTED_WINDOWS_OS_FIELDS)
+        return base_fields
+
+    @classmethod
+    def check_os_supported(cls, detected_os_info):
+        # TODO(aznashwan): add more detailed checks for Windows:
+        if detected_os_info['os_type'] == constants.OS_TYPE_WINDOWS:
+            return True
+        return False
+
     def __init__(
     def __init__(
             self, conn, os_root_dir, os_root_device, hypervisor,
             self, conn, os_root_dir, os_root_device, hypervisor,
-            event_manager):
+            event_manager, detected_os_info):
         super(BaseWindowsMorphingTools, self).__init__(
         super(BaseWindowsMorphingTools, self).__init__(
             conn, os_root_dir, os_root_device, hypervisor,
             conn, os_root_dir, os_root_device, hypervisor,
-            event_manager)
-
-        self._version_number = None
-        self._edition_id = None
-        self._installation_type = None
-        self._product_name = None
+            event_manager, detected_os_info)
 
 
-    def _check_os(self):
-        try:
-            (self._version_number,
-             self._edition_id,
-             self._installation_type,
-             self._product_name) = self._get_image_version_info()
-            LOG.debug(
-                "Identified Windows release as: Version no.: %s; "
-                "Edition id: %s; Install type: %s; Name: %s",
-                self._version_number, self._edition_id,
-                self._installation_type, self._product_name)
-            return ('Windows', self._product_name)
-        except exception.CoriolisException as ex:
-            LOG.debug("Exception during OS detection: %s", ex)
-
-    def set_net_config(self, nics_info, dhcp):
-        # TODO(alexpilotti): implement
-        pass
+        self._version_number = detected_os_info['version_number']
+        self._edition_id = detected_os_info['edition_id']
+        self._installation_type = detected_os_info['installation_type']
+        self._product_name = detected_os_info['product_name']
 
 
     def _get_worker_os_drive_path(self):
     def _get_worker_os_drive_path(self):
         return self._conn.exec_ps_command(
         return self._conn.exec_ps_command(
@@ -126,49 +130,6 @@ class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
         if m:
         if m:
             return m.groups()[0]
             return m.groups()[0]
 
 
-    def _get_image_version_info(self):
-        key_name = str(uuid.uuid4())
-
-        self._load_registry_hive(
-            "HKLM\\%s" % key_name,
-            "%sWindows\\System32\\config\\SOFTWARE" % self._os_root_dir)
-        try:
-            version_info_str = self._conn.exec_ps_command(
-                "Get-ItemProperty "
-                "'HKLM:\\%s\\Microsoft\\Windows NT\\CurrentVersion' "
-                "| select CurrentVersion, CurrentMajorVersionNumber, "
-                "CurrentMinorVersionNumber,  CurrentBuildNumber, "
-                "InstallationType, ProductName, EditionID | FL" %
-                key_name).replace(self._conn.EOL, os.linesep)
-        finally:
-            self._unload_registry_hive("HKLM\\%s" % key_name)
-
-        version_info = {}
-        for n in ["CurrentVersion", "CurrentMajorVersionNumber",
-                  "CurrentMinorVersionNumber", "CurrentBuildNumber",
-                  "InstallationType", "ProductName", "EditionID"]:
-            version_info[n] = self._get_ps_fl_value(version_info_str, n)
-
-        if (not version_info["CurrentMajorVersionNumber"] and
-                not version_info["CurrentVersion"]):
-            raise exception.CoriolisException(
-                "Cannot find Windows version info")
-
-        if version_info["CurrentMajorVersionNumber"]:
-            version_str = "%s.%s.%s" % (
-                version_info["CurrentMajorVersionNumber"],
-                version_info["CurrentMinorVersionNumber"],
-                version_info["CurrentBuildNumber"])
-        else:
-            version_str = "%s.%s" % (
-                version_info["CurrentVersion"],
-                version_info["CurrentBuildNumber"])
-
-        return (version.LooseVersion(version_str),
-                version_info["EditionID"],
-                version_info["InstallationType"],
-                version_info["ProductName"])
-
     def _add_dism_driver(self, driver_path):
     def _add_dism_driver(self, driver_path):
         LOG.info("Adding driver: %s" % driver_path)
         LOG.info("Adding driver: %s" % driver_path)
         dism_path = self._get_dism_path()
         dism_path = self._get_dism_path()
@@ -461,3 +422,27 @@ class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
             self._unload_registry_hive("HKLM\\%s" % key_name)
             self._unload_registry_hive("HKLM\\%s" % key_name)
 
 
         return cloudbaseinit_base_dir
         return cloudbaseinit_base_dir
+
+    def set_net_config(self, nics_info, dhcp):
+        pass
+
+    def get_packages(self):
+        return [], []
+
+    def pre_packages_install(self, package_names):
+        pass
+
+    def install_packages(self, package_names):
+        pass
+
+    def post_packages_install(self, package_names):
+        pass
+
+    def pre_packages_uninstall(self, package_names):
+        pass
+
+    def uninstall_packages(self, package_names):
+        pass
+
+    def post_packages_uninstall(self, package_names):
+        pass

+ 21 - 6
coriolis/providers/base.py

@@ -190,11 +190,26 @@ class BaseEndpointSourceOptionsProvider(
 
 
 class BaseInstanceProvider(BaseProvider):
 class BaseInstanceProvider(BaseProvider):
 
 
-    def get_os_morphing_tools(self, conn, osmorphing_info):
-        """ Returns a tuple containing the instantiated OSMorphing tools class
-        to use as well as the OS info returned by the tools' `check_os` method.
+    @abc.abstractmethod
+    def get_os_morphing_tools(self, os_type, osmorphing_info):
+        """ Returns a list of possible OSMorphing classes for the given
+        os type and osmorphing info.
+        The OSMorphing classes will be asked to validate compatibility
+        in order using their `check_os` method in order, so any classes whose
+        `check_os` classmethods might both return a positive result should be
+        placed in the correct order (from more specific to less specific).
         """
         """
-        raise exception.OSMorphingToolsNotFound()
+        raise exception.OSMorphingToolsNotFound(os_type=os_type)
+
+
+    def get_custom_os_detect_tools(self, os_type, osmorphing_info):
+        """ Returns a list of custom OSDetect classes which inherit from
+        coriolis.osmorphing.osdetect.base.BaseOSDetectTools.
+        These detect tools will be run before the standard ones already
+        present in the standard coriolis.osmorphing.osdetect module in case
+        there will be any provider-specific supported OS releases.
+        """
+        return []
 
 
 
 
 class BaseImportInstanceProvider(BaseInstanceProvider):
 class BaseImportInstanceProvider(BaseInstanceProvider):
@@ -438,14 +453,14 @@ def get_os_morphing_tools_helper(conn, os_morphing_tools_clss,
 
 
     for cls in os_morphing_tools_clss.get(
     for cls in os_morphing_tools_clss.get(
             os_type, itertools.chain(*os_morphing_tools_clss.values())):
             os_type, itertools.chain(*os_morphing_tools_clss.values())):
-        LOG.debug("Loading osmorphing instance: %s", cls)
+        LOG.debug("Checking using OSMorphing class: %s", cls)
         tools = cls(
         tools = cls(
             conn, os_root_dir, os_root_dev, hypervisor_type, event_manager)
             conn, os_root_dir, os_root_dev, hypervisor_type, event_manager)
         LOG.debug("Testing OS morphing tools: %s", cls.__name__)
         LOG.debug("Testing OS morphing tools: %s", cls.__name__)
         os_info = tools.check_os()
         os_info = tools.check_os()
         if os_info:
         if os_info:
             return (tools, os_info)
             return (tools, os_info)
-    raise exception.OSMorphingToolsNotFound()
+    raise exception.OSMorphingToolsNotFound(os_type=os_type)
 
 
 
 
 class BaseEndpointStorageProvider(object, with_metaclass(abc.ABCMeta)):
 class BaseEndpointStorageProvider(object, with_metaclass(abc.ABCMeta)):

+ 5 - 0
coriolis/schemas.py

@@ -40,6 +40,8 @@ _CORIOLIS_DISK_SYNC_RESOURCES_CONN_INFO_SCHEMA_NAME = (
     "disk_sync_resources_conn_info_schema.json")
     "disk_sync_resources_conn_info_schema.json")
 _CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA_NAME = (
 _CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA_NAME = (
     "replication_worker_conn_info_schema.json")
     "replication_worker_conn_info_schema.json")
+_CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA_NAME = (
+    "detected_os_morphing_info_schema.json")
 
 
 
 
 def get_schema(package_name, schema_name,
 def get_schema(package_name, schema_name,
@@ -133,3 +135,6 @@ CORIOLIS_DISK_SYNC_RESOURCES_CONN_INFO_SCHEMA = get_schema(
 
 
 CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA = get_schema(
 CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA = get_schema(
     __name__, _CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA_NAME)
     __name__, _CORIOLIS_REPLICATION_WORKER_CONN_INFO_SCHEMA_NAME)
+
+CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA = get_schema(
+    __name__, _CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA_NAME)

+ 24 - 0
coriolis/schemas/detected_os_morphing_info_schema.json

@@ -0,0 +1,24 @@
+{
+  "$schema": "http://cloudbase.it/coriolis/schemas/detected_os_morphing_info#",
+  "type": "object",
+  "description": "Schema defining the format the OS Morphing detection tools should return. Additional distro-specific properties may be freely added if needed.",
+  "properties": {
+    "os_type": {
+      "enum": ["bsd", "linux", "osx", "solaris", "windows"]
+    },
+    "distribution_name": {
+      "type": "string",
+      "description": "String identifier of the OS or distribution. Ex: 'Ubuntu' for Linux or 'Server' for Windows."
+    },
+    "release_version": {
+      "type": "string",
+      "description": "String version for the OS. (ex: '2012R2' for Windows or '18.04LTS' for Ubuntu Trusty)"
+    },
+    "friendly_release_name": {
+      "type": "string",
+      "description": "String friendly name for the OS. (ex: 'Suse Linux Enterprise 13.1' for SLES 13 SP1)"
+    }
+  },
+  "required": ["os_type", "distribution_name", "release_version", "friendly_release_name"],
+  "additionalProperties": true
+}

+ 23 - 0
coriolis/utils.py

@@ -649,6 +649,29 @@ def sanitize_task_info(task_info):
     return new
     return new
 
 
 
 
+def parse_ini_config(file_contents):
+    """ Parses the contents of the given .ini config file and
+    returns a dict with the options/values within it.
+    """
+    config = {}
+    regex_expr = '(.*[^-\\s])\\s*=\\s*(?:"|\')?([^"\']*)(?:"|\')?\\s*'
+    for config_line in file_contents.splitlines():
+        m = re.match(regex_expr, config_line)
+        if m:
+            name, value = m.groups()
+            config[name] = value
+    return config
+
+
+def read_ssh_ini_config_file(ssh, path, check_exists=False):
+    """ Reads and parses the contents of an .ini file at the given path. """
+    if not check_exists or test_ssh_path(ssh, path):
+        content = read_ssh_file(ssh, path).decode()
+        return parse_ini_config(content)
+    else:
+        return {}
+
+
 def _write_systemd(ssh, cmdline, svcname, run_as=None, start=True):
 def _write_systemd(ssh, cmdline, svcname, run_as=None, start=True):
     serviceFilePath = "/lib/systemd/system/%s.service" % svcname
     serviceFilePath = "/lib/systemd/system/%s.service" % svcname