Merge "Refactoring: move iSCSI deploy code to iscsi_deploy.py"

This commit is contained in:
Zuul 2020-03-17 22:25:46 +00:00 committed by Gerrit Code Review
commit 70f3cd2aa4
4 changed files with 838 additions and 862 deletions

View File

@ -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.

View File

@ -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 '

View File

@ -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)

View File

@ -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)