diff options
author | Zuul <zuul@review.opendev.org> | 2020-03-17 22:25:46 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2020-03-17 22:25:46 +0000 |
commit | 70f3cd2aa4b461aaad23e6badf7446eb9c23f501 (patch) | |
tree | a937533c4b4629454c345081926e9d5fe5b051cb | |
parent | 3eb0c164011a744126330de44413ab704b55ea83 (diff) | |
parent | fb79ea2c4018ea190ee5202cb45221868ee8e31c (diff) | |
download | ironic-70f3cd2aa4b461aaad23e6badf7446eb9c23f501.tar.gz |
Merge "Refactoring: move iSCSI deploy code to iscsi_deploy.py"
-rw-r--r-- | ironic/drivers/modules/deploy_utils.py | 281 | ||||
-rw-r--r-- | ironic/drivers/modules/iscsi_deploy.py | 281 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/modules/test_deploy_utils.py | 573 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/modules/test_iscsi_deploy.py | 565 |
4 files changed, 838 insertions, 862 deletions
diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 301f84ed7..582c33423 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -14,20 +14,15 @@ # under the License. -import contextlib -import glob import os import re import time -from ironic_lib import disk_utils from ironic_lib import metrics_utils from ironic_lib import utils as il_utils -from oslo_concurrency import processutils from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import fileutils -from oslo_utils import netutils from oslo_utils import strutils from ironic.common import exception @@ -86,12 +81,6 @@ def _get_ironic_session(): return _IRONIC_SESSION -def _wrap_ipv6(ip): - if netutils.is_valid_ipv6(ip): - return "[%s]" % ip - return ip - - def get_ironic_api_url(): """Resolve Ironic API endpoint @@ -127,145 +116,6 @@ def rescue_or_deploy_mode(node): else 'deploy') -def discovery(portal_address, portal_port): - """Do iSCSI discovery on portal.""" - utils.execute('iscsiadm', - '-m', 'discovery', - '-t', 'st', - '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), - run_as_root=True, - check_exit_code=[0], - attempts=5, - delay_on_retry=True) - - -def login_iscsi(portal_address, portal_port, target_iqn): - """Login to an iSCSI target.""" - utils.execute('iscsiadm', - '-m', 'node', - '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), - '-T', target_iqn, - '--login', - run_as_root=True, - check_exit_code=[0], - attempts=5, - delay_on_retry=True) - - error_occurred = False - try: - # Ensure the login complete - verify_iscsi_connection(target_iqn) - # force iSCSI initiator to re-read luns - force_iscsi_lun_update(target_iqn) - # ensure file system sees the block device - check_file_system_for_iscsi_device(portal_address, - portal_port, - target_iqn) - except (exception.InstanceDeployFailure, - processutils.ProcessExecutionError) as e: - with excutils.save_and_reraise_exception(): - error_occurred = True - LOG.error("Failed to login to an iSCSI target due to %s", e) - finally: - if error_occurred: - try: - logout_iscsi(portal_address, portal_port, target_iqn) - delete_iscsi(portal_address, portal_port, target_iqn) - except processutils.ProcessExecutionError as e: - LOG.warning("An error occurred when trying to cleanup " - "failed ISCSI session error %s", e) - - -def check_file_system_for_iscsi_device(portal_address, - portal_port, - target_iqn): - """Ensure the file system sees the iSCSI block device.""" - check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (portal_address, - portal_port, - target_iqn) - total_checks = CONF.iscsi.verify_attempts - for attempt in range(total_checks): - if os.path.exists(check_dir): - break - time.sleep(1) - if LOG.isEnabledFor(logging.DEBUG): - existing_devs = ', '.join(glob.iglob('/dev/disk/by-path/*iscsi*')) - LOG.debug("iSCSI connection not seen by file system. Rechecking. " - "Attempt %(attempt)d out of %(total)d. Available iSCSI " - "devices: %(devs)s.", - {"attempt": attempt + 1, - "total": total_checks, - "devs": existing_devs}) - else: - msg = _("iSCSI connection was not seen by the file system after " - "attempting to verify %d times.") % total_checks - LOG.error(msg) - raise exception.InstanceDeployFailure(msg) - - -def verify_iscsi_connection(target_iqn): - """Verify iscsi connection.""" - LOG.debug("Checking for iSCSI target to become active.") - - total_checks = CONF.iscsi.verify_attempts - for attempt in range(total_checks): - out, _err = utils.execute('iscsiadm', - '-m', 'node', - '-S', - run_as_root=True, - check_exit_code=[0]) - if target_iqn in out: - break - time.sleep(1) - LOG.debug("iSCSI connection not active. Rechecking. Attempt " - "%(attempt)d out of %(total)d", - {"attempt": attempt + 1, "total": total_checks}) - else: - msg = _("iSCSI connection did not become active after attempting to " - "verify %d times.") % total_checks - LOG.error(msg) - raise exception.InstanceDeployFailure(msg) - - -def force_iscsi_lun_update(target_iqn): - """force iSCSI initiator to re-read luns.""" - LOG.debug("Re-reading iSCSI luns.") - utils.execute('iscsiadm', - '-m', 'node', - '-T', target_iqn, - '-R', - run_as_root=True, - check_exit_code=[0]) - - -def logout_iscsi(portal_address, portal_port, target_iqn): - """Logout from an iSCSI target.""" - utils.execute('iscsiadm', - '-m', 'node', - '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), - '-T', target_iqn, - '--logout', - run_as_root=True, - check_exit_code=[0], - attempts=5, - delay_on_retry=True) - - -def delete_iscsi(portal_address, portal_port, target_iqn): - """Delete the iSCSI target.""" - # Retry delete until it succeeds (exit code 0) or until there is - # no longer a target to delete (exit code 21). - utils.execute('iscsiadm', - '-m', 'node', - '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), - '-T', target_iqn, - '-o', 'delete', - run_as_root=True, - check_exit_code=[0, 21], - attempts=5, - delay_on_retry=True) - - def _replace_lines_in_file(path, regex_pattern, replacement): with open(path) as f: lines = f.readlines() @@ -343,137 +193,6 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, iscsi_boot, ramdisk_boot, ipxe_enabled) -def get_dev(address, port, iqn, lun): - """Returns a device path for given parameters.""" - dev = ("/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" - % (address, port, iqn, lun)) - return dev - - -def deploy_partition_image( - address, port, iqn, lun, image_path, - root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid, - preserve_ephemeral=False, configdrive=None, - boot_option=None, boot_mode="bios", disk_label=None, - cpu_arch=""): - """All-in-one function to deploy a partition image to a node. - - :param address: The iSCSI IP address. - :param port: The iSCSI port number. - :param iqn: The iSCSI qualified name. - :param lun: The iSCSI logical unit number. - :param image_path: Path for the instance's disk image. - :param root_mb: Size of the root partition in megabytes. - :param swap_mb: Size of the swap partition in megabytes. - :param ephemeral_mb: Size of the ephemeral partition in megabytes. If 0, - no ephemeral partition will be created. - :param ephemeral_format: The type of file system to format the ephemeral - partition. - :param node_uuid: node's uuid. Used for logging. - :param preserve_ephemeral: If True, no filesystem is written to the - ephemeral block device, preserving whatever - content it had (if the partition table has - not changed). - :param configdrive: Optional. Base64 encoded Gzipped configdrive content - or configdrive HTTP URL. - :param boot_option: Can be "local" or "netboot". - "netboot" by default. - :param boot_mode: Can be "bios" or "uefi". "bios" by default. - :param disk_label: The disk label to be used when creating the - partition table. Valid values are: "msdos", - "gpt" or None; If None ironic will figure it - out according to the boot_mode parameter. - :param cpu_arch: Architecture of the node being deployed to. - :raises: InstanceDeployFailure if image virtual size is bigger than root - partition size. - :returns: a dictionary containing the following keys: - 'root uuid': UUID of root partition - 'efi system partition uuid': UUID of the uefi system partition - (if boot mode is uefi). - NOTE: If key exists but value is None, it means partition doesn't - exist. - """ - boot_option = boot_option or get_default_boot_option() - image_mb = disk_utils.get_image_mb(image_path) - if image_mb > root_mb: - msg = (_('Root partition is too small for requested image. Image ' - 'virtual size: %(image_mb)d MB, Root size: %(root_mb)d MB') - % {'image_mb': image_mb, 'root_mb': root_mb}) - raise exception.InstanceDeployFailure(msg) - - with _iscsi_setup_and_handle_errors(address, port, iqn, lun) as dev: - uuid_dict_returned = disk_utils.work_on_disk( - dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path, - node_uuid, preserve_ephemeral=preserve_ephemeral, - configdrive=configdrive, boot_option=boot_option, - boot_mode=boot_mode, disk_label=disk_label, cpu_arch=cpu_arch) - - return uuid_dict_returned - - -def deploy_disk_image(address, port, iqn, lun, - image_path, node_uuid, configdrive=None, - conv_flags=None): - """All-in-one function to deploy a whole disk image to a node. - - :param address: The iSCSI IP address. - :param port: The iSCSI port number. - :param iqn: The iSCSI qualified name. - :param lun: The iSCSI logical unit number. - :param image_path: Path for the instance's disk image. - :param node_uuid: node's uuid. - :param configdrive: Optional. Base64 encoded Gzipped configdrive content - or configdrive HTTP URL. - :param conv_flags: Optional. Add a flag that will modify the behaviour of - the image copy to disk. - :returns: a dictionary containing the key 'disk identifier' to identify - the disk which was used for deployment. - """ - with _iscsi_setup_and_handle_errors(address, port, iqn, - lun) as dev: - disk_utils.populate_image(image_path, dev, conv_flags=conv_flags) - - if configdrive: - disk_utils.create_config_drive_partition(node_uuid, dev, - configdrive) - - disk_identifier = disk_utils.get_disk_identifier(dev) - - return {'disk identifier': disk_identifier} - - -@contextlib.contextmanager -def _iscsi_setup_and_handle_errors(address, port, iqn, lun): - """Function that yields an iSCSI target device to work on. - - :param address: The iSCSI IP address. - :param port: The iSCSI port number. - :param iqn: The iSCSI qualified name. - :param lun: The iSCSI logical unit number. - """ - dev = get_dev(address, port, iqn, lun) - discovery(address, port) - login_iscsi(address, port, iqn) - if not disk_utils.is_block_device(dev): - raise exception.InstanceDeployFailure(_("Parent device '%s' not found") - % dev) - try: - yield dev - except processutils.ProcessExecutionError as err: - with excutils.save_and_reraise_exception(): - LOG.error("Deploy to address %s failed.", address) - LOG.error("Command: %s", err.cmd) - LOG.error("StdOut: %r", err.stdout) - LOG.error("StdErr: %r", err.stderr) - except exception.InstanceDeployFailure as e: - with excutils.save_and_reraise_exception(): - LOG.error("Deploy to address %s failed.", address) - LOG.error(e) - finally: - logout_iscsi(address, port, iqn) - delete_iscsi(address, port, iqn) - - def check_for_missing_params(info_dict, error_msg, param_prefix=''): """Check for empty params in the provided dictionary. diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index cb885ae51..f47b8a956 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -13,17 +13,24 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib +import glob +import os +import time from urllib import parse as urlparse from ironic_lib import disk_utils from ironic_lib import metrics_utils +from oslo_concurrency import processutils from oslo_log import log as logging from oslo_utils import excutils +from oslo_utils import netutils from ironic.common import dhcp_factory from ironic.common import exception from ironic.common.i18n import _ from ironic.common import states +from ironic.common import utils from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic.conf import CONF @@ -58,6 +65,276 @@ def _save_disk_layout(node, i_info): node.save() +def _wrap_ipv6(ip): + if netutils.is_valid_ipv6(ip): + return "[%s]" % ip + return ip + + +def discovery(portal_address, portal_port): + """Do iSCSI discovery on portal.""" + utils.execute('iscsiadm', + '-m', 'discovery', + '-t', 'st', + '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + +def login_iscsi(portal_address, portal_port, target_iqn): + """Login to an iSCSI target.""" + utils.execute('iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), + '-T', target_iqn, + '--login', + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + error_occurred = False + try: + # Ensure the login complete + verify_iscsi_connection(target_iqn) + # force iSCSI initiator to re-read luns + force_iscsi_lun_update(target_iqn) + # ensure file system sees the block device + check_file_system_for_iscsi_device(portal_address, + portal_port, + target_iqn) + except (exception.InstanceDeployFailure, + processutils.ProcessExecutionError) as e: + with excutils.save_and_reraise_exception(): + error_occurred = True + LOG.error("Failed to login to an iSCSI target due to %s", e) + finally: + if error_occurred: + try: + logout_iscsi(portal_address, portal_port, target_iqn) + delete_iscsi(portal_address, portal_port, target_iqn) + except processutils.ProcessExecutionError as e: + LOG.warning("An error occurred when trying to cleanup " + "failed ISCSI session error %s", e) + + +def check_file_system_for_iscsi_device(portal_address, + portal_port, + target_iqn): + """Ensure the file system sees the iSCSI block device.""" + check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (portal_address, + portal_port, + target_iqn) + total_checks = CONF.iscsi.verify_attempts + for attempt in range(total_checks): + if os.path.exists(check_dir): + break + time.sleep(1) + if LOG.isEnabledFor(logging.DEBUG): + existing_devs = ', '.join(glob.iglob('/dev/disk/by-path/*iscsi*')) + LOG.debug("iSCSI connection not seen by file system. Rechecking. " + "Attempt %(attempt)d out of %(total)d. Available iSCSI " + "devices: %(devs)s.", + {"attempt": attempt + 1, + "total": total_checks, + "devs": existing_devs}) + else: + msg = _("iSCSI connection was not seen by the file system after " + "attempting to verify %d times.") % total_checks + LOG.error(msg) + raise exception.InstanceDeployFailure(msg) + + +def verify_iscsi_connection(target_iqn): + """Verify iscsi connection.""" + LOG.debug("Checking for iSCSI target to become active.") + + total_checks = CONF.iscsi.verify_attempts + for attempt in range(total_checks): + out, _err = utils.execute('iscsiadm', + '-m', 'node', + '-S', + run_as_root=True, + check_exit_code=[0]) + if target_iqn in out: + break + time.sleep(1) + LOG.debug("iSCSI connection not active. Rechecking. Attempt " + "%(attempt)d out of %(total)d", + {"attempt": attempt + 1, "total": total_checks}) + else: + msg = _("iSCSI connection did not become active after attempting to " + "verify %d times.") % total_checks + LOG.error(msg) + raise exception.InstanceDeployFailure(msg) + + +def force_iscsi_lun_update(target_iqn): + """force iSCSI initiator to re-read luns.""" + LOG.debug("Re-reading iSCSI luns.") + utils.execute('iscsiadm', + '-m', 'node', + '-T', target_iqn, + '-R', + run_as_root=True, + check_exit_code=[0]) + + +def logout_iscsi(portal_address, portal_port, target_iqn): + """Logout from an iSCSI target.""" + utils.execute('iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), + '-T', target_iqn, + '--logout', + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + +def delete_iscsi(portal_address, portal_port, target_iqn): + """Delete the iSCSI target.""" + # Retry delete until it succeeds (exit code 0) or until there is + # no longer a target to delete (exit code 21). + utils.execute('iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (_wrap_ipv6(portal_address), portal_port), + '-T', target_iqn, + '-o', 'delete', + run_as_root=True, + check_exit_code=[0, 21], + attempts=5, + delay_on_retry=True) + + +@contextlib.contextmanager +def _iscsi_setup_and_handle_errors(address, port, iqn, lun): + """Function that yields an iSCSI target device to work on. + + :param address: The iSCSI IP address. + :param port: The iSCSI port number. + :param iqn: The iSCSI qualified name. + :param lun: The iSCSI logical unit number. + """ + dev = ("/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" + % (address, port, iqn, lun)) + discovery(address, port) + login_iscsi(address, port, iqn) + if not disk_utils.is_block_device(dev): + raise exception.InstanceDeployFailure(_("Parent device '%s' not found") + % dev) + try: + yield dev + except processutils.ProcessExecutionError as err: + with excutils.save_and_reraise_exception(): + LOG.error("Deploy to address %s failed.", address) + LOG.error("Command: %s", err.cmd) + LOG.error("StdOut: %r", err.stdout) + LOG.error("StdErr: %r", err.stderr) + except exception.InstanceDeployFailure as e: + with excutils.save_and_reraise_exception(): + LOG.error("Deploy to address %s failed.", address) + LOG.error(e) + finally: + logout_iscsi(address, port, iqn) + delete_iscsi(address, port, iqn) + + +def deploy_partition_image( + address, port, iqn, lun, image_path, + root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid, + preserve_ephemeral=False, configdrive=None, + boot_option=None, boot_mode="bios", disk_label=None, + cpu_arch=""): + """All-in-one function to deploy a partition image to a node. + + :param address: The iSCSI IP address. + :param port: The iSCSI port number. + :param iqn: The iSCSI qualified name. + :param lun: The iSCSI logical unit number. + :param image_path: Path for the instance's disk image. + :param root_mb: Size of the root partition in megabytes. + :param swap_mb: Size of the swap partition in megabytes. + :param ephemeral_mb: Size of the ephemeral partition in megabytes. If 0, + no ephemeral partition will be created. + :param ephemeral_format: The type of file system to format the ephemeral + partition. + :param node_uuid: node's uuid. Used for logging. + :param preserve_ephemeral: If True, no filesystem is written to the + ephemeral block device, preserving whatever + content it had (if the partition table has + not changed). + :param configdrive: Optional. Base64 encoded Gzipped configdrive content + or configdrive HTTP URL. + :param boot_option: Can be "local" or "netboot". + "netboot" by default. + :param boot_mode: Can be "bios" or "uefi". "bios" by default. + :param disk_label: The disk label to be used when creating the + partition table. Valid values are: "msdos", + "gpt" or None; If None ironic will figure it + out according to the boot_mode parameter. + :param cpu_arch: Architecture of the node being deployed to. + :raises: InstanceDeployFailure if image virtual size is bigger than root + partition size. + :returns: a dictionary containing the following keys: + 'root uuid': UUID of root partition + 'efi system partition uuid': UUID of the uefi system partition + (if boot mode is uefi). + NOTE: If key exists but value is None, it means partition doesn't + exist. + """ + boot_option = boot_option or deploy_utils.get_default_boot_option() + image_mb = disk_utils.get_image_mb(image_path) + if image_mb > root_mb: + msg = (_('Root partition is too small for requested image. Image ' + 'virtual size: %(image_mb)d MB, Root size: %(root_mb)d MB') + % {'image_mb': image_mb, 'root_mb': root_mb}) + raise exception.InstanceDeployFailure(msg) + + with _iscsi_setup_and_handle_errors(address, port, iqn, lun) as dev: + uuid_dict_returned = disk_utils.work_on_disk( + dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path, + node_uuid, preserve_ephemeral=preserve_ephemeral, + configdrive=configdrive, boot_option=boot_option, + boot_mode=boot_mode, disk_label=disk_label, cpu_arch=cpu_arch) + + return uuid_dict_returned + + +def deploy_disk_image(address, port, iqn, lun, + image_path, node_uuid, configdrive=None, + conv_flags=None): + """All-in-one function to deploy a whole disk image to a node. + + :param address: The iSCSI IP address. + :param port: The iSCSI port number. + :param iqn: The iSCSI qualified name. + :param lun: The iSCSI logical unit number. + :param image_path: Path for the instance's disk image. + :param node_uuid: node's uuid. + :param configdrive: Optional. Base64 encoded Gzipped configdrive content + or configdrive HTTP URL. + :param conv_flags: Optional. Add a flag that will modify the behaviour of + the image copy to disk. + :returns: a dictionary containing the key 'disk identifier' to identify + the disk which was used for deployment. + """ + with _iscsi_setup_and_handle_errors(address, port, iqn, + lun) as dev: + disk_utils.populate_image(image_path, dev, conv_flags=conv_flags) + + if configdrive: + disk_utils.create_config_drive_partition(node_uuid, dev, + configdrive) + + disk_identifier = disk_utils.get_disk_identifier(dev) + + return {'disk identifier': disk_identifier} + + @METRICS.timer('check_image_size') def check_image_size(task): """Check if the requested image is larger than the root partition size. @@ -203,9 +480,9 @@ def continue_deploy(task, **kwargs): uuid_dict_returned = {} try: if node.driver_internal_info['is_whole_disk_image']: - uuid_dict_returned = deploy_utils.deploy_disk_image(**params) + uuid_dict_returned = deploy_disk_image(**params) else: - uuid_dict_returned = deploy_utils.deploy_partition_image(**params) + uuid_dict_returned = deploy_partition_image(**params) except exception.IronicException as e: with excutils.save_and_reraise_exception(): LOG.error('Deploy of instance %(instance)s on node %(node)s ' diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 3d5ee4e1c..be2472c89 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -16,17 +16,12 @@ import os import tempfile -import time -import types import fixtures -from ironic_lib import disk_utils import mock -from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import fileutils from oslo_utils import uuidutils -import testtools from ironic.common import boot_devices from ironic.common import exception @@ -345,533 +340,6 @@ menuentry "boot_whole_disk" { """ -@mock.patch.object(time, 'sleep', lambda seconds: None) -class PhysicalWorkTestCase(tests_base.TestCase): - - def _mock_calls(self, name_list, module): - patch_list = [mock.patch.object(module, name, - spec_set=types.FunctionType) - for name in name_list] - mock_list = [patcher.start() for patcher in patch_list] - for patcher in patch_list: - self.addCleanup(patcher.stop) - - parent_mock = mock.MagicMock(spec=[]) - for mocker, name in zip(mock_list, name_list): - parent_mock.attach_mock(mocker, name) - return parent_mock - - @mock.patch.object(disk_utils, 'work_on_disk', autospec=True) - @mock.patch.object(disk_utils, 'is_block_device', autospec=True) - @mock.patch.object(disk_utils, 'get_image_mb', autospec=True) - @mock.patch.object(utils, 'logout_iscsi', autospec=True) - @mock.patch.object(utils, 'login_iscsi', autospec=True) - @mock.patch.object(utils, 'get_dev', autospec=True) - @mock.patch.object(utils, 'discovery', autospec=True) - @mock.patch.object(utils, 'delete_iscsi', autospec=True) - def _test_deploy_partition_image(self, - mock_delete_iscsi, - mock_discovery, - mock_get_dev, - mock_login_iscsi, - mock_logout_iscsi, - mock_get_image_mb, - mock_is_block_device, - mock_work_on_disk, **kwargs): - # Below are the only values we allow callers to modify for testing. - # Check that values other than this aren't passed in. - deploy_args = { - 'boot_mode': None, - 'boot_option': None, - 'configdrive': None, - 'cpu_arch': None, - 'disk_label': None, - 'ephemeral_format': None, - 'ephemeral_mb': None, - 'image_mb': 1, - 'preserve_ephemeral': False, - 'root_mb': 128, - 'swap_mb': 64 - } - disallowed_values = set(kwargs) - set(deploy_args) - if disallowed_values: - raise ValueError("Only the following kwargs are allowed in " - "_test_deploy_partition_image: %(allowed)s. " - "Disallowed values: %(disallowed)s." - % {"allowed": ", ".join(deploy_args), - "disallowed": ", ".join(disallowed_values)}) - deploy_args.update(kwargs) - - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - image_path = '/tmp/xyz/image' - node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" - dev = '/dev/fake' - root_uuid = '12345678-1234-1234-12345678-12345678abcdef' - - mock_get_dev.return_value = dev - mock_is_block_device.return_value = True - mock_get_image_mb.return_value = deploy_args['image_mb'] - mock_work_on_disk.return_value = { - 'root uuid': root_uuid, - 'efi system partition uuid': None - } - - deploy_kwargs = { - 'boot_mode': deploy_args['boot_mode'], - 'boot_option': deploy_args['boot_option'], - 'configdrive': deploy_args['configdrive'], - 'disk_label': deploy_args['disk_label'], - 'cpu_arch': deploy_args['cpu_arch'] or '', - 'preserve_ephemeral': deploy_args['preserve_ephemeral'] - } - utils.deploy_partition_image( - address, port, iqn, lun, image_path, deploy_args['root_mb'], - deploy_args['swap_mb'], deploy_args['ephemeral_mb'], - deploy_args['ephemeral_format'], node_uuid, **deploy_kwargs) - - mock_get_dev.assert_called_once_with(address, port, iqn, lun) - mock_discovery.assert_called_once_with(address, port) - mock_login_iscsi.assert_called_once_with(address, port, iqn) - mock_logout_iscsi.assert_called_once_with(address, port, iqn) - mock_delete_iscsi.assert_called_once_with(address, port, iqn) - mock_get_image_mb.assert_called_once_with(image_path) - mock_is_block_device.assert_called_once_with(dev) - - work_on_disk_kwargs = { - 'preserve_ephemeral': deploy_args['preserve_ephemeral'], - 'configdrive': deploy_args['configdrive'], - # boot_option defaults to 'netboot' if - # not set - 'boot_option': deploy_args['boot_option'] or 'netboot', - 'boot_mode': deploy_args['boot_mode'], - 'disk_label': deploy_args['disk_label'], - 'cpu_arch': deploy_args['cpu_arch'] or '' - } - mock_work_on_disk.assert_called_once_with( - dev, deploy_args['root_mb'], deploy_args['swap_mb'], - deploy_args['ephemeral_mb'], deploy_args['ephemeral_format'], - image_path, node_uuid, **work_on_disk_kwargs) - - def test_deploy_partition_image_without_boot_option(self): - self._test_deploy_partition_image() - - def test_deploy_partition_image_netboot(self): - self._test_deploy_partition_image(boot_option="netboot") - - def test_deploy_partition_image_localboot(self): - self._test_deploy_partition_image(boot_option="local") - - def test_deploy_partition_image_wo_boot_option_and_wo_boot_mode(self): - self._test_deploy_partition_image() - - def test_deploy_partition_image_netboot_bios(self): - self._test_deploy_partition_image(boot_option="netboot", - boot_mode="bios") - - def test_deploy_partition_image_localboot_bios(self): - self._test_deploy_partition_image(boot_option="local", - boot_mode="bios") - - def test_deploy_partition_image_netboot_uefi(self): - self._test_deploy_partition_image(boot_option="netboot", - boot_mode="uefi") - - def test_deploy_partition_image_disk_label(self): - self._test_deploy_partition_image(disk_label='gpt') - - def test_deploy_partition_image_image_exceeds_root_partition(self): - self.assertRaises(exception.InstanceDeployFailure, - self._test_deploy_partition_image, image_mb=129, - root_mb=128) - - def test_deploy_partition_image_localboot_uefi(self): - self._test_deploy_partition_image(boot_option="local", - boot_mode="uefi") - - def test_deploy_partition_image_without_swap(self): - self._test_deploy_partition_image(swap_mb=0) - - def test_deploy_partition_image_with_ephemeral(self): - self._test_deploy_partition_image(ephemeral_format='exttest', - ephemeral_mb=256) - - def test_deploy_partition_image_preserve_ephemeral(self): - self._test_deploy_partition_image(ephemeral_format='exttest', - ephemeral_mb=256, - preserve_ephemeral=True) - - def test_deploy_partition_image_with_configdrive(self): - self._test_deploy_partition_image(configdrive='http://1.2.3.4/cd') - - def test_deploy_partition_image_with_cpu_arch(self): - self._test_deploy_partition_image(cpu_arch='generic') - - @mock.patch.object(disk_utils, 'create_config_drive_partition', - autospec=True) - @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) - def test_deploy_whole_disk_image(self, mock_gdi, create_config_drive_mock): - """Check loosely all functions are called with right args.""" - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - image_path = '/tmp/xyz/image' - node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" - - dev = '/dev/fake' - utils_name_list = ['get_dev', 'discovery', 'login_iscsi', - 'logout_iscsi', 'delete_iscsi'] - disk_utils_name_list = ['is_block_device', 'populate_image'] - - utils_mock = self._mock_calls(utils_name_list, utils) - utils_mock.get_dev.return_value = dev - - disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) - disk_utils_mock.is_block_device.return_value = True - mock_gdi.return_value = '0x12345678' - utils_calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.discovery(address, port), - mock.call.login_iscsi(address, port, iqn), - mock.call.logout_iscsi(address, port, iqn), - mock.call.delete_iscsi(address, port, iqn)] - disk_utils_calls_expected = [mock.call.is_block_device(dev), - mock.call.populate_image(image_path, dev, - conv_flags=None)] - uuid_dict_returned = utils.deploy_disk_image(address, port, iqn, lun, - image_path, node_uuid) - - self.assertEqual(utils_calls_expected, utils_mock.mock_calls) - self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) - self.assertFalse(create_config_drive_mock.called) - self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) - - @mock.patch.object(disk_utils, 'create_config_drive_partition', - autospec=True) - @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) - def test_deploy_whole_disk_image_with_config_drive(self, mock_gdi, - create_partition_mock): - """Check loosely all functions are called with right args.""" - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - image_path = '/tmp/xyz/image' - node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" - config_url = 'http://1.2.3.4/cd' - - dev = '/dev/fake' - utils_list = ['get_dev', 'discovery', 'login_iscsi', 'logout_iscsi', - 'delete_iscsi'] - - disk_utils_list = ['is_block_device', 'populate_image'] - utils_mock = self._mock_calls(utils_list, utils) - disk_utils_mock = self._mock_calls(disk_utils_list, disk_utils) - utils_mock.get_dev.return_value = dev - disk_utils_mock.is_block_device.return_value = True - mock_gdi.return_value = '0x12345678' - utils_calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.discovery(address, port), - mock.call.login_iscsi(address, port, iqn), - mock.call.logout_iscsi(address, port, iqn), - mock.call.delete_iscsi(address, port, iqn)] - - disk_utils_calls_expected = [mock.call.is_block_device(dev), - mock.call.populate_image(image_path, dev, - conv_flags=None)] - - uuid_dict_returned = utils.deploy_disk_image(address, port, iqn, lun, - image_path, node_uuid, - configdrive=config_url) - - utils_mock.assert_has_calls(utils_calls_expected) - disk_utils_mock.assert_has_calls(disk_utils_calls_expected) - create_partition_mock.assert_called_once_with(node_uuid, dev, - config_url) - self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) - - @mock.patch.object(disk_utils, 'create_config_drive_partition', - autospec=True) - @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) - def test_deploy_whole_disk_image_sparse(self, mock_gdi, - create_config_drive_mock): - """Check loosely all functions are called with right args.""" - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - image_path = '/tmp/xyz/image' - node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" - - dev = '/dev/fake' - utils_name_list = ['get_dev', 'discovery', 'login_iscsi', - 'logout_iscsi', 'delete_iscsi'] - disk_utils_name_list = ['is_block_device', 'populate_image'] - - utils_mock = self._mock_calls(utils_name_list, utils) - utils_mock.get_dev.return_value = dev - - disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) - disk_utils_mock.is_block_device.return_value = True - mock_gdi.return_value = '0x12345678' - utils_calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.discovery(address, port), - mock.call.login_iscsi(address, port, iqn), - mock.call.logout_iscsi(address, port, iqn), - mock.call.delete_iscsi(address, port, iqn)] - disk_utils_calls_expected = [mock.call.is_block_device(dev), - mock.call.populate_image( - image_path, dev, conv_flags='sparse')] - - uuid_dict_returned = utils.deploy_disk_image(address, port, iqn, lun, - image_path, node_uuid, - configdrive=None, - conv_flags='sparse') - - self.assertEqual(utils_calls_expected, utils_mock.mock_calls) - self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) - self.assertFalse(create_config_drive_mock.called) - self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) - - @mock.patch.object(common_utils, 'execute', autospec=True) - def test_verify_iscsi_connection_raises(self, mock_exec): - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.abc', ''] - self.assertRaises(exception.InstanceDeployFailure, - utils.verify_iscsi_connection, iqn) - self.assertEqual(3, mock_exec.call_count) - - @mock.patch.object(common_utils, 'execute', autospec=True) - def test_verify_iscsi_connection_override_attempts(self, mock_exec): - utils.CONF.set_override('verify_attempts', 2, group='iscsi') - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.abc', ''] - self.assertRaises(exception.InstanceDeployFailure, - utils.verify_iscsi_connection, iqn) - self.assertEqual(2, mock_exec.call_count) - - @mock.patch.object(os.path, 'exists', autospec=True) - def test_check_file_system_for_iscsi_device_raises(self, mock_os): - iqn = 'iqn.xyz' - ip = "127.0.0.1" - port = "22" - mock_os.return_value = False - self.assertRaises(exception.InstanceDeployFailure, - utils.check_file_system_for_iscsi_device, - ip, port, iqn) - self.assertEqual(3, mock_os.call_count) - - @mock.patch.object(os.path, 'exists', autospec=True) - def test_check_file_system_for_iscsi_device(self, mock_os): - iqn = 'iqn.xyz' - ip = "127.0.0.1" - port = "22" - check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (ip, - port, - iqn) - - mock_os.return_value = True - utils.check_file_system_for_iscsi_device(ip, port, iqn) - mock_os.assert_called_once_with(check_dir) - - @mock.patch.object(common_utils, 'execute', autospec=True) - def test_verify_iscsi_connection(self, mock_exec): - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.xyz', ''] - utils.verify_iscsi_connection(iqn) - mock_exec.assert_called_once_with( - 'iscsiadm', - '-m', 'node', - '-S', - run_as_root=True, - check_exit_code=[0]) - - @mock.patch.object(common_utils, 'execute', autospec=True) - def test_force_iscsi_lun_update(self, mock_exec): - iqn = 'iqn.xyz' - utils.force_iscsi_lun_update(iqn) - mock_exec.assert_called_once_with( - 'iscsiadm', - '-m', 'node', - '-T', iqn, - '-R', - run_as_root=True, - check_exit_code=[0]) - - @mock.patch.object(common_utils, 'execute', autospec=True) - @mock.patch.object(utils, 'verify_iscsi_connection', autospec=True) - @mock.patch.object(utils, 'force_iscsi_lun_update', autospec=True) - @mock.patch.object(utils, 'check_file_system_for_iscsi_device', - autospec=True) - def test_login_iscsi_calls_verify_and_update(self, - mock_check_dev, - mock_update, - mock_verify, - mock_exec): - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.xyz', ''] - utils.login_iscsi(address, port, iqn) - mock_exec.assert_called_once_with( - 'iscsiadm', - '-m', 'node', - '-p', '%s:%s' % (address, port), - '-T', iqn, - '--login', - run_as_root=True, - check_exit_code=[0], - attempts=5, - delay_on_retry=True) - - mock_verify.assert_called_once_with(iqn) - mock_update.assert_called_once_with(iqn) - mock_check_dev.assert_called_once_with(address, port, iqn) - - @mock.patch.object(utils, 'LOG', autospec=True) - @mock.patch.object(common_utils, 'execute', autospec=True) - @mock.patch.object(utils, 'verify_iscsi_connection', autospec=True) - @mock.patch.object(utils, 'force_iscsi_lun_update', autospec=True) - @mock.patch.object(utils, 'check_file_system_for_iscsi_device', - autospec=True) - @mock.patch.object(utils, 'delete_iscsi', autospec=True) - @mock.patch.object(utils, 'logout_iscsi', autospec=True) - def test_login_iscsi_calls_raises( - self, mock_loiscsi, mock_discsi, mock_check_dev, mock_update, - mock_verify, mock_exec, mock_log): - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.xyz', ''] - mock_check_dev.side_effect = exception.InstanceDeployFailure('boom') - self.assertRaises(exception.InstanceDeployFailure, - utils.login_iscsi, - address, port, iqn) - mock_verify.assert_called_once_with(iqn) - mock_update.assert_called_once_with(iqn) - mock_loiscsi.assert_called_once_with(address, port, iqn) - mock_discsi.assert_called_once_with(address, port, iqn) - self.assertIsInstance(mock_log.error.call_args[0][1], - exception.InstanceDeployFailure) - - @mock.patch.object(utils, 'LOG', autospec=True) - @mock.patch.object(common_utils, 'execute', autospec=True) - @mock.patch.object(utils, 'verify_iscsi_connection', autospec=True) - @mock.patch.object(utils, 'force_iscsi_lun_update', autospec=True) - @mock.patch.object(utils, 'check_file_system_for_iscsi_device', - autospec=True) - @mock.patch.object(utils, 'delete_iscsi', autospec=True) - @mock.patch.object(utils, 'logout_iscsi', autospec=True) - def test_login_iscsi_calls_raises_during_cleanup( - self, mock_loiscsi, mock_discsi, mock_check_dev, mock_update, - mock_verify, mock_exec, mock_log): - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.xyz', ''] - mock_check_dev.side_effect = exception.InstanceDeployFailure('boom') - mock_discsi.side_effect = processutils.ProcessExecutionError('boom') - self.assertRaises(exception.InstanceDeployFailure, - utils.login_iscsi, - address, port, iqn) - mock_verify.assert_called_once_with(iqn) - mock_update.assert_called_once_with(iqn) - mock_loiscsi.assert_called_once_with(address, port, iqn) - mock_discsi.assert_called_once_with(address, port, iqn) - self.assertIsInstance(mock_log.error.call_args[0][1], - exception.InstanceDeployFailure) - self.assertIsInstance(mock_log.warning.call_args[0][1], - processutils.ProcessExecutionError) - - @mock.patch.object(disk_utils, 'is_block_device', lambda d: True) - def test_always_logout_and_delete_iscsi(self): - """Check if logout_iscsi() and delete_iscsi() are called. - - Make sure that logout_iscsi() and delete_iscsi() are called once - login_iscsi() is invoked. - - """ - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - image_path = '/tmp/xyz/image' - root_mb = 128 - swap_mb = 64 - ephemeral_mb = 256 - ephemeral_format = 'exttest' - node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" - - dev = '/dev/fake' - - class TestException(Exception): - pass - - utils_name_list = ['get_dev', 'discovery', 'login_iscsi', - 'logout_iscsi', 'delete_iscsi'] - - disk_utils_name_list = ['get_image_mb', 'work_on_disk'] - - utils_mock = self._mock_calls(utils_name_list, utils) - utils_mock.get_dev.return_value = dev - - disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) - disk_utils_mock.get_image_mb.return_value = 1 - disk_utils_mock.work_on_disk.side_effect = TestException - utils_calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.discovery(address, port), - mock.call.login_iscsi(address, port, iqn), - mock.call.logout_iscsi(address, port, iqn), - mock.call.delete_iscsi(address, port, iqn)] - disk_utils_calls_expected = [mock.call.get_image_mb(image_path), - mock.call.work_on_disk( - dev, root_mb, swap_mb, - ephemeral_mb, - ephemeral_format, image_path, - node_uuid, configdrive=None, - preserve_ephemeral=False, - boot_option="netboot", - boot_mode="bios", - disk_label=None, - cpu_arch="")] - - self.assertRaises(TestException, utils.deploy_partition_image, - address, port, iqn, lun, image_path, - root_mb, swap_mb, ephemeral_mb, ephemeral_format, - node_uuid) - - self.assertEqual(utils_calls_expected, utils_mock.mock_calls) - self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) - - @mock.patch.object(common_utils, 'execute', autospec=True) - @mock.patch.object(utils, 'verify_iscsi_connection', autospec=True) - @mock.patch.object(utils, 'force_iscsi_lun_update', autospec=True) - @mock.patch.object(utils, 'check_file_system_for_iscsi_device', - autospec=True) - def test_ipv6_address_wrapped(self, - mock_check_dev, - mock_update, - mock_verify, - mock_exec): - address = '2001:DB8::1111' - port = 3306 - iqn = 'iqn.xyz' - mock_exec.return_value = ['iqn.xyz', ''] - utils.login_iscsi(address, port, iqn) - mock_exec.assert_called_once_with( - 'iscsiadm', - '-m', 'node', - '-p', '[%s]:%s' % (address, port), - '-T', iqn, - '--login', - run_as_root=True, - check_exit_code=[0], - attempts=5, - delay_on_retry=True) - - class SwitchPxeConfigTestCase(tests_base.TestCase): # NOTE(TheJulia): Remove elilo support after the deprecation period, @@ -1138,11 +606,6 @@ class OtherFunctionTestCase(db_base.DbTestCase): self.node = obj_utils.create_test_node(self.context, boot_interface='pxe') - def test_get_dev(self): - expected = '/dev/disk/by-path/ip-1.2.3.4:5678-iscsi-iqn.fake-lun-9' - actual = utils.get_dev('1.2.3.4', 5678, 'iqn.fake', 9) - self.assertEqual(expected, actual) - @mock.patch.object(utils, 'LOG', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(manager_utils, 'deploying_error_handler', autospec=True) @@ -1779,42 +1242,6 @@ class AgentMethodsTestCase(db_base.DbTestCase): utils.direct_deploy_should_convert_raw_image(self.node)) -@mock.patch.object(disk_utils, 'is_block_device', autospec=True) -@mock.patch.object(utils, 'login_iscsi', lambda *_: None) -@mock.patch.object(utils, 'discovery', lambda *_: None) -@mock.patch.object(utils, 'logout_iscsi', lambda *_: None) -@mock.patch.object(utils, 'delete_iscsi', lambda *_: None) -@mock.patch.object(utils, 'get_dev', lambda *_: '/dev/fake') -class ISCSISetupAndHandleErrorsTestCase(tests_base.TestCase): - - def test_no_parent_device(self, mock_ibd): - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - mock_ibd.return_value = False - expected_dev = '/dev/fake' - with testtools.ExpectedException(exception.InstanceDeployFailure): - with utils._iscsi_setup_and_handle_errors( - address, port, iqn, lun) as dev: - self.assertEqual(expected_dev, dev) - - mock_ibd.assert_called_once_with(expected_dev) - - def test_parent_device_yield(self, mock_ibd): - address = '127.0.0.1' - port = 3306 - iqn = 'iqn.xyz' - lun = 1 - expected_dev = '/dev/fake' - mock_ibd.return_value = True - with utils._iscsi_setup_and_handle_errors( - address, port, iqn, lun) as dev: - self.assertEqual(expected_dev, dev) - - mock_ibd.assert_called_once_with(expected_dev) - - class ValidateImagePropertiesTestCase(db_base.DbTestCase): @mock.patch.object(image_service, 'get_image_service', autospec=True) diff --git a/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py index adf4fc247..3c382f792 100644 --- a/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py +++ b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py @@ -17,12 +17,16 @@ import os import tempfile +import time +import types from ironic_lib import disk_utils from ironic_lib import utils as ironic_utils import mock +from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import fileutils +import testtools from ironic.common import boot_devices from ironic.common import dhcp_factory @@ -41,6 +45,7 @@ from ironic.drivers.modules.network import flat as flat_network from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import noop as noop_storage from ironic.drivers import utils as driver_utils +from ironic.tests import base as tests_base from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.objects import utils as obj_utils @@ -187,7 +192,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_partition_image', autospec=True) def test_continue_deploy_fail( self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout, mock_collect_logs): @@ -220,7 +225,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_partition_image', autospec=True) def test_continue_deploy_unexpected_fail( self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout, mock_collect_logs): @@ -251,7 +256,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_partition_image', autospec=True) def test_continue_deploy_fail_no_root_uuid_or_disk_id( self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout, mock_collect_logs): @@ -281,7 +286,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_partition_image', autospec=True) def test_continue_deploy_fail_empty_root_uuid( self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout, mock_collect_logs): @@ -312,7 +317,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, 'get_deploy_info', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_partition_image', autospec=True) def test_continue_deploy(self, deploy_mock, power_mock, mock_image_cache, mock_deploy_info, mock_log, mock_disk_layout): kwargs = {'address': '123456', 'iqn': 'aaa-bbb'} @@ -364,7 +369,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, 'get_deploy_info', autospec=True) @mock.patch.object(deploy_utils, 'InstanceImageCache', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True) - @mock.patch.object(deploy_utils, 'deploy_disk_image', autospec=True) + @mock.patch.object(iscsi_deploy, 'deploy_disk_image', autospec=True) def test_continue_deploy_whole_disk_image( self, deploy_mock, power_mock, mock_image_cache, mock_deploy_info, mock_log): @@ -1284,3 +1289,551 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase): + self.files): self.assertFalse(os.path.exists(path), '%s is not expected to exist' % path) + + +@mock.patch.object(time, 'sleep', lambda seconds: None) +class PhysicalWorkTestCase(tests_base.TestCase): + + def setUp(self): + super(PhysicalWorkTestCase, self).setUp() + self.address = '127.0.0.1' + self.port = 3306 + self.iqn = 'iqn.xyz' + self.lun = 1 + self.image_path = '/tmp/xyz/image' + self.node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + self.dev = ("/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" + % (self.address, self.port, self.iqn, self.lun)) + + def _mock_calls(self, name_list, module): + patch_list = [mock.patch.object(module, name, + spec_set=types.FunctionType) + for name in name_list] + mock_list = [patcher.start() for patcher in patch_list] + for patcher in patch_list: + self.addCleanup(patcher.stop) + + parent_mock = mock.MagicMock(spec=[]) + for mocker, name in zip(mock_list, name_list): + parent_mock.attach_mock(mocker, name) + return parent_mock + + @mock.patch.object(disk_utils, 'work_on_disk', autospec=True) + @mock.patch.object(disk_utils, 'is_block_device', autospec=True) + @mock.patch.object(disk_utils, 'get_image_mb', autospec=True) + @mock.patch.object(iscsi_deploy, 'logout_iscsi', autospec=True) + @mock.patch.object(iscsi_deploy, 'login_iscsi', autospec=True) + @mock.patch.object(iscsi_deploy, 'discovery', autospec=True) + @mock.patch.object(iscsi_deploy, 'delete_iscsi', autospec=True) + def _test_deploy_partition_image(self, + mock_delete_iscsi, + mock_discovery, + mock_login_iscsi, + mock_logout_iscsi, + mock_get_image_mb, + mock_is_block_device, + mock_work_on_disk, **kwargs): + # Below are the only values we allow callers to modify for testing. + # Check that values other than this aren't passed in. + deploy_args = { + 'boot_mode': None, + 'boot_option': None, + 'configdrive': None, + 'cpu_arch': None, + 'disk_label': None, + 'ephemeral_format': None, + 'ephemeral_mb': None, + 'image_mb': 1, + 'preserve_ephemeral': False, + 'root_mb': 128, + 'swap_mb': 64 + } + disallowed_values = set(kwargs) - set(deploy_args) + if disallowed_values: + raise ValueError("Only the following kwargs are allowed in " + "_test_deploy_partition_image: %(allowed)s. " + "Disallowed values: %(disallowed)s." + % {"allowed": ", ".join(deploy_args), + "disallowed": ", ".join(disallowed_values)}) + deploy_args.update(kwargs) + + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + mock_is_block_device.return_value = True + mock_get_image_mb.return_value = deploy_args['image_mb'] + mock_work_on_disk.return_value = { + 'root uuid': root_uuid, + 'efi system partition uuid': None + } + + deploy_kwargs = { + 'boot_mode': deploy_args['boot_mode'], + 'boot_option': deploy_args['boot_option'], + 'configdrive': deploy_args['configdrive'], + 'disk_label': deploy_args['disk_label'], + 'cpu_arch': deploy_args['cpu_arch'] or '', + 'preserve_ephemeral': deploy_args['preserve_ephemeral'] + } + iscsi_deploy.deploy_partition_image( + self.address, self.port, self.iqn, self.lun, self.image_path, + deploy_args['root_mb'], + deploy_args['swap_mb'], deploy_args['ephemeral_mb'], + deploy_args['ephemeral_format'], self.node_uuid, **deploy_kwargs) + + mock_discovery.assert_called_once_with(self.address, self.port) + mock_login_iscsi.assert_called_once_with(self.address, self.port, + self.iqn) + mock_logout_iscsi.assert_called_once_with(self.address, self.port, + self.iqn) + mock_delete_iscsi.assert_called_once_with(self.address, self.port, + self.iqn) + mock_get_image_mb.assert_called_once_with(self.image_path) + mock_is_block_device.assert_called_once_with(self.dev) + + work_on_disk_kwargs = { + 'preserve_ephemeral': deploy_args['preserve_ephemeral'], + 'configdrive': deploy_args['configdrive'], + # boot_option defaults to 'netboot' if + # not set + 'boot_option': deploy_args['boot_option'] or 'netboot', + 'boot_mode': deploy_args['boot_mode'], + 'disk_label': deploy_args['disk_label'], + 'cpu_arch': deploy_args['cpu_arch'] or '' + } + mock_work_on_disk.assert_called_once_with( + self.dev, deploy_args['root_mb'], deploy_args['swap_mb'], + deploy_args['ephemeral_mb'], deploy_args['ephemeral_format'], + self.image_path, self.node_uuid, **work_on_disk_kwargs) + + def test_deploy_partition_image_without_boot_option(self): + self._test_deploy_partition_image() + + def test_deploy_partition_image_netboot(self): + self._test_deploy_partition_image(boot_option="netboot") + + def test_deploy_partition_image_localboot(self): + self._test_deploy_partition_image(boot_option="local") + + def test_deploy_partition_image_wo_boot_option_and_wo_boot_mode(self): + self._test_deploy_partition_image() + + def test_deploy_partition_image_netboot_bios(self): + self._test_deploy_partition_image(boot_option="netboot", + boot_mode="bios") + + def test_deploy_partition_image_localboot_bios(self): + self._test_deploy_partition_image(boot_option="local", + boot_mode="bios") + + def test_deploy_partition_image_netboot_uefi(self): + self._test_deploy_partition_image(boot_option="netboot", + boot_mode="uefi") + + def test_deploy_partition_image_disk_label(self): + self._test_deploy_partition_image(disk_label='gpt') + + def test_deploy_partition_image_image_exceeds_root_partition(self): + self.assertRaises(exception.InstanceDeployFailure, + self._test_deploy_partition_image, image_mb=129, + root_mb=128) + + def test_deploy_partition_image_localboot_uefi(self): + self._test_deploy_partition_image(boot_option="local", + boot_mode="uefi") + + def test_deploy_partition_image_without_swap(self): + self._test_deploy_partition_image(swap_mb=0) + + def test_deploy_partition_image_with_ephemeral(self): + self._test_deploy_partition_image(ephemeral_format='exttest', + ephemeral_mb=256) + + def test_deploy_partition_image_preserve_ephemeral(self): + self._test_deploy_partition_image(ephemeral_format='exttest', + ephemeral_mb=256, + preserve_ephemeral=True) + + def test_deploy_partition_image_with_configdrive(self): + self._test_deploy_partition_image(configdrive='http://1.2.3.4/cd') + + def test_deploy_partition_image_with_cpu_arch(self): + self._test_deploy_partition_image(cpu_arch='generic') + + @mock.patch.object(disk_utils, 'create_config_drive_partition', + autospec=True) + @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) + def test_deploy_whole_disk_image(self, mock_gdi, create_config_drive_mock): + """Check loosely all functions are called with right args.""" + + name_list = ['discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi'] + disk_utils_name_list = ['is_block_device', 'populate_image'] + + iscsi_mock = self._mock_calls(name_list, iscsi_deploy) + + disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) + disk_utils_mock.is_block_device.return_value = True + mock_gdi.return_value = '0x12345678' + utils_calls_expected = [mock.call.discovery(self.address, self.port), + mock.call.login_iscsi(self.address, self.port, + self.iqn), + mock.call.logout_iscsi(self.address, self.port, + self.iqn), + mock.call.delete_iscsi(self.address, self.port, + self.iqn)] + disk_utils_calls_expected = [mock.call.is_block_device(self.dev), + mock.call.populate_image(self.image_path, + self.dev, + conv_flags=None)] + uuid_dict_returned = iscsi_deploy.deploy_disk_image( + self.address, self.port, self.iqn, self.lun, self.image_path, + self.node_uuid) + + self.assertEqual(utils_calls_expected, iscsi_mock.mock_calls) + self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) + self.assertFalse(create_config_drive_mock.called) + self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) + + @mock.patch.object(disk_utils, 'create_config_drive_partition', + autospec=True) + @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) + def test_deploy_whole_disk_image_with_config_drive(self, mock_gdi, + create_partition_mock): + """Check loosely all functions are called with right args.""" + config_url = 'http://1.2.3.4/cd' + + iscsi_list = ['discovery', 'login_iscsi', 'logout_iscsi', + 'delete_iscsi'] + + disk_utils_list = ['is_block_device', 'populate_image'] + iscsi_mock = self._mock_calls(iscsi_list, iscsi_deploy) + disk_utils_mock = self._mock_calls(disk_utils_list, disk_utils) + disk_utils_mock.is_block_device.return_value = True + mock_gdi.return_value = '0x12345678' + utils_calls_expected = [mock.call.discovery(self.address, self.port), + mock.call.login_iscsi(self.address, self.port, + self.iqn), + mock.call.logout_iscsi(self.address, self.port, + self.iqn), + mock.call.delete_iscsi(self.address, self.port, + self.iqn)] + + disk_utils_calls_expected = [mock.call.is_block_device(self.dev), + mock.call.populate_image(self.image_path, + self.dev, + conv_flags=None)] + + uuid_dict_returned = iscsi_deploy.deploy_disk_image( + self.address, self.port, self.iqn, self.lun, self.image_path, + self.node_uuid, configdrive=config_url) + + iscsi_mock.assert_has_calls(utils_calls_expected) + disk_utils_mock.assert_has_calls(disk_utils_calls_expected) + create_partition_mock.assert_called_once_with(self.node_uuid, self.dev, + config_url) + self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) + + @mock.patch.object(disk_utils, 'create_config_drive_partition', + autospec=True) + @mock.patch.object(disk_utils, 'get_disk_identifier', autospec=True) + def test_deploy_whole_disk_image_sparse(self, mock_gdi, + create_config_drive_mock): + """Check loosely all functions are called with right args.""" + iscsi_name_list = ['discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi'] + disk_utils_name_list = ['is_block_device', 'populate_image'] + + iscsi_mock = self._mock_calls(iscsi_name_list, iscsi_deploy) + + disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) + disk_utils_mock.is_block_device.return_value = True + mock_gdi.return_value = '0x12345678' + utils_calls_expected = [mock.call.discovery(self.address, self.port), + mock.call.login_iscsi(self.address, self.port, + self.iqn), + mock.call.logout_iscsi(self.address, self.port, + self.iqn), + mock.call.delete_iscsi(self.address, self.port, + self.iqn)] + disk_utils_calls_expected = [mock.call.is_block_device(self.dev), + mock.call.populate_image( + self.image_path, self.dev, + conv_flags='sparse')] + + uuid_dict_returned = iscsi_deploy.deploy_disk_image( + self.address, self.port, self.iqn, self.lun, self.image_path, + self.node_uuid, configdrive=None, conv_flags='sparse') + + self.assertEqual(utils_calls_expected, iscsi_mock.mock_calls) + self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) + self.assertFalse(create_config_drive_mock.called) + self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_verify_iscsi_connection_raises(self, mock_exec): + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.abc', ''] + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.verify_iscsi_connection, iqn) + self.assertEqual(3, mock_exec.call_count) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_verify_iscsi_connection_override_attempts(self, mock_exec): + utils.CONF.set_override('verify_attempts', 2, group='iscsi') + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.abc', ''] + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.verify_iscsi_connection, iqn) + self.assertEqual(2, mock_exec.call_count) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_check_file_system_for_iscsi_device_raises(self, mock_os): + iqn = 'iqn.xyz' + ip = "127.0.0.1" + port = "22" + mock_os.return_value = False + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.check_file_system_for_iscsi_device, + ip, port, iqn) + self.assertEqual(3, mock_os.call_count) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_check_file_system_for_iscsi_device(self, mock_os): + iqn = 'iqn.xyz' + ip = "127.0.0.1" + port = "22" + check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (ip, + port, + iqn) + + mock_os.return_value = True + iscsi_deploy.check_file_system_for_iscsi_device(ip, port, iqn) + mock_os.assert_called_once_with(check_dir) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_verify_iscsi_connection(self, mock_exec): + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + iscsi_deploy.verify_iscsi_connection(iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-S', + run_as_root=True, + check_exit_code=[0]) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_force_iscsi_lun_update(self, mock_exec): + iqn = 'iqn.xyz' + iscsi_deploy.force_iscsi_lun_update(iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-T', iqn, + '-R', + run_as_root=True, + check_exit_code=[0]) + + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(iscsi_deploy, 'verify_iscsi_connection', autospec=True) + @mock.patch.object(iscsi_deploy, 'force_iscsi_lun_update', autospec=True) + @mock.patch.object(iscsi_deploy, 'check_file_system_for_iscsi_device', + autospec=True) + def test_login_iscsi_calls_verify_and_update(self, + mock_check_dev, + mock_update, + mock_verify, + mock_exec): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + iscsi_deploy.login_iscsi(address, port, iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (address, port), + '-T', iqn, + '--login', + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + mock_verify.assert_called_once_with(iqn) + mock_update.assert_called_once_with(iqn) + mock_check_dev.assert_called_once_with(address, port, iqn) + + @mock.patch.object(iscsi_deploy, 'LOG', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(iscsi_deploy, 'verify_iscsi_connection', autospec=True) + @mock.patch.object(iscsi_deploy, 'force_iscsi_lun_update', autospec=True) + @mock.patch.object(iscsi_deploy, 'check_file_system_for_iscsi_device', + autospec=True) + @mock.patch.object(iscsi_deploy, 'delete_iscsi', autospec=True) + @mock.patch.object(iscsi_deploy, 'logout_iscsi', autospec=True) + def test_login_iscsi_calls_raises( + self, mock_loiscsi, mock_discsi, mock_check_dev, mock_update, + mock_verify, mock_exec, mock_log): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + mock_check_dev.side_effect = exception.InstanceDeployFailure('boom') + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.login_iscsi, + address, port, iqn) + mock_verify.assert_called_once_with(iqn) + mock_update.assert_called_once_with(iqn) + mock_loiscsi.assert_called_once_with(address, port, iqn) + mock_discsi.assert_called_once_with(address, port, iqn) + self.assertIsInstance(mock_log.error.call_args[0][1], + exception.InstanceDeployFailure) + + @mock.patch.object(iscsi_deploy, 'LOG', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(iscsi_deploy, 'verify_iscsi_connection', autospec=True) + @mock.patch.object(iscsi_deploy, 'force_iscsi_lun_update', autospec=True) + @mock.patch.object(iscsi_deploy, 'check_file_system_for_iscsi_device', + autospec=True) + @mock.patch.object(iscsi_deploy, 'delete_iscsi', autospec=True) + @mock.patch.object(iscsi_deploy, 'logout_iscsi', autospec=True) + def test_login_iscsi_calls_raises_during_cleanup( + self, mock_loiscsi, mock_discsi, mock_check_dev, mock_update, + mock_verify, mock_exec, mock_log): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + mock_check_dev.side_effect = exception.InstanceDeployFailure('boom') + mock_discsi.side_effect = processutils.ProcessExecutionError('boom') + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.login_iscsi, + address, port, iqn) + mock_verify.assert_called_once_with(iqn) + mock_update.assert_called_once_with(iqn) + mock_loiscsi.assert_called_once_with(address, port, iqn) + mock_discsi.assert_called_once_with(address, port, iqn) + self.assertIsInstance(mock_log.error.call_args[0][1], + exception.InstanceDeployFailure) + self.assertIsInstance(mock_log.warning.call_args[0][1], + processutils.ProcessExecutionError) + + @mock.patch.object(disk_utils, 'is_block_device', lambda d: True) + def test_always_logout_and_delete_iscsi(self): + """Check if logout_iscsi() and delete_iscsi() are called. + + Make sure that logout_iscsi() and delete_iscsi() are called once + login_iscsi() is invoked. + + """ + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 256 + ephemeral_format = 'exttest' + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + class TestException(Exception): + pass + + iscsi_name_list = ['discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi'] + + disk_utils_name_list = ['get_image_mb', 'work_on_disk'] + + iscsi_mock = self._mock_calls(iscsi_name_list, iscsi_deploy) + + disk_utils_mock = self._mock_calls(disk_utils_name_list, disk_utils) + disk_utils_mock.get_image_mb.return_value = 1 + disk_utils_mock.work_on_disk.side_effect = TestException + utils_calls_expected = [mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + disk_utils_calls_expected = [mock.call.get_image_mb(image_path), + mock.call.work_on_disk( + self.dev, root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, image_path, + node_uuid, configdrive=None, + preserve_ephemeral=False, + boot_option="netboot", + boot_mode="bios", + disk_label=None, + cpu_arch="")] + + self.assertRaises(TestException, iscsi_deploy.deploy_partition_image, + address, port, iqn, lun, image_path, + root_mb, swap_mb, ephemeral_mb, ephemeral_format, + node_uuid) + + self.assertEqual(utils_calls_expected, iscsi_mock.mock_calls) + self.assertEqual(disk_utils_calls_expected, disk_utils_mock.mock_calls) + + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(iscsi_deploy, 'verify_iscsi_connection', autospec=True) + @mock.patch.object(iscsi_deploy, 'force_iscsi_lun_update', autospec=True) + @mock.patch.object(iscsi_deploy, 'check_file_system_for_iscsi_device', + autospec=True) + def test_ipv6_address_wrapped(self, + mock_check_dev, + mock_update, + mock_verify, + mock_exec): + address = '2001:DB8::1111' + port = 3306 + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + iscsi_deploy.login_iscsi(address, port, iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-p', '[%s]:%s' % (address, port), + '-T', iqn, + '--login', + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + +@mock.patch.object(disk_utils, 'is_block_device', autospec=True) +@mock.patch.object(iscsi_deploy, 'login_iscsi', lambda *_: None) +@mock.patch.object(iscsi_deploy, 'discovery', lambda *_: None) +@mock.patch.object(iscsi_deploy, 'logout_iscsi', lambda *_: None) +@mock.patch.object(iscsi_deploy, 'delete_iscsi', lambda *_: None) +class ISCSISetupAndHandleErrorsTestCase(tests_base.TestCase): + + def test_no_parent_device(self, mock_ibd): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + mock_ibd.return_value = False + expected_dev = ("/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" + % (address, port, iqn, lun)) + with testtools.ExpectedException(exception.InstanceDeployFailure): + with iscsi_deploy._iscsi_setup_and_handle_errors( + address, port, iqn, lun) as dev: + self.assertEqual(expected_dev, dev) + + mock_ibd.assert_called_once_with(expected_dev) + + def test_parent_device_yield(self, mock_ibd): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + expected_dev = ("/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" + % (address, port, iqn, lun)) + mock_ibd.return_value = True + with iscsi_deploy._iscsi_setup_and_handle_errors( + address, port, iqn, lun) as dev: + self.assertEqual(expected_dev, dev) + + mock_ibd.assert_called_once_with(expected_dev) |