diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py index 7d104b0..7a4cbdc 100644 --- a/os_brick/initiator/connector.py +++ b/os_brick/initiator/connector.py @@ -38,13 +38,11 @@ from oslo_concurrency import lockutils from oslo_concurrency import processutils as putils from oslo_log import log as logging from oslo_service import loopingcall +from oslo_utils import importutils from oslo_utils import strutils import six from six.moves import urllib -S390X = "s390x" -S390 = "s390" - from os_brick import exception from os_brick import executor from os_brick import utils @@ -54,7 +52,6 @@ from os_brick.initiator import linuxfc from os_brick.initiator import linuxrbd from os_brick.initiator import linuxscsi from os_brick.initiator import linuxsheepdog -from os_brick.privileged import rootwrap as priv_rootwrap from os_brick.remotefs import remotefs from os_brick.i18n import _, _LE, _LI, _LW @@ -66,6 +63,15 @@ MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$") MULTIPATH_DEV_CHECK_REGEX = re.compile("\s+dm-\d+\s+") MULTIPATH_PATH_CHECK_REGEX = re.compile("\s+\d+:\d+:\d+:\d+\s+") +PLATFORM_ALL = 'ALL' +PLATFORM_x86 = 'X86' +PLATFORM_S390 = 'S390' +OS_TYPE_ALL = 'ALL' +OS_TYPE_LINUX = 'LINUX' + +S390X = "s390x" +S390 = "s390" + ISCSI = "ISCSI" ISER = "ISER" FIBRE_CHANNEL = "FIBRE_CHANNEL" @@ -84,19 +90,21 @@ DISCO = "DISCO" VZSTORAGE = "VZSTORAGE" SHEEPDOG = "SHEEPDOG" - -def _check_multipathd_running(root_helper, enforce_multipath): - try: - priv_rootwrap.execute('multipathd', 'show', 'status', - run_as_root=True, root_helper=root_helper) - except putils.ProcessExecutionError as err: - LOG.error(_LE('multipathd is not running: exit code %(err)s'), - {'err': err.exit_code}) - if enforce_multipath: - raise - return False - - return True +connector_list = [ + 'os_brick.initiator.connector.InitiatorConnector', + 'os_brick.initiator.connector.ISCSIConnector', + 'os_brick.initiator.connector.FibreChannelConnector', + 'os_brick.initiator.connector.FibreChannelConnectorS390X', + 'os_brick.initiator.connector.AoEConnector', + 'os_brick.initiator.connector.RemoteFsConnector', + 'os_brick.initiator.connector.RBDConnector', + 'os_brick.initiator.connector.LocalConnector', + 'os_brick.initiator.connector.DRBDConnector', + 'os_brick.initiator.connector.HuaweiStorHyperConnector', + 'os_brick.initiator.connector.HGSTConnector', + 'os_brick.initiator.connector.ScaleIOConnector', + 'os_brick.initiator.connector.DISCOConnector', +] def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, @@ -123,32 +131,39 @@ def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, :type enforce_multipath: bool :returns: dict containing all of the collected initiator values. """ - - iscsi = ISCSIConnector(root_helper=root_helper) - fc = linuxfc.LinuxFibreChannel(root_helper=root_helper) - props = {} - props['ip'] = my_ip - props['host'] = host if host else socket.gethostname() - initiator = iscsi.get_initiator() - if initiator: - props['initiator'] = initiator - wwpns = fc.get_fc_wwpns() - if wwpns: - props['wwpns'] = wwpns - wwnns = fc.get_fc_wwnns() - if wwnns: - props['wwnns'] = wwnns - props['multipath'] = (multipath and - _check_multipathd_running(root_helper, - enforce_multipath)) props['platform'] = platform.machine() props['os_type'] = sys.platform + props['ip'] = my_ip + props['host'] = host if host else socket.gethostname() + + for item in connector_list: + connector = importutils.import_class(item) + + if (utils.platform_matches(props['platform'], connector.platform) and + utils.os_matches(props['os_type'], connector.os_type)): + LOG.debug("Fetching connector for %s" % connector.__name__) + props = utils.merge_dict(props, + connector.get_connector_properties( + root_helper, + host=host, + multipath=multipath, + enforce_multipath=enforce_multipath)) + return props @six.add_metaclass(abc.ABCMeta) class InitiatorConnector(executor.Executor): + + # This object can be used on any platform (x86, S390) + platform = PLATFORM_ALL + + # This object can be used on any os type (linux, windows) + # TODO(walter-boring) This class stil has a reliance on + # linuxscsi object, making it specific to linux. Need to fix that. + os_type = OS_TYPE_LINUX + def __init__(self, root_helper, driver=None, execute=None, device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, *args, **kwargs): @@ -162,9 +177,21 @@ class InitiatorConnector(executor.Executor): def set_driver(self, driver): """The driver is used to find used LUNs.""" - self.driver = driver + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The generic connector properties.""" + multipath = kwargs['multipath'] + enforce_multipath = kwargs['enforce_multipath'] + props = {} + # TODO(walter-boring) move this into platform specific lib + props['multipath'] = (multipath and + linuxscsi.LinuxSCSI.is_multipath_running( + enforce_multipath, root_helper)) + + return props + @staticmethod def factory(protocol, root_helper, driver=None, use_multipath=False, @@ -514,6 +541,17 @@ class ISCSIConnector(InitiatorConnector): self.use_multipath = use_multipath self.transport = self._validate_iface_transport(transport) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The iSCSI connector properties.""" + props = {} + iscsi = ISCSIConnector(root_helper=root_helper) + initiator = iscsi.get_initiator() + if initiator: + props['initiator'] = initiator + + return props + def get_search_path(self): """Where do we look for iSCSI based volumes.""" return '/dev/disk/by-path' @@ -1318,6 +1356,21 @@ class FibreChannelConnector(InitiatorConnector): self._linuxscsi.set_execute(execute) self._linuxfc.set_execute(execute) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Fibre Channel connector properties.""" + props = {} + fc = linuxfc.LinuxFibreChannel(root_helper) + + wwpns = fc.get_fc_wwpns() + if wwpns: + props['wwpns'] = wwpns + wwnns = fc.get_fc_wwnns() + if wwnns: + props['wwnns'] = wwnns + + return props + def get_search_path(self): """Where do we look for FC based volumes.""" return '/dev/disk/by-path' @@ -1550,6 +1603,7 @@ class FibreChannelConnector(InitiatorConnector): class FibreChannelConnectorS390X(FibreChannelConnector): """Connector class to attach/detach Fibre Channel volumes on S390X arch.""" + platform = PLATFORM_S390 def __init__(self, root_helper, driver=None, execute=None, use_multipath=False, @@ -1613,6 +1667,7 @@ class FibreChannelConnectorS390X(FibreChannelConnector): class AoEConnector(InitiatorConnector): """Connector class to attach/detach AoE volumes.""" + def __init__(self, root_helper, driver=None, device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, *args, **kwargs): @@ -1622,6 +1677,11 @@ class AoEConnector(InitiatorConnector): device_scan_attempts=device_scan_attempts, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The AoE connector properties.""" + return {} + def get_search_path(self): return '/dev/etherd' @@ -1785,6 +1845,11 @@ class RemoteFsConnector(InitiatorConnector): device_scan_attempts=device_scan_attempts, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The RemoteFS connector properties.""" + return {} + def set_execute(self, execute): super(RemoteFsConnector, self).set_execute(execute) self._remotefsclient.set_execute(execute) @@ -1852,6 +1917,11 @@ class RBDConnector(InitiatorConnector): device_scan_attempts, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The RBD connector properties.""" + return {} + def get_volume_paths(self, connection_properties): # TODO(walter-boring): don't know where the connector # looks for RBD volumes. @@ -1938,6 +2008,11 @@ class LocalConnector(InitiatorConnector): super(LocalConnector, self).__init__(root_helper, driver=driver, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Local connector properties.""" + return {} + def get_volume_paths(self, connection_properties): path = connection_properties['device_path'] return [path] @@ -1987,6 +2062,20 @@ class LocalConnector(InitiatorConnector): class DRBDConnector(InitiatorConnector): """"Connector class to attach/detach DRBD resources.""" + def __init__(self, root_helper, driver=None, + execute=putils.execute, *args, **kwargs): + + super(DRBDConnector, self).__init__(root_helper, driver=driver, + execute=execute, *args, **kwargs) + + self._execute = execute + self._root_helper = root_helper + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The DRBD connector properties.""" + return {} + def check_valid_device(self, path, run_as_root=True): """Verify an existing volume.""" # TODO(linbit): check via drbdsetup first, to avoid blocking/hanging @@ -2058,6 +2147,7 @@ class DRBDConnector(InitiatorConnector): class HuaweiStorHyperConnector(InitiatorConnector): """"Connector class to attach/detach SDSHypervisor volumes.""" + attached_success_code = 0 has_been_attached_code = 50151401 attach_mnid_done_code = 50151405 @@ -2080,6 +2170,11 @@ class HuaweiStorHyperConnector(InitiatorConnector): driver=driver, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The HuaweiStor connector properties.""" + return {} + def get_search_path(self): # TODO(walter-boring): Where is the location on the filesystem to # look for Huawei volumes to show up? @@ -2216,6 +2311,7 @@ class HuaweiStorHyperConnector(InitiatorConnector): class HGSTConnector(InitiatorConnector): """Connector class to attach/detach HGST volumes.""" + VGCCLUSTER = 'vgc-cluster' def __init__(self, root_helper, driver=None, @@ -2227,6 +2323,11 @@ class HGSTConnector(InitiatorConnector): *args, **kwargs) self._vgc_host = None + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The HGST connector properties.""" + return {} + def _log_cli_err(self, err): """Dumps the full command output to a logfile in error cases.""" LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n" @@ -2363,6 +2464,7 @@ class HGSTConnector(InitiatorConnector): class ScaleIOConnector(InitiatorConnector): """Class implements the connector driver for ScaleIO.""" + OK_STATUS_CODE = 200 VOLUME_NOT_MAPPED_ERROR = 84 VOLUME_ALREADY_MAPPED_ERROR = 81 @@ -2390,6 +2492,11 @@ class ScaleIOConnector(InitiatorConnector): self.iops_limit = None self.bandwidth_limit = None + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The ScaleIO connector properties.""" + return {} + def get_search_path(self): return "/dev/disk/by-id" @@ -2830,6 +2937,11 @@ class DISCOConnector(InitiatorConnector): self.server_port = None self.server_ip = None + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The DISCO connector properties.""" + return {} + def get_search_path(self): """Get directory path where to get DISCO volumes.""" return "/dev" @@ -2990,6 +3102,11 @@ class SheepdogConnector(InitiatorConnector): device_scan_attempts, *args, **kwargs) + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Sheepdog connector properties.""" + return {} + def get_volume_paths(self, connection_properties): # TODO(lixiaoy1): don't know where the connector # looks for sheepdog volumes. diff --git a/os_brick/initiator/linuxscsi.py b/os_brick/initiator/linuxscsi.py index b891b66..9feb094 100644 --- a/os_brick/initiator/linuxscsi.py +++ b/os_brick/initiator/linuxscsi.py @@ -25,8 +25,10 @@ from oslo_log import log as logging from os_brick import exception from os_brick import executor +from os_brick.i18n import _LE from os_brick.i18n import _LI from os_brick.i18n import _LW +from os_brick.privileged import rootwrap as priv_rootwrap from os_brick import utils LOG = logging.getLogger(__name__) @@ -110,6 +112,21 @@ class LinuxSCSI(executor.Executor): root_helper=self._root_helper) return out.strip() + @staticmethod + def is_multipath_running(enforce_multipath, root_helper): + try: + priv_rootwrap.execute('multipathd', 'show', 'status', + run_as_root=True, + root_helper=root_helper) + except putils.ProcessExecutionError as err: + LOG.error(_LE('multipathd is not running: exit code %(err)s'), + {'err': err.exit_code}) + if enforce_multipath: + raise + return False + + return True + def remove_multipath_device(self, device): """This removes LUNs associated with a multipath device and the multipath device itself. diff --git a/os_brick/tests/initiator/test_connector.py b/os_brick/tests/initiator/test_connector.py index d519805..904027e 100644 --- a/os_brick/tests/initiator/test_connector.py +++ b/os_brick/tests/initiator/test_connector.py @@ -14,6 +14,7 @@ import os.path import platform +import sys import tempfile import time @@ -77,6 +78,29 @@ class ConnectorUtilsTestCase(base.TestCase): 'platform': platform} self.assertEqual(props, props_actual) + def test_brick_get_connector_properties_connectors_called(self): + """Make sure every connector is called.""" + + mock_list = [] + # Make sure every connector is called + for item in connector.connector_list: + patched = mock.MagicMock() + patched.platform = platform.machine() + patched.os_type = sys.platform + patched.__name__ = item + patched.get_connector_properties.return_value = {} + patcher = mock.patch(item, new=patched) + patcher.start() + self.addCleanup(patcher.stop) + mock_list.append(patched) + + connector.get_connector_properties('sudo', + MY_IP, + True, True) + + for item in mock_list: + assert item.get_connector_properties.called + def test_brick_get_connector_properties(self): self._test_brick_get_connector_properties(False, False, False) @@ -138,6 +162,37 @@ class ConnectorTestCase(base.TestCase): def test_disconnect_volume(self): self.connector = connector.FakeConnector(None) + def test_get_connector_properties(self): + with mock.patch.object(priv_rootwrap, 'execute') as mock_exec: + mock_exec.return_value = True + multipath = True + enforce_multipath = True + props = connector.InitiatorConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + expected_props = {'multipath': True} + self.assertEqual(expected_props, props) + + multipath = False + enforce_multipath = True + props = connector.InitiatorConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + expected_props = {'multipath': False} + self.assertEqual(expected_props, props) + + with mock.patch.object(priv_rootwrap, 'execute', + side_effect=putils.ProcessExecutionError): + multipath = True + enforce_multipath = True + self.assertRaises( + putils.ProcessExecutionError, + connector.InitiatorConnector.get_connector_properties, + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + def test_factory(self): obj = connector.InitiatorConnector.factory('iscsi', None) self.assertEqual(obj.__class__.__name__, "ISCSIConnector") @@ -217,6 +272,7 @@ class ISCSIConnectorTestCase(ConnectorTestCase): mock.patch.object(self.connector._linuxscsi, 'get_name_from_path', return_value="/dev/sdb").start() self.addCleanup(mock.patch.stopall) + self._fake_iqn = 'iqn.1234-56.foo.bar:01:23456789abc' def generate_device(self, location, iqn, transport=None, lun=1): dev_format = "ip-%s-iscsi-%s-lun-%s" % (location, iqn, lun) @@ -267,29 +323,41 @@ class ISCSIConnectorTestCase(ConnectorTestCase): } } + def _initiator_get_text(self, *arg, **kwargs): + text = ('## DO NOT EDIT OR REMOVE THIS FILE!\n' + '## If you remove this file, the iSCSI daemon ' + 'will not start.\n' + '## If you change the InitiatorName, existing ' + 'access control lists\n' + '## may reject this initiator. The InitiatorName must ' + 'be unique\n' + '## for each iSCSI initiator. Do NOT duplicate iSCSI ' + 'InitiatorNames.\n' + 'InitiatorName=%s' % self._fake_iqn) + return text, None + def test_get_initiator(self): def initiator_no_file(*args, **kwargs): raise putils.ProcessExecutionError('No file') - def initiator_get_text(*arg, **kwargs): - text = ('## DO NOT EDIT OR REMOVE THIS FILE!\n' - '## If you remove this file, the iSCSI daemon ' - 'will not start.\n' - '## If you change the InitiatorName, existing ' - 'access control lists\n' - '## may reject this initiator. The InitiatorName must ' - 'be unique\n' - '## for each iSCSI initiator. Do NOT duplicate iSCSI ' - 'InitiatorNames.\n' - 'InitiatorName=iqn.1234-56.foo.bar:01:23456789abc') - return text, None - self.connector._execute = initiator_no_file initiator = self.connector.get_initiator() self.assertIsNone(initiator) - self.connector._execute = initiator_get_text + self.connector._execute = self._initiator_get_text initiator = self.connector.get_initiator() - self.assertEqual(initiator, 'iqn.1234-56.foo.bar:01:23456789abc') + self.assertEqual(initiator, self._fake_iqn) + + def test_get_connector_properties(self): + with mock.patch.object(priv_rootwrap, 'execute') as mock_exec: + mock_exec.return_value = self._initiator_get_text() + multipath = True + enforce_multipath = True + props = connector.ISCSIConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + expected_props = {'initiator': self._fake_iqn} + self.assertEqual(expected_props, props) @mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm_bare') def test_brick_iscsi_validate_transport(self, mock_iscsiadm): @@ -1185,6 +1253,20 @@ class FibreChannelConnectorTestCase(ConnectorTestCase): 'target_lun': 1, }} + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + def test_get_connector_properties(self, mock_hbas): + mock_hbas.return_value = self.fake_get_fc_hbas() + multipath = True + enforce_multipath = True + props = connector.FibreChannelConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + hbas = self.fake_get_fc_hbas() + expected_props = {'wwpns': [hbas[0]['port_name'].replace('0x', '')], + 'wwnns': [hbas[0]['node_name'].replace('0x', '')]} + self.assertEqual(expected_props, props) + def test_get_search_path(self): search_path = self.connector.get_search_path() expected = "/dev/disk/by-path" @@ -1592,6 +1674,13 @@ class AoEConnectorTestCase(ConnectorTestCase): paths = self.connector.get_volume_paths(self.connection_properties) self.assertEqual(expected, paths) + def test_get_connector_properties(self): + props = connector.AoEConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + @mock.patch.object(os.path, 'exists', side_effect=[True, True]) def test_connect_volume(self, exists_mock): """Ensure that if path exist aoe-revalidate was called.""" @@ -1669,6 +1758,13 @@ class RemoteFsConnectorTestCase(ConnectorTestCase): connector.RemoteFsConnector('scality', root_helper='sudo') self.assertEqual(1, mock_scality_remotefs_client.call_count) + def test_get_connector_properties(self): + props = connector.RemoteFsConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_get_search_path(self): expected = self.TEST_BASE actual = self.connector.get_search_path() @@ -1707,6 +1803,13 @@ class LocalConnectorTestCase(ConnectorTestCase): 'device_path': '/tmp/bar'} self.connector = connector.LocalConnector(None) + def test_get_connector_properties(self): + props = connector.LocalConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_get_search_path(self): actual = self.connector.get_search_path() self.assertIsNone(actual) @@ -1802,6 +1905,13 @@ class HuaweiStorHyperConnectorTestCase(ConnectorTestCase): HuaweiStorHyperConnectorTestCase.attached = True return 'ret_code=330155007', None + def test_get_connector_properties(self): + props = connector.HuaweiStorHyperConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_get_search_path(self): actual = self.connector.get_search_path() self.assertIsNone(actual) @@ -2054,6 +2164,13 @@ Request Succeeded self.assertEqual('space', dev_info['device']) self.assertEqual('/dev/space', dev_info['path']) + def test_get_connector_properties(self): + props = connector.HGSTConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_connect_volume_nohost_fail(self): """This host should not be found, connect should fail.""" self._fail_set_apphosts = False @@ -2169,6 +2286,13 @@ class RBDConnectorTestCase(ConnectorTestCase): actual = rbd.get_volume_paths(self.connection_properties) self.assertEqual(expected, actual) + def test_get_connector_properties(self): + props = connector.RBDConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + @mock.patch('os_brick.initiator.linuxrbd.rbd') @mock.patch('os_brick.initiator.linuxrbd.rados') def test_connect_volume(self, mock_rados, mock_rbd): @@ -2240,6 +2364,13 @@ class DRBDConnectorTestCase(ConnectorTestCase): # out, err return ('', '') + def test_get_connector_properties(self): + props = connector.DRBDConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_connect_volume(self): """Test connect_volume.""" @@ -2417,6 +2548,13 @@ class ScaleIOConnectorTestCase(ConnectorTestCase): self.fake_connection_properties) self.assertEqual(expected, actual) + def test_get_connector_properties(self): + props = connector.ScaleIOConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_connect_volume(self): """Successful connect to volume""" self.connector.connect_volume(self.fake_connection_properties) @@ -2584,6 +2722,13 @@ class DISCOConnectorTestCase(ConnectorTestCase): volume_path = ''.join(volume_items) return [volume_path] + def test_get_connector_properties(self): + props = connector.DISCOConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_get_search_path(self): """DISCO volumes should be under /dev.""" expected = "/dev" @@ -2665,6 +2810,13 @@ class SheepdogConnectorTestCase(ConnectorTestCase): 'ports': self.ports, } + def test_get_connector_properties(self): + props = connector.SheepdogConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + def test_get_search_path(self): sheepdog = connector.SheepdogConnector(None) path = sheepdog.get_search_path() diff --git a/os_brick/utils.py b/os_brick/utils.py index 5bd7f61..93103fb 100644 --- a/os_brick/utils.py +++ b/os_brick/utils.py @@ -56,3 +56,42 @@ def retry(exceptions, interval=1, retries=3, backoff_rate=2): return _wrapper return _decorator + + +def platform_matches(current_platform, connector_platform): + curr_p = current_platform.upper() + conn_p = connector_platform.upper() + if conn_p == 'ALL': + return True + + # Add tests against families of platforms + if curr_p == conn_p: + return True + + return False + + +def os_matches(current_os, connector_os): + curr_os = current_os.upper() + conn_os = connector_os.upper() + if conn_os == 'ALL': + return True + + # add tests against OSs + if (conn_os == curr_os or + conn_os in curr_os): + return True + + return False + + +def merge_dict(dict1, dict2): + """Try to safely merge 2 dictionaries.""" + if type(dict1) is not dict: + raise Exception("dict1 is not a dictionary") + if type(dict2) is not dict: + raise Exception("dict2 is not a dictionary") + + dict3 = dict1.copy() + dict3.update(dict2) + return dict3