From 222c84fff52be8383b26495c37df28cc5a0f98b9 Mon Sep 17 00:00:00 2001 From: Sirushti Murugesan Date: Mon, 26 Jan 2015 23:44:14 +0530 Subject: [PATCH] Adds support for deploying whole disk images Ironic decides whether it should deploy whole disk images or partition based images based on the presence of a kernel and a ramdisk. Partition based images are still deployed the way it is currently. No change made in the deployment workflow of partition based images. Whole Disk Images are dumped directly on the lun of the disk presented so that they can be localbooted. Separate PXE-Config entries have been added for whole disk images which when used to deploy whole disk images chainloads onto the hard drive that was exposed by the deploy ramdisk. Partially-Implements blueprint whole-disk-image-support Change-Id: I4478eff302a0ffeb3e9f2077a41170671863c478 --- doc/source/deploy/install-guide.rst | 20 + etc/ironic/rootwrap.d/ironic-utils.filters | 1 + ironic/common/image_service.py | 7 +- ironic/common/images.py | 31 ++ ironic/common/pxe_utils.py | 4 +- ironic/conductor/manager.py | 39 +- ironic/drivers/modules/agent_base_vendor.py | 15 +- ironic/drivers/modules/deploy_utils.py | 152 ++++-- .../modules/elilo_efi_pxe_config.template | 6 +- ironic/drivers/modules/ipxe_config.template | 7 +- ironic/drivers/modules/iscsi_deploy.py | 70 ++- ironic/drivers/modules/pxe.py | 84 +-- ironic/drivers/modules/pxe_config.template | 7 +- ironic/tests/conductor/test_manager.py | 482 ++++++++++-------- ironic/tests/db/utils.py | 6 + ironic/tests/drivers/pxe_config.template | 7 +- ironic/tests/drivers/test_deploy_utils.py | 362 +++++++++---- ironic/tests/drivers/test_iscsi_deploy.py | 193 +++++-- ironic/tests/drivers/test_pxe.py | 228 +++++++-- ironic/tests/test_images.py | 63 +++ 20 files changed, 1311 insertions(+), 473 deletions(-) diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index 0ba4ee8adf..cc179d5e27 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -513,6 +513,13 @@ them to Glance service: kernel_id=$MY_VMLINUZ_UUID --property \ ramdisk_id=$MY_INITRD_UUID < my-image.qcow2 + - *Note:* To deploy a whole disk image, a kernel_id and a ramdisk_id + shouldn't be associated with the image. An example is as follows:: + + glance image-create --name my-whole-disk-image --is-public True \ + --disk-format qcow2 \ + --container-format bare < my-whole-disk-image.qcow2 + 3. Add the deploy images to glance Add the *my-deploy-ramdisk.kernel* and @@ -612,6 +619,19 @@ node(s) where ``ironic-conductor`` is running. Ubuntu (14.10 and after): sudo cp /usr/lib/PXELINUX/pxelinux.0 /tftpboot +#. If whole disk images need to be deployed via PXE-netboot, copy the + chain.c32 image to ``/tftpboot`` to support it. The chain.c32 image + might be found at:: + + Ubuntu (Up to and including 14.04): + sudo cp /usr/lib/syslinux/chain.c32 /tftpboot + + Ubuntu (14.10 and after): + sudo cp /usr/lib/syslinux/modules/bios/chain.c32 /tftpboot + + Fedora: + sudo cp /boot/extlinux/chain.c32 /tftpboot + #. If the version of syslinux is **greater than** 4 we also need to make sure that we copy the library modules into the ``/tftpboot`` directory [2]_ [1]_:: diff --git a/etc/ironic/rootwrap.d/ironic-utils.filters b/etc/ironic/rootwrap.d/ironic-utils.filters index 01a1258426..972ddf5cf2 100644 --- a/etc/ironic/rootwrap.d/ironic-utils.filters +++ b/etc/ironic/rootwrap.d/ironic-utils.filters @@ -6,6 +6,7 @@ iscsiadm: CommandFilter, iscsiadm, root blkid: CommandFilter, blkid, root blockdev: CommandFilter, blockdev, root +hexdump: CommandFilter, hexdump, root # ironic/common/utils.py mkswap: CommandFilter, mkswap, root diff --git a/ironic/common/image_service.py b/ironic/common/image_service.py index 9f3f797520..57ab194d75 100644 --- a/ironic/common/image_service.py +++ b/ironic/common/image_service.py @@ -35,6 +35,11 @@ LOG = logging.getLogger(__name__) IMAGE_CHUNK_SIZE = 1024 * 1024 # 1mb +CONF = cfg.CONF +# Import this opt early so that it is available when registering +# glance_opts below. +CONF.import_opt('my_ip', 'ironic.netconf') + glance_opts = [ cfg.StrOpt('glance_host', default='$my_ip', @@ -64,8 +69,6 @@ glance_opts = [ 'Set to https for SSL.'), ] - -CONF = cfg.CONF CONF.register_opts(glance_opts, group='glance') diff --git a/ironic/common/images.py b/ironic/common/images.py index 8919055e04..1490910094 100644 --- a/ironic/common/images.py +++ b/ironic/common/images.py @@ -27,6 +27,7 @@ from oslo_concurrency import processutils from oslo_config import cfg from ironic.common import exception +from ironic.common.glance_service import service_utils as glance_utils from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common import image_service as service @@ -377,3 +378,33 @@ def create_boot_iso(context, output_filename, kernel_href, create_isolinux_image(output_filename, kernel_path, ramdisk_path, params) + + +def is_whole_disk_image(ctx, instance_info): + """Find out if the image is a partition image or a whole disk image. + + :param ctx: an admin context + :param instance_info: a node's instance info dict + + :returns True for whole disk images and False for partition images + and None on no image_source or Error. + """ + image_source = instance_info.get('image_source') + if not image_source: + return + + is_whole_disk_image = False + if glance_utils.is_glance_image(image_source): + try: + iproperties = get_image_properties(ctx, image_source) + except Exception: + return + is_whole_disk_image = (not iproperties.get('kernel_id') and + not iproperties.get('ramdisk_id')) + else: + # Non glance image ref + if (not instance_info.get('kernel') and + not instance_info.get('ramdisk')): + is_whole_disk_image = True + + return is_whole_disk_image diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index ff79707565..3a214c3da2 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -68,7 +68,9 @@ def _build_pxe_config(pxe_options, template): env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path)) template = env.get_template(tmpl_file) return template.render({'pxe_options': pxe_options, - 'ROOT': '{{ ROOT }}'}) + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}', + }) def _link_mac_pxe_configs(task): diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 837f46030e..baf824bc43 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -66,6 +66,7 @@ from ironic.common.i18n import _LC from ironic.common.i18n import _LE from ironic.common.i18n import _LI from ironic.common.i18n import _LW +from ironic.common import images from ironic.common import keystone from ironic.common import rpc from ironic.common import states @@ -667,14 +668,6 @@ class ConductorManager(periodic_task.PeriodicTasks): if node.maintenance: raise exception.NodeInMaintenance(op=_('provisioning'), node=node.uuid) - try: - task.driver.power.validate(task) - task.driver.deploy.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue) as e: - raise exception.InstanceDeployFailure(_( - "RPC do_node_deploy failed to validate deploy or " - "power info. Error: %(msg)s") % {'msg': e}) if rebuild: event = 'rebuild' @@ -689,10 +682,29 @@ class ConductorManager(periodic_task.PeriodicTasks): instance_info.pop('kernel', None) instance_info.pop('ramdisk', None) node.instance_info = instance_info - node.save() else: event = 'deploy' + driver_internal_info = node.driver_internal_info + # Infer the image type to make sure the deploy driver + # validates only the necessary variables for different + # image types. + # NOTE(sirushtim): The iwdi variable can be None. It's upto + # the deploy driver to validate this. + iwdi = images.is_whole_disk_image(context, node.instance_info) + driver_internal_info['is_whole_disk_image'] = iwdi + node.driver_internal_info = driver_internal_info + node.save() + + try: + task.driver.power.validate(task) + task.driver.deploy.validate(task) + except (exception.InvalidParameterValue, + exception.MissingParameterValue) as e: + raise exception.InstanceDeployFailure(_( + "RPC do_node_deploy failed to validate deploy or " + "power info. Error: %(msg)s") % {'msg': e}) + LOG.debug("do_node_deploy Calling event: %(event)s for node: " "%(node)s", {'event': event, 'node': node.uuid}) try: @@ -1040,6 +1052,15 @@ class ConductorManager(periodic_task.PeriodicTasks): node_id) ret_dict = {} with task_manager.acquire(context, node_id, shared=True) as task: + # NOTE(sirushtim): the is_whole_disk_image variable is needed by + # deploy drivers for doing their validate(). Since the deploy + # isn't being done yet and the driver information could change in + # the meantime, we don't know if the is_whole_disk_image value will + # change or not. It isn't saved to the DB, but only used with this + # node instance for the current validations. + iwdi = images.is_whole_disk_image(context, + task.node.instance_info) + task.node.driver_internal_info['is_whole_disk_image'] = iwdi for iface_name in (task.driver.core_interfaces + task.driver.standard_interfaces): iface = getattr(task.driver, iface_name, None) diff --git a/ironic/drivers/modules/agent_base_vendor.py b/ironic/drivers/modules/agent_base_vendor.py index fc33ca9fc3..3b624d07e4 100644 --- a/ironic/drivers/modules/agent_base_vendor.py +++ b/ironic/drivers/modules/agent_base_vendor.py @@ -360,13 +360,14 @@ class BaseAgentVendor(base.VendorInterface): on encountering error while setting the boot device on the node. """ node = task.node - result = self._client.install_bootloader(node, root_uuid) - if result['command_status'] == 'FAILED': - msg = (_("Failed to install a bootloader when " - "deploying node %(node)s. Error: %(error)s") % - {'node': node.uuid, - 'error': result['command_error']}) - self._log_and_raise_deployment_error(task, msg) + if not node.driver_internal_info.get('is_whole_disk_image'): + result = self._client.install_bootloader(node, root_uuid) + if result['command_status'] == 'FAILED': + msg = (_("Failed to install a bootloader when " + "deploying node %(node)s. Error: %(error)s") % + {'node': node.uuid, + 'error': result['command_error']}) + self._log_and_raise_deployment_error(task, msg) try: deploy_utils.try_set_boot_device(task, boot_devices.DISK) diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index e3efbbd8a0..8318ae754b 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -15,6 +15,7 @@ import base64 +import contextlib import gzip import math import os @@ -185,6 +186,28 @@ def delete_iscsi(portal_address, portal_port, target_iqn): delay_on_retry=True) +def get_disk_identifier(dev): + """Get the disk identifier from the disk being exposed by the ramdisk. + + This disk identifier is appended to the pxe config which will then be + used by chain.c32 to detect the correct disk to chainload. This is helpful + in deployments to nodes with multiple disks. + + http://www.syslinux.org/wiki/index.php/Comboot/chain.c32#mbr: + + :param dev: Path for the already populated disk device. + :returns The Disk Identifier. + """ + disk_identifier = utils.execute('hexdump', '-s', '440', '-n', '4', + '-e', '''\"0x%08x\"''', + dev, + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + return disk_identifier[0] + + def make_partitions(dev, root_mb, swap_mb, ephemeral_mb, configdrive_mb, commit=True, boot_option="netboot"): """Partition the disk device. @@ -290,28 +313,63 @@ def block_uuid(dev): return out.strip() -def switch_pxe_config(path, root_uuid, boot_mode): - """Switch a pxe config from deployment mode to service mode.""" +def _replace_lines_in_file(path, regex_pattern, replacement): with open(path) as f: lines = f.readlines() - root = 'UUID=%s' % root_uuid - rre = re.compile(r'\{\{ ROOT \}\}') - - if boot_mode == 'uefi': - dre = re.compile('^default=.*$') - boot_line = 'default=boot' - else: - pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default' - dre = re.compile('^%s .*$' % pxe_cmd) - boot_line = '%s boot' % pxe_cmd + compiled_pattern = re.compile(regex_pattern) with open(path, 'w') as f: for line in lines: - line = rre.sub(root, line) - line = dre.sub(boot_line, line) + line = compiled_pattern.sub(replacement, line) f.write(line) +def _replace_root_uuid(path, root_uuid): + root = 'UUID=%s' % root_uuid + pattern = r'\{\{ ROOT \}\}' + _replace_lines_in_file(path, pattern, root) + + +def _replace_boot_line(path, boot_mode, is_whole_disk_image): + if is_whole_disk_image: + boot_disk_type = 'boot_whole_disk' + else: + boot_disk_type = 'boot_partition' + + if boot_mode == 'uefi': + pattern = '^default=.*$' + boot_line = 'default=%s' % boot_disk_type + else: + pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default' + pattern = '^%s .*$' % pxe_cmd + boot_line = '%s %s' % (pxe_cmd, boot_disk_type) + + _replace_lines_in_file(path, pattern, boot_line) + + +def _replace_disk_identifier(path, disk_identifier): + pattern = r'\{\{ DISK_IDENTIFIER \}\}' + _replace_lines_in_file(path, pattern, disk_identifier) + + +def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, + is_whole_disk_image): + """Switch a pxe config from deployment mode to service mode. + + :param path: path to the pxe config file in tftpboot. + :param root_uuid_or_disk_id: root uuid in case of partition image or + disk_id in case of whole disk image. + :param boot_mode: if boot mode is uefi or bios. + :param is_whole_disk_image: if the image is a whole disk image or not. + """ + if not is_whole_disk_image: + _replace_root_uuid(path, root_uuid_or_disk_id) + else: + _replace_disk_identifier(path, root_uuid_or_disk_id) + + _replace_boot_line(path, boot_mode, is_whole_disk_image) + + def notify(address, port): """Notify a node that it becomes ready to reboot.""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -480,10 +538,6 @@ def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, :param boot_option: Can be "local" or "netboot". "netboot" by default. :returns: the UUID of the root partition. """ - if not is_block_device(dev): - raise exception.InstanceDeployFailure( - _("Parent device '%s' not found") % dev) - # the only way for preserve_ephemeral to be set to true is if we are # rebuilding an instance with --preserve_ephemeral. commit = not preserve_ephemeral @@ -550,11 +604,11 @@ def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, return root_uuid -def deploy(address, port, iqn, lun, image_path, +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="netboot"): - """All-in-one function to deploy a node. + """All-in-one function to deploy a partition image to a node. :param address: The iSCSI IP address. :param port: The iSCSI port number. @@ -576,18 +630,60 @@ def deploy(address, port, iqn, lun, image_path, :param boot_option: Can be "local" or "netboot". "netboot" by default. :returns: the UUID of the root partition. """ - dev = get_dev(address, port, iqn, lun) - image_mb = get_image_mb(image_path) - if image_mb > root_mb: - root_mb = image_mb - discovery(address, port) - login_iscsi(address, port, iqn) - try: + with _iscsi_setup_and_handle_errors(address, port, iqn, + lun, image_path) as dev: + image_mb = get_image_mb(image_path) + if image_mb > root_mb: + root_mb = image_mb + root_uuid = 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) + + return root_uuid + + +def deploy_disk_image(address, port, iqn, lun, + image_path, node_uuid): + """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. Used for logging. Currently not in use + by this function but could be used in the future. + """ + with _iscsi_setup_and_handle_errors(address, port, iqn, + lun, image_path) as dev: + populate_image(image_path, dev) + disk_identifier = get_disk_identifier(dev) + + return disk_identifier + + +@contextlib.contextmanager +def _iscsi_setup_and_handle_errors(address, port, iqn, lun, + image_path): + """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. + :param image_path: Path for the instance's disk image. + """ + dev = get_dev(address, port, iqn, lun) + discovery(address, port) + login_iscsi(address, port, iqn) + if not 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(_LE("Deploy to address %s failed."), address) @@ -602,8 +698,6 @@ def deploy(address, port, iqn, lun, image_path, logout_iscsi(address, port, iqn) delete_iscsi(address, port, iqn) - return root_uuid - def notify_deploy_complete(address): """Notifies the completion of deployment to the baremetal node. diff --git a/ironic/drivers/modules/elilo_efi_pxe_config.template b/ironic/drivers/modules/elilo_efi_pxe_config.template index 18e46da51f..71e6809821 100644 --- a/ironic/drivers/modules/elilo_efi_pxe_config.template +++ b/ironic/drivers/modules/elilo_efi_pxe_config.template @@ -7,6 +7,10 @@ image={{pxe_options.deployment_aki_path}} image={{pxe_options.aki_path}} - label=boot + label=boot_partition initrd={{pxe_options.ari_path}} append="root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} ip=%I:{{pxe_options.tftp_server}}:%G:%M:%H::on" + +image=chain.c32 + label=boot_whole_disk + append mbr:{{ DISK_IDENTIFIER }} diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index 8a4bc2354a..bd5647841b 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -10,7 +10,12 @@ kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk initrd {{ pxe_options.deployment_ari_path }} boot -:boot +:boot_partition kernel {{ pxe_options.aki_path }} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} initrd {{ pxe_options.ari_path }} boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index e9070f55da..b93357528c 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -114,10 +114,12 @@ def parse_instance_info(node): info = node.instance_info i_info = {} i_info['image_source'] = info.get('image_source') - if (i_info['image_source'] and + is_whole_disk_image = node.driver_internal_info.get('is_whole_disk_image') + if not is_whole_disk_image: + if (i_info['image_source'] and not glance_service_utils.is_glance_image(i_info['image_source'])): - i_info['kernel'] = info.get('kernel') - i_info['ramdisk'] = info.get('ramdisk') + i_info['kernel'] = info.get('kernel') + i_info['ramdisk'] = info.get('ramdisk') i_info['root_gb'] = info.get('root_gb') error_msg = _("Cannot validate iSCSI deploy. Some parameters were missing" @@ -129,18 +131,26 @@ def parse_instance_info(node): i_info['swap_mb'] = info.get('swap_mb', 0) i_info['ephemeral_gb'] = info.get('ephemeral_gb', 0) - i_info['ephemeral_format'] = info.get('ephemeral_format') - i_info['configdrive'] = info.get('configdrive') - err_msg_invalid = _("Cannot validate parameter for iSCSI deploy. " "Invalid parameter %(param)s. Reason: %(reason)s") for param in ('root_gb', 'swap_mb', 'ephemeral_gb'): try: int(i_info[param]) except ValueError: - reason = _("'%s' is not an integer value.") % i_info[param] + reason = _("%s is not an integer value.") % i_info[param] raise exception.InvalidParameterValue(err_msg_invalid % - {'param': param, 'reason': reason}) + {'param': param, + 'reason': reason}) + + if is_whole_disk_image: + if int(i_info['swap_mb']) > 0 or int(i_info['ephemeral_gb']) > 0: + err_msg_invalid = _("Cannot deploy whole disk image with " + "swap or ephemeral size set") + raise exception.InvalidParameterValue(err_msg_invalid) + return i_info + + i_info['ephemeral_format'] = info.get('ephemeral_format') + i_info['configdrive'] = info.get('configdrive') if i_info['ephemeral_gb'] and not i_info['ephemeral_format']: i_info['ephemeral_format'] = CONF.pxe.default_ephemeral_format @@ -228,12 +238,15 @@ def get_deploy_info(node, **kwargs): 'iqn': kwargs.get('iqn'), 'lun': kwargs.get('lun', '1'), 'image_path': _get_image_file_path(node.uuid), - 'root_mb': 1024 * int(i_info['root_gb']), - 'swap_mb': int(i_info['swap_mb']), - 'ephemeral_mb': 1024 * int(i_info['ephemeral_gb']), - 'preserve_ephemeral': i_info['preserve_ephemeral'], - 'node_uuid': node.uuid, - 'boot_option': get_boot_option(node)} + 'node_uuid': node.uuid} + + is_whole_disk_image = node.driver_internal_info['is_whole_disk_image'] + if not is_whole_disk_image: + params.update({'root_mb': 1024 * int(i_info['root_gb']), + 'swap_mb': int(i_info['swap_mb']), + 'ephemeral_mb': 1024 * int(i_info['ephemeral_gb']), + 'preserve_ephemeral': i_info['preserve_ephemeral'], + 'boot_option': get_boot_option(node)}) missing = [key for key in params if params[key] is None] if missing: @@ -241,6 +254,9 @@ def get_deploy_info(node, **kwargs): "Parameters %s were not passed to ironic" " for deploy.") % missing) + if is_whole_disk_image: + return params + # configdrive and ephemeral_format are nullable params['ephemeral_format'] = i_info.get('ephemeral_format') params['configdrive'] = i_info.get('configdrive') @@ -258,7 +274,8 @@ def continue_deploy(task, **kwargs): :param kwargs: the kwargs to be passed to deploy. :raises: InvalidState if the event is not allowed by the associated state machine. - :returns: UUID of the root partition or None on error. + :returns: UUID of the root partition for partition images or disk + identifier for whole disk images or None on error. """ node = task.node @@ -283,9 +300,13 @@ def continue_deploy(task, **kwargs): LOG.debug('Continuing deployment for node %(node)s, params %(params)s', {'node': node.uuid, 'params': log_params}) - root_uuid = None + root_uuid_or_disk_id = None try: - root_uuid = deploy_utils.deploy(**params) + if node.driver_internal_info['is_whole_disk_image']: + root_uuid_or_disk_id = deploy_utils.deploy_disk_image(**params) + else: + root_uuid_or_disk_id = deploy_utils.deploy_partition_image( + **params) except Exception as e: LOG.error(_LE('Deploy failed for instance %(instance)s. ' 'Error: %(error)s'), @@ -294,7 +315,7 @@ def continue_deploy(task, **kwargs): 'iSCSI deployment.')) destroy_images(node.uuid) - return root_uuid + return root_uuid_or_disk_id def do_agent_iscsi_deploy(task, agent_client): @@ -337,21 +358,22 @@ def do_agent_iscsi_deploy(task, agent_client): 'key': iscsi_options['deployment_key'], 'address': address} - root_uuid = continue_deploy(task, **iscsi_params) - if not root_uuid: - msg = (_("Couldn't determine the UUID of the root partition " - "when deploying node %s") % node.uuid) + root_uuid_or_disk_id = continue_deploy(task, **iscsi_params) + if not root_uuid_or_disk_id: + msg = (_("Couldn't determine the UUID of the root " + "partition or the disk identifier when deploying " + "node %s") % node.uuid) deploy_utils.set_failed_state(task, msg) raise exception.InstanceDeployFailure(reason=msg) # TODO(lucasagomes): Move this bit saving the root_uuid to # iscsi_deploy.continue_deploy() driver_internal_info = node.driver_internal_info - driver_internal_info['root_uuid'] = root_uuid + driver_internal_info['root_uuid_or_disk_id'] = root_uuid_or_disk_id node.driver_internal_info = driver_internal_info node.save() - return root_uuid + return root_uuid_or_disk_id def parse_root_device_hints(node): diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 12bb347699..4d5d836a4d 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -185,28 +185,34 @@ def _build_pxe_config_options(node, pxe_info, ctx): :returns: A dictionary of pxe options to be used in the pxe bootfile template. """ + is_whole_disk_image = node.driver_internal_info['is_whole_disk_image'] + if CONF.pxe.ipxe_enabled: deploy_kernel = '/'.join([CONF.pxe.http_url, node.uuid, 'deploy_kernel']) deploy_ramdisk = '/'.join([CONF.pxe.http_url, node.uuid, 'deploy_ramdisk']) - kernel = '/'.join([CONF.pxe.http_url, node.uuid, 'kernel']) - ramdisk = '/'.join([CONF.pxe.http_url, node.uuid, 'ramdisk']) + if not is_whole_disk_image: + kernel = '/'.join([CONF.pxe.http_url, node.uuid, 'kernel']) + ramdisk = '/'.join([CONF.pxe.http_url, node.uuid, 'ramdisk']) else: deploy_kernel = pxe_info['deploy_kernel'][1] deploy_ramdisk = pxe_info['deploy_ramdisk'][1] - kernel = pxe_info['kernel'][1] - ramdisk = pxe_info['ramdisk'][1] + if not is_whole_disk_image: + kernel = pxe_info['kernel'][1] + ramdisk = pxe_info['ramdisk'][1] pxe_options = { 'deployment_aki_path': deploy_kernel, 'deployment_ari_path': deploy_ramdisk, - 'aki_path': kernel, - 'ari_path': ramdisk, 'pxe_append_params': CONF.pxe.pxe_append_params, 'tftp_server': CONF.pxe.tftp_server } + if not is_whole_disk_image: + pxe_options.update({'aki_path': kernel, + 'ari_path': ramdisk}) + deploy_ramdisk_options = iscsi_deploy.build_deploy_ramdisk_options(node) pxe_options.update(deploy_ramdisk_options) @@ -240,7 +246,7 @@ def _cache_ramdisk_kernel(ctx, node, pxe_info): """Fetch the necessary kernels and ramdisks for the instance.""" fileutils.ensure_tree( os.path.join(pxe_utils.get_root_dir(), node.uuid)) - LOG.debug("Fetching kernel and ramdisk for node %s", + LOG.debug("Fetching necessary kernel and ramdisk for node %s", node.uuid) deploy_utils.fetch_images(ctx, TFTPImageCache(), pxe_info.values(), CONF.force_raw_images) @@ -261,6 +267,9 @@ def _get_image_info(node, ctx): image_info.update(pxe_utils.get_deploy_kr_info(node.uuid, d_info)) + if node.driver_internal_info['is_whole_disk_image']: + return image_info + i_info = node.instance_info labels = ('kernel', 'ramdisk') if not (i_info.get('kernel') and i_info.get('ramdisk')): @@ -349,10 +358,13 @@ class PXEDeploy(base.DeployInterface): iscsi_deploy.validate(task) - if service_utils.is_glance_image(d_info['image_source']): + if node.driver_internal_info.get('is_whole_disk_image'): + props = [] + elif service_utils.is_glance_image(d_info['image_source']): props = ['kernel_id', 'ramdisk_id'] else: props = ['kernel', 'ramdisk'] + iscsi_deploy.validate_image_properties(task.context, d_info, props) @task_manager.require_exclusive_lock @@ -428,25 +440,34 @@ class PXEDeploy(base.DeployInterface): # the image kernel and ramdisk (Or even require it). _cache_ramdisk_kernel(task.context, task.node, pxe_info) + iwdi = task.node.driver_internal_info['is_whole_disk_image'] # NOTE(deva): prepare may be called from conductor._do_takeover # in which case, it may need to regenerate the PXE config file for an # already-active deployment. if task.node.provision_state == states.ACTIVE: + # this should have been stashed when the deploy was done + # but let's guard, just in case it's missing try: - # this should have been stashed when the deploy was done - # but let's guard, just in case it's missing - root_uuid = task.node.driver_internal_info['root_uuid'] + root_uuid_or_disk_id = task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] except KeyError: - LOG.warn(_LW("The UUID for the root partition can't be found, " - "unable to switch the pxe config from deployment " - "mode to service (boot) mode for node %(node)s"), - {"node": task.node.uuid}) + if not iwdi: + LOG.warn(_LW("The UUID for the root partition can't be " + "found, unable to switch the pxe config from " + "deployment mode to service (boot) mode for node " + "%(node)s"), {"node": task.node.uuid}) + else: + LOG.warn(_LW("The disk id for the whole disk image can't " + "be found, unable to switch the pxe config from " + "deployment mode to service (boot) mode for " + "node %(node)s"), {"node": task.node.uuid}) else: pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) deploy_utils.switch_pxe_config( - pxe_config_path, root_uuid, - driver_utils.get_node_capability(task.node, 'boot_mode')) + pxe_config_path, root_uuid_or_disk_id, + driver_utils.get_node_capability(task.node, 'boot_mode'), + iwdi) def clean_up(self, task): """Clean up the deployment environment for the task's node. @@ -529,10 +550,9 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): task.process_event('resume') _destroy_token_file(node) - - root_uuid = iscsi_deploy.continue_deploy(task, **kwargs) - - if not root_uuid: + is_whole_disk_image = node.driver_internal_info['is_whole_disk_image'] + root_uuid_or_disk_id = iscsi_deploy.continue_deploy(task, **kwargs) + if not root_uuid_or_disk_id: return # save the node's root disk UUID so that another conductor could @@ -540,7 +560,7 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): # we have to assign to node.driver_internal_info so the node knows it # has changed. driver_internal_info = node.driver_internal_info - driver_internal_info['root_uuid'] = root_uuid + driver_internal_info['root_uuid_or_disk_id'] = root_uuid_or_disk_id node.driver_internal_info = driver_internal_info node.save() @@ -552,8 +572,10 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): pxe_utils.clean_up_pxe_config(task) else: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) - deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, - driver_utils.get_node_capability(node, 'boot_mode')) + node_cap = driver_utils.get_node_capability(node, 'boot_mode') + deploy_utils.switch_pxe_config(pxe_config_path, + root_uuid_or_disk_id, + node_cap, is_whole_disk_image) deploy_utils.notify_deploy_complete(kwargs['address']) LOG.info(_LI('Deployment to node %s done'), node.uuid) @@ -588,11 +610,13 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): # it here. _destroy_token_file(node) - root_uuid = iscsi_deploy.do_agent_iscsi_deploy(task, self._client) - + root_uuid_or_disk_id = iscsi_deploy.do_agent_iscsi_deploy( + task, + self._client) + is_whole_disk_image = node.driver_internal_info['is_whole_disk_image'] if iscsi_deploy.get_boot_option(node) == "local": # Install the boot loader - self.configure_local_boot(task, root_uuid) + self.configure_local_boot(task, root_uuid_or_disk_id) # If it's going to boot from the local disk, get rid of # the PXE configuration files used for the deployment @@ -600,7 +624,9 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): else: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) boot_mode = driver_utils.get_node_capability(node, 'boot_mode') - deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, - boot_mode) + deploy_utils.switch_pxe_config( + pxe_config_path, + root_uuid_or_disk_id, + boot_mode, is_whole_disk_image) self.reboot_and_finish_deploy(task) diff --git a/ironic/drivers/modules/pxe_config.template b/ironic/drivers/modules/pxe_config.template index 9846374a54..d231ca80b6 100644 --- a/ironic/drivers/modules/pxe_config.template +++ b/ironic/drivers/modules/pxe_config.template @@ -6,6 +6,11 @@ append initrd={{ pxe_options.deployment_ari_path }} selinux=0 disk={{ pxe_option ipappend 3 -label boot +label boot_partition kernel {{ pxe_options.aki_path }} append initrd={{ pxe_options.ari_path }} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} + + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py index f48ee2b68c..6e656d9608 100644 --- a/ironic/tests/conductor/test_manager.py +++ b/ironic/tests/conductor/test_manager.py @@ -31,6 +31,7 @@ from oslo_utils import uuidutils from ironic.common import boot_devices from ironic.common import driver_factory from ironic.common import exception +from ironic.common import images from ironic.common import keystone from ironic.common import states from ironic.common import swift @@ -922,9 +923,11 @@ class VendorPassthruTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): @_mock_record_keepalive -class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, +@mock.patch.object(images, 'is_whole_disk_image') +class ServiceDoNodeDeployTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): - def test_do_node_deploy_invalid_state(self): + def test_do_node_deploy_invalid_state(self, mock_iwdi): + mock_iwdi.return_value = False self._start_service() # test that node deploy fails if the node is already provisioned node = obj_utils.create_test_node(self.context, driver='fake', @@ -939,8 +942,11 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertIsNone(node.last_error) # Verify reservation has been cleared. self.assertIsNone(node.reservation) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertNotIn('is_whole_disk_image', node.driver_internal_info) - def test_do_node_deploy_maintenance(self): + def test_do_node_deploy_maintenance(self, mock_iwdi): + mock_iwdi.return_value = False node = obj_utils.create_test_node(self.context, driver='fake', maintenance=True) exc = self.assertRaises(messaging.rpc.ExpectedException, @@ -952,8 +958,10 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertIsNone(node.last_error) # Verify reservation has been cleared. self.assertIsNone(node.reservation) + self.assertFalse(mock_iwdi.called) - def _test_do_node_deploy_validate_fail(self, mock_validate): + def _test_do_node_deploy_validate_fail(self, mock_validate, mock_iwdi): + mock_iwdi.return_value = False # InvalidParameterValue should be re-raised as InstanceDeployFailure mock_validate.side_effect = exception.InvalidParameterValue('error') node = obj_utils.create_test_node(self.context, driver='fake') @@ -966,15 +974,238 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertIsNone(node.last_error) # Verify reservation has been cleared. self.assertIsNone(node.reservation) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertNotIn('is_whole_disk_image', node.driver_internal_info) @mock.patch('ironic.drivers.modules.fake.FakeDeploy.validate') - def test_do_node_deploy_validate_fail(self, mock_validate): - self._test_do_node_deploy_validate_fail(mock_validate) + def test_do_node_deploy_validate_fail(self, mock_validate, mock_iwdi): + self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi) @mock.patch('ironic.drivers.modules.fake.FakePower.validate') - def test_do_node_deploy_power_validate_fail(self, mock_validate): - self._test_do_node_deploy_validate_fail(mock_validate) + def test_do_node_deploy_power_validate_fail(self, mock_validate, + mock_iwdi): + self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi) + @mock.patch('ironic.conductor.task_manager.TaskManager.process_event') + def test_deploy_with_nostate_converts_to_available(self, mock_pe, + mock_iwdi): + # expressly create a node using the Juno-era NOSTATE state + # and assert that it does not result in an error, and that the state + # is converted to the new AVAILABLE state. + # Mock the process_event call, because the transitions from + # AVAILABLE are tested thoroughly elsewhere + # NOTE(deva): This test can be deleted after Kilo is released + mock_iwdi.return_value = False + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.NOSTATE) + self.assertEqual(states.NOSTATE, node.provision_state) + self.service.do_node_deploy(self.context, node.uuid) + self.assertTrue(mock_pe.called) + node.refresh() + self.assertEqual(states.AVAILABLE, node.provision_state) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + def test_do_node_deploy_partial_ok(self, mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + thread = self.service._spawn_worker(lambda: None) + with mock.patch.object(self.service, '_spawn_worker') as mock_spawn: + mock_spawn.return_value = thread + + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.AVAILABLE) + + self.service.do_node_deploy(self.context, node.uuid) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.DEPLOYING, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + # This is a sync operation last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_spawn.assert_called_once_with(mock.ANY, mock.ANY, + mock.ANY, None) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test_do_node_deploy_rebuild_active_state(self, mock_deploy, mock_iwdi): + # This tests manager.do_node_deploy(), the 'else' path of + # 'if new_state == states.DEPLOYDONE'. The node's states + # aren't changed in this case. + mock_iwdi.return_value = True + self._start_service() + mock_deploy.return_value = states.DEPLOYING + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.ACTIVE, + target_provision_state=states.NOSTATE, + instance_info={'image_source': uuidutils.generate_uuid(), + 'kernel': 'aaaa', 'ramdisk': 'bbbb'}, + driver_internal_info={'is_whole_disk_image': False}) + + self.service.do_node_deploy(self.context, node.uuid, rebuild=True) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.DEPLOYING, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + # last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_deploy.assert_called_once_with(mock.ANY) + # Verify instance_info values has been cleared. + self.assertNotIn('kernel', node.instance_info) + self.assertNotIn('ramdisk', node.instance_info) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + # Verify is_whole_disk_image reflects correct value on rebuild. + self.assertTrue(node.driver_internal_info['is_whole_disk_image']) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test_do_node_deploy_rebuild_active_state_waiting(self, mock_deploy, + mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + mock_deploy.return_value = states.DEPLOYWAIT + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.ACTIVE, + target_provision_state=states.NOSTATE, + instance_info={'image_source': uuidutils.generate_uuid()}) + + self.service.do_node_deploy(self.context, node.uuid, rebuild=True) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.DEPLOYWAIT, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + # last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_deploy.assert_called_once_with(mock.ANY) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test_do_node_deploy_rebuild_active_state_done(self, mock_deploy, + mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.ACTIVE, + target_provision_state=states.NOSTATE) + + self.service.do_node_deploy(self.context, node.uuid, rebuild=True) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + # last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_deploy.assert_called_once_with(mock.ANY) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test_do_node_deploy_rebuild_deployfail_state(self, mock_deploy, + mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.DEPLOYFAIL, + target_provision_state=states.NOSTATE) + + self.service.do_node_deploy(self.context, node.uuid, rebuild=True) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + # last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_deploy.assert_called_once_with(mock.ANY) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test_do_node_deploy_rebuild_error_state(self, mock_deploy, mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.ERROR, + target_provision_state=states.NOSTATE) + + self.service.do_node_deploy(self.context, node.uuid, rebuild=True) + self.service._worker_pool.waitall() + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + # last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_deploy.assert_called_once_with(mock.ANY) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + def test_do_node_deploy_rebuild_from_available_state(self, mock_iwdi): + mock_iwdi.return_value = False + self._start_service() + # test node will not rebuild if state is AVAILABLE + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.AVAILABLE) + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.do_node_deploy, + self.context, node['uuid'], rebuild=True) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.InvalidStateRequested, exc.exc_info[0]) + # Last_error should be None. + self.assertIsNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertNotIn('is_whole_disk_image', node.driver_internal_info) + + def test_do_node_deploy_worker_pool_full(self, mock_iwdi): + mock_iwdi.return_value = False + prv_state = states.AVAILABLE + tgt_prv_state = states.NOSTATE + node = obj_utils.create_test_node(self.context, + provision_state=prv_state, + target_provision_state=tgt_prv_state, + last_error=None, driver='fake') + self._start_service() + + with mock.patch.object(self.service, '_spawn_worker') as mock_spawn: + mock_spawn.side_effect = exception.NoFreeConductorWorker() + + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.do_node_deploy, + self.context, node.uuid) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0]) + self.service._worker_pool.waitall() + node.refresh() + # Make sure things were rolled back + self.assertEqual(prv_state, node.provision_state) + self.assertEqual(tgt_prv_state, node.target_provision_state) + self.assertIsNotNone(node.last_error) + # Verify reservation has been cleared. + self.assertIsNone(node.reservation) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) + self.assertFalse(node.driver_internal_info['is_whole_disk_image']) + + +@_mock_record_keepalive +class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, + tests_db_base.DbTestCase): @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') def test__do_node_deploy_driver_raises_prepare_error(self, mock_prepare, @@ -1105,163 +1336,6 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertIsNone(node.last_error) mock_deploy.assert_called_once_with(mock.ANY) - @mock.patch('ironic.conductor.task_manager.TaskManager.process_event') - def test_deploy_with_nostate_converts_to_available(self, mock_pe): - # expressly create a node using the Juno-era NOSTATE state - # and assert that it does not result in an error, and that the state - # is converted to the new AVAILABLE state. - # Mock the process_event call, because the transitions from - # AVAILABLE are tested thoroughly elsewhere - # NOTE(deva): This test can be deleted after Kilo is released - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.NOSTATE) - self.assertEqual(states.NOSTATE, node.provision_state) - self.service.do_node_deploy(self.context, node.uuid) - self.assertTrue(mock_pe.called) - node.refresh() - self.assertEqual(states.AVAILABLE, node.provision_state) - - def test_do_node_deploy_partial_ok(self): - self._start_service() - thread = self.service._spawn_worker(lambda: None) - with mock.patch.object(self.service, '_spawn_worker') as mock_spawn: - mock_spawn.return_value = thread - - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.AVAILABLE) - - self.service.do_node_deploy(self.context, node.uuid) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.DEPLOYING, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - # This is a sync operation last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_spawn.assert_called_once_with(mock.ANY, mock.ANY, - mock.ANY, None) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test_do_node_deploy_rebuild_active_state(self, mock_deploy): - # This tests manager.do_node_deploy(), the 'else' path of - # 'if new_state == states.DEPLOYDONE'. The node's states - # aren't changed in this case. - self._start_service() - mock_deploy.return_value = states.DEPLOYING - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.ACTIVE, - target_provision_state=states.NOSTATE, - instance_info={'image_source': uuidutils.generate_uuid(), - 'kernel': 'aaaa', 'ramdisk': 'bbbb'}) - - self.service.do_node_deploy(self.context, node.uuid, rebuild=True) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.DEPLOYING, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - # last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_deploy.assert_called_once_with(mock.ANY) - # Verify instance_info values has been cleared. - self.assertNotIn('kernel', node.instance_info) - self.assertNotIn('ramdisk', node.instance_info) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test_do_node_deploy_rebuild_active_state_waiting(self, mock_deploy): - self._start_service() - mock_deploy.return_value = states.DEPLOYWAIT - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.ACTIVE, - target_provision_state=states.NOSTATE, - instance_info={'image_source': uuidutils.generate_uuid()}) - - self.service.do_node_deploy(self.context, node.uuid, rebuild=True) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.DEPLOYWAIT, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - # last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_deploy.assert_called_once_with(mock.ANY) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test_do_node_deploy_rebuild_active_state_done(self, mock_deploy): - self._start_service() - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.ACTIVE, - target_provision_state=states.NOSTATE) - - self.service.do_node_deploy(self.context, node.uuid, rebuild=True) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - # last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_deploy.assert_called_once_with(mock.ANY) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test_do_node_deploy_rebuild_deployfail_state(self, mock_deploy): - self._start_service() - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.DEPLOYFAIL, - target_provision_state=states.NOSTATE) - - self.service.do_node_deploy(self.context, node.uuid, rebuild=True) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - # last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_deploy.assert_called_once_with(mock.ANY) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test_do_node_deploy_rebuild_error_state(self, mock_deploy): - self._start_service() - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.ERROR, - target_provision_state=states.NOSTATE) - - self.service.do_node_deploy(self.context, node.uuid, rebuild=True) - self.service._worker_pool.waitall() - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - # last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - mock_deploy.assert_called_once_with(mock.ANY) - - def test_do_node_deploy_rebuild_from_available_state(self): - self._start_service() - # test node will not rebuild if state is AVAILABLE - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.AVAILABLE) - exc = self.assertRaises(messaging.rpc.ExpectedException, - self.service.do_node_deploy, - self.context, node['uuid'], rebuild=True) - # Compare true exception hidden by @messaging.expected_exceptions - self.assertEqual(exception.InvalidStateRequested, exc.exc_info[0]) - # Last_error should be None. - self.assertIsNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.clean_up') def test__check_deploy_timeouts(self, mock_cleanup): self._start_service() @@ -1279,32 +1353,6 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertIsNotNone(node.last_error) mock_cleanup.assert_called_once_with(mock.ANY) - def test_do_node_deploy_worker_pool_full(self): - prv_state = states.AVAILABLE - tgt_prv_state = states.NOSTATE - node = obj_utils.create_test_node(self.context, - provision_state=prv_state, - target_provision_state=tgt_prv_state, - last_error=None, driver='fake') - self._start_service() - - with mock.patch.object(self.service, '_spawn_worker') as mock_spawn: - mock_spawn.side_effect = exception.NoFreeConductorWorker() - - exc = self.assertRaises(messaging.rpc.ExpectedException, - self.service.do_node_deploy, - self.context, node.uuid) - # Compare true exception hidden by @messaging.expected_exceptions - self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0]) - self.service._worker_pool.waitall() - node.refresh() - # Make sure things were rolled back - self.assertEqual(prv_state, node.provision_state) - self.assertEqual(tgt_prv_state, node.target_provision_state) - self.assertIsNotNone(node.last_error) - # Verify reservation has been cleared. - self.assertIsNone(node.reservation) - def test_do_node_tear_down_invalid_state(self): self._start_service() # test node.provision_state is incorrect for tear_down @@ -1332,10 +1380,11 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, @mock.patch('ironic.drivers.modules.fake.FakeDeploy.tear_down') def test_do_node_tear_down_driver_raises_error(self, mock_tear_down): # test when driver.deploy.tear_down raises exception - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.DELETING, - target_provision_state=states.AVAILABLE, - instance_info={'foo': 'bar'}) + node = obj_utils.create_test_node( + self.context, driver='fake', provision_state=states.DELETING, + target_provision_state=states.AVAILABLE, + instance_info={'foo': 'bar'}, + driver_internal_info={'is_whole_disk_image': False}) task = task_manager.TaskManager(self.context, node.uuid) self._start_service() @@ -1354,10 +1403,11 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, @mock.patch('ironic.drivers.modules.fake.FakeDeploy.tear_down') def test__do_node_tear_down_ok(self, mock_tear_down, mock_clean): # test when driver.deploy.tear_down succeeds - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=states.DELETING, - target_provision_state=states.AVAILABLE, - instance_info={'foo': 'bar'}) + node = obj_utils.create_test_node( + self.context, driver='fake', provision_state=states.DELETING, + target_provision_state=states.AVAILABLE, + instance_info={'foo': 'bar'}, + driver_internal_info={'is_whole_disk_image': False}) task = task_manager.TaskManager(self.context, node.uuid) self._start_service() @@ -1375,10 +1425,11 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, @mock.patch('ironic.drivers.modules.fake.FakeDeploy.tear_down') def _test_do_node_tear_down_from_state(self, init_state, mock_tear_down, mock_clean): - node = obj_utils.create_test_node(self.context, driver='fake', - uuid=uuidutils.generate_uuid(), - provision_state=init_state, - target_provision_state=states.AVAILABLE) + node = obj_utils.create_test_node( + self.context, driver='fake', uuid=uuidutils.generate_uuid(), + provision_state=init_state, + target_provision_state=states.AVAILABLE, + driver_internal_info={'is_whole_disk_image': False}) self.service.do_node_tear_down(self.context, node.uuid) self.service._worker_pool.waitall() @@ -1410,11 +1461,12 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, prv_state = states.ACTIVE tgt_prv_state = states.NOSTATE fake_instance_info = {'foo': 'bar'} - node = obj_utils.create_test_node(self.context, driver='fake', - provision_state=prv_state, - target_provision_state=tgt_prv_state, - instance_info=fake_instance_info, - last_error=None) + driver_internal_info = {'is_whole_disk_image': False} + node = obj_utils.create_test_node( + self.context, driver='fake', provision_state=prv_state, + target_provision_state=tgt_prv_state, + instance_info=fake_instance_info, + driver_internal_info=driver_internal_info, last_error=None) self._start_service() mock_spawn.side_effect = exception.NoFreeConductorWorker() @@ -1426,8 +1478,9 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0]) self.service._worker_pool.waitall() node.refresh() - # Assert instance_info was not touched + # Assert instance_info/driver_internal_info was not touched self.assertEqual(fake_instance_info, node.instance_info) + self.assertEqual(driver_internal_info, node.driver_internal_info) # Make sure things were rolled back self.assertEqual(prv_state, node.provision_state) self.assertEqual(tgt_prv_state, node.target_provision_state) @@ -1511,9 +1564,12 @@ class MiscTestCase(_ServiceSetUpMixin, _CommonMixIn, tests_db_base.DbTestCase): self.assertTrue(self.service._mapped_to_this_conductor(n['uuid'], 'fake')) self.assertFalse(self.service._mapped_to_this_conductor(n['uuid'], + 'otherdriver')) - def test_validate_driver_interfaces(self): + @mock.patch.object(images, 'is_whole_disk_image') + def test_validate_driver_interfaces(self, mock_iwdi): + mock_iwdi.return_value = False node = obj_utils.create_test_node(self.context, driver='fake') ret = self.service.validate_driver_interfaces(self.context, node.uuid) @@ -1523,8 +1579,11 @@ class MiscTestCase(_ServiceSetUpMixin, _CommonMixIn, tests_db_base.DbTestCase): 'management': {'result': True}, 'deploy': {'result': True}} self.assertEqual(expected, ret) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) - def test_validate_driver_interfaces_validation_fail(self): + @mock.patch.object(images, 'is_whole_disk_image') + def test_validate_driver_interfaces_validation_fail(self, mock_iwdi): + mock_iwdi.return_value = False node = obj_utils.create_test_node(self.context, driver='fake') with mock.patch( 'ironic.drivers.modules.fake.FakeDeploy.validate' @@ -1535,6 +1594,7 @@ class MiscTestCase(_ServiceSetUpMixin, _CommonMixIn, tests_db_base.DbTestCase): node.uuid) self.assertFalse(ret['deploy']['result']) self.assertEqual(reason, ret['deploy']['reason']) + mock_iwdi.assert_called_once_with(self.context, node.instance_info) @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') diff --git a/ironic/tests/db/utils.py b/ironic/tests/db/utils.py index 0e5da57b16..4b91171f2b 100644 --- a/ironic/tests/db/utils.py +++ b/ironic/tests/db/utils.py @@ -69,6 +69,12 @@ def get_test_pxe_driver_info(): } +def get_test_pxe_driver_internal_info(): + return { + "is_whole_disk_image": False, + } + + def get_test_pxe_instance_info(): return { "image_source": "glance://image_uuid", diff --git a/ironic/tests/drivers/pxe_config.template b/ironic/tests/drivers/pxe_config.template index d4dad50072..c6a28313a1 100644 --- a/ironic/tests/drivers/pxe_config.template +++ b/ironic/tests/drivers/pxe_config.template @@ -6,6 +6,11 @@ append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk seli ipappend 3 -label boot +label boot_partition kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk root={{ ROOT }} ro text test_param + + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index 6308a769b3..6789b96e6d 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -27,6 +27,7 @@ from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import uuidutils import requests +import testtools from ironic.common import boot_devices from ironic.common import disk_partitioner @@ -51,22 +52,47 @@ kernel deploy_kernel append initrd=deploy_ramdisk ipappend 3 -label boot +label boot_partition kernel kernel append initrd=ramdisk root={{ ROOT }} + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} """ -_PXECONF_BOOT = """ -default boot +_PXECONF_BOOT_PARTITION = """ +default boot_partition label deploy kernel deploy_kernel append initrd=deploy_ramdisk ipappend 3 -label boot +label boot_partition kernel kernel append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +""" + +_PXECONF_BOOT_WHOLE_DISK = """ +default boot_whole_disk + +label deploy +kernel deploy_kernel +append initrd=deploy_ramdisk +ipappend 3 + +label boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} + +label boot_whole_disk +COM32 chain.c32 +append mbr:0x12345678 """ _IPXECONF_DEPLOY = """ @@ -81,28 +107,61 @@ kernel deploy_kernel initrd deploy_ramdisk boot -:boot +:boot_partition kernel kernel append initrd=ramdisk root={{ ROOT }} boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot """ -_IPXECONF_BOOT = """ +_IPXECONF_BOOT_PARTITION = """ #!ipxe dhcp -goto boot +goto boot_partition :deploy kernel deploy_kernel initrd deploy_ramdisk boot -:boot +:boot_partition kernel kernel append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot +""" + +_IPXECONF_BOOT_WHOLE_DISK = """ +#!ipxe + +dhcp + +goto boot_whole_disk + +:deploy +kernel deploy_kernel +initrd deploy_ramdisk +boot + +:boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} +boot + +:boot_whole_disk +kernel chain.c32 +append mbr:0x12345678 +boot """ _UEFI_PXECONF_DEPLOY = """ @@ -114,13 +173,17 @@ image=deploy_kernel append="ro text" image=kernel - label=boot + label=boot_partition initrd=ramdisk append="root={{ ROOT }}" + +image=chain.c32 + label=boot_whole_disk + append mbr:{{ DISK_IDENTIFIER }} """ -_UEFI_PXECONF_BOOT = """ -default=boot +_UEFI_PXECONF_BOOT_PARTITION = """ +default=boot_partition image=deploy_kernel label=deploy @@ -128,9 +191,31 @@ image=deploy_kernel append="ro text" image=kernel - label=boot + label=boot_partition initrd=ramdisk append="root=UUID=12345678-1234-1234-1234-1234567890abcdef" + +image=chain.c32 + label=boot_whole_disk + append mbr:{{ DISK_IDENTIFIER }} +""" + +_UEFI_PXECONF_BOOT_WHOLE_DISK = """ +default=boot_whole_disk + +image=deploy_kernel + label=deploy + initrd=deploy_ramdisk + append="ro text" + +image=kernel + label=boot_partition + initrd=ramdisk + append="root={{ ROOT }}" + +image=chain.c32 + label=boot_whole_disk + append mbr:0x12345678 """ @@ -148,7 +233,7 @@ class PhysicalWorkTestCase(tests_base.TestCase): parent_mock.attach_mock(mocker, name) return parent_mock - def _test_deploy(self, boot_option=None): + def _test_deploy_partition_image(self, boot_option=None): """Check loosely all functions are called with right args.""" address = '127.0.0.1' port = 3306 @@ -179,10 +264,10 @@ class PhysicalWorkTestCase(tests_base.TestCase): parent_mock.make_partitions.return_value = {'root': root_part, 'swap': swap_part} calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), mock.call.is_block_device(dev), + mock.call.get_image_mb(image_path), mock.call.destroy_disk_metadata(dev, node_uuid)] if boot_option: @@ -207,24 +292,26 @@ class PhysicalWorkTestCase(tests_base.TestCase): if boot_option: kwargs = {'boot_option': boot_option} - returned_root_uuid = utils.deploy(address, port, iqn, lun, - image_path, root_mb, swap_mb, - ephemeral_mb, ephemeral_format, - node_uuid, **kwargs) + returned_root_uuid = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid, **kwargs) self.assertEqual(calls_expected, parent_mock.mock_calls) self.assertEqual(root_uuid, returned_root_uuid) - def test_deploy_without_boot_option(self): - self._test_deploy() + def test_deploy_partition_image_without_boot_option(self): + self._test_deploy_partition_image() - def test_deploy_netboot(self): - self._test_deploy(boot_option="netboot") + def test_deploy_partition_image_netboot(self): + self._test_deploy_partition_image(boot_option="netboot") - def test_deploy_localboot(self): - self._test_deploy(boot_option="local") + def test_deploy_partition_image_localboot(self): + self._test_deploy_partition_image(boot_option="local") - def test_deploy_without_swap(self): + def test_deploy_partition_image_without_swap(self): """Check loosely all functions are called with right args.""" address = '127.0.0.1' port = 3306 @@ -253,10 +340,10 @@ class PhysicalWorkTestCase(tests_base.TestCase): parent_mock.block_uuid.return_value = root_uuid parent_mock.make_partitions.return_value = {'root': root_part} calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), mock.call.is_block_device(dev), + mock.call.get_image_mb(image_path), mock.call.destroy_disk_metadata(dev, node_uuid), mock.call.make_partitions(dev, root_mb, swap_mb, ephemeral_mb, @@ -269,15 +356,17 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock.call.logout_iscsi(address, port, iqn), mock.call.delete_iscsi(address, port, iqn)] - returned_root_uuid = utils.deploy(address, port, iqn, lun, - image_path, root_mb, swap_mb, - ephemeral_mb, ephemeral_format, - node_uuid) + returned_root_uuid = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid) self.assertEqual(calls_expected, parent_mock.mock_calls) self.assertEqual(root_uuid, returned_root_uuid) - def test_deploy_with_ephemeral(self): + def test_deploy_partition_image_with_ephemeral(self): """Check loosely all functions are called with right args.""" address = '127.0.0.1' port = 3306 @@ -311,10 +400,10 @@ class PhysicalWorkTestCase(tests_base.TestCase): 'ephemeral': ephemeral_part, 'root': root_part} calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), mock.call.is_block_device(dev), + mock.call.get_image_mb(image_path), mock.call.destroy_disk_metadata(dev, node_uuid), mock.call.make_partitions(dev, root_mb, swap_mb, ephemeral_mb, @@ -332,15 +421,17 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock.call.logout_iscsi(address, port, iqn), mock.call.delete_iscsi(address, port, iqn)] - returned_root_uuid = utils.deploy(address, port, iqn, lun, - image_path, root_mb, swap_mb, - ephemeral_mb, ephemeral_format, - node_uuid) + returned_root_uuid = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid) self.assertEqual(calls_expected, parent_mock.mock_calls) self.assertEqual(root_uuid, returned_root_uuid) - def test_deploy_preserve_ephemeral(self): + def test_deploy_partition_image_preserve_ephemeral(self): """Check if all functions are called with right args.""" address = '127.0.0.1' port = 3306 @@ -375,10 +466,10 @@ class PhysicalWorkTestCase(tests_base.TestCase): 'root': root_part} parent_mock.block_uuid.return_value = root_uuid calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), mock.call.is_block_device(dev), + mock.call.get_image_mb(image_path), mock.call.make_partitions(dev, root_mb, swap_mb, ephemeral_mb, configdrive_mb, @@ -393,18 +484,21 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock.call.logout_iscsi(address, port, iqn), mock.call.delete_iscsi(address, port, iqn)] - returned_root_uuid = utils.deploy(address, port, iqn, lun, - image_path, root_mb, swap_mb, - ephemeral_mb, ephemeral_format, - node_uuid, preserve_ephemeral=True, - boot_option="netboot") + returned_root_uuid = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid, + preserve_ephemeral=True, + boot_option="netboot") self.assertEqual(calls_expected, parent_mock.mock_calls) self.assertFalse(parent_mock.mkfs_ephemeral.called) self.assertFalse(parent_mock.get_dev_block_size.called) self.assertEqual(root_uuid, returned_root_uuid) @mock.patch.object(common_utils, 'unlink_without_raise') - def test_deploy_with_configdrive(self, mock_unlink): + def test_deploy_partition_image_with_configdrive(self, mock_unlink): """Check loosely all functions are called with right args.""" address = '127.0.0.1' port = 3306 @@ -439,10 +533,10 @@ class PhysicalWorkTestCase(tests_base.TestCase): configdrive_part} parent_mock._get_configdrive.return_value = (10, 'configdrive-path') calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), mock.call.is_block_device(dev), + mock.call.get_image_mb(image_path), mock.call.destroy_disk_metadata(dev, node_uuid), mock.call._get_configdrive(configdrive_url, node_uuid), @@ -459,16 +553,49 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock.call.logout_iscsi(address, port, iqn), mock.call.delete_iscsi(address, port, iqn)] - returned_root_uuid = utils.deploy(address, port, iqn, lun, - image_path, root_mb, swap_mb, - ephemeral_mb, ephemeral_format, - node_uuid, - configdrive=configdrive_url) + returned_root_uuid = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid, + configdrive=configdrive_url) self.assertEqual(calls_expected, parent_mock.mock_calls) self.assertEqual(root_uuid, returned_root_uuid) mock_unlink.assert_called_once_with('configdrive-path') + @mock.patch.object(utils, 'get_disk_identifier') + def test_deploy_whole_disk_image(self, mock_gdi): + """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' + name_list = ['get_dev', 'discovery', 'login_iscsi', 'logout_iscsi', + 'delete_iscsi', 'is_block_device', 'populate_image', + 'notify'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.is_block_device.return_value = True + mock_gdi.return_value = '0x12345678' + calls_expected = [mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.populate_image(image_path, dev), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + utils.deploy_disk_image(address, port, iqn, lun, + image_path, node_uuid) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + @mock.patch.object(common_utils, 'execute') def test_verify_iscsi_connection_raises(self, mock_exec): iqn = 'iqn.xyz' @@ -552,6 +679,7 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock_check_dev.assert_called_once_with(address, port, iqn) + @mock.patch.object(utils, 'is_block_device', lambda d: True) def test_always_logout_and_delete_iscsi(self): """Check if logout_iscsi() and delete_iscsi() are called. @@ -590,9 +718,9 @@ class PhysicalWorkTestCase(tests_base.TestCase): parent_mock.get_image_mb.return_value = 1 parent_mock.work_on_disk.side_effect = TestException calls_expected = [mock.call.get_dev(address, port, iqn, lun), - mock.call.get_image_mb(image_path), mock.call.discovery(address, port), mock.call.login_iscsi(address, port, iqn), + mock.call.get_image_mb(image_path), mock.call.work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path, @@ -602,7 +730,7 @@ class PhysicalWorkTestCase(tests_base.TestCase): mock.call.logout_iscsi(address, port, iqn), mock.call.delete_iscsi(address, port, iqn)] - self.assertRaises(TestException, utils.deploy, + self.assertRaises(TestException, utils.deploy_partition_image, address, port, iqn, lun, image_path, root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid) @@ -623,36 +751,73 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): self.addCleanup(os.unlink, fname) return fname - def test_switch_pxe_config(self): + def test_switch_pxe_config_partition_image(self): boot_mode = 'bios' fname = self._create_config() utils.switch_pxe_config(fname, - '12345678-1234-1234-1234-1234567890abcdef', - boot_mode) + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) with open(fname, 'r') as f: pxeconf = f.read() - self.assertEqual(_PXECONF_BOOT, pxeconf) + self.assertEqual(_PXECONF_BOOT_PARTITION, pxeconf) - def test_switch_ipxe_config(self): + def test_switch_pxe_config_whole_disk_image(self): + boot_mode = 'bios' + fname = self._create_config() + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_PXECONF_BOOT_WHOLE_DISK, pxeconf) + + def test_switch_ipxe_config_partition_image(self): boot_mode = 'bios' cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(ipxe=True) utils.switch_pxe_config(fname, - '12345678-1234-1234-1234-1234567890abcdef', - boot_mode) + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) with open(fname, 'r') as f: pxeconf = f.read() - self.assertEqual(_IPXECONF_BOOT, pxeconf) + self.assertEqual(_IPXECONF_BOOT_PARTITION, pxeconf) - def test_switch_uefi_pxe_config(self): + def test_switch_ipxe_config_whole_disk_image(self): + boot_mode = 'bios' + cfg.CONF.set_override('ipxe_enabled', True, 'pxe') + fname = self._create_config(ipxe=True) + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) + + def test_switch_uefi_pxe_config_partition_image(self): boot_mode = 'uefi' fname = self._create_config(boot_mode=boot_mode) utils.switch_pxe_config(fname, - '12345678-1234-1234-1234-1234567890abcdef', - boot_mode) + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) with open(fname, 'r') as f: pxeconf = f.read() - self.assertEqual(_UEFI_PXECONF_BOOT, pxeconf) + self.assertEqual(_UEFI_PXECONF_BOOT_PARTITION, pxeconf) + + def test_switch_uefi_config_whole_disk_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode) + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_WHOLE_DISK, pxeconf) @mock.patch('time.sleep', lambda sec: None) @@ -727,34 +892,21 @@ class WorkOnDiskTestCase(tests_base.TestCase): self.mock_mp.return_value = {'swap': self.swap_part, 'root': self.root_part} - def test_no_parent_device(self): + def test_no_root_partition(self): self.mock_ibd.return_value = False self.assertRaises(exception.InstanceDeployFailure, utils.work_on_disk, self.dev, self.root_mb, self.swap_mb, self.ephemeral_mb, self.ephemeral_format, self.image_path, 'fake-uuid') - self.mock_ibd.assert_called_once_with(self.dev) - self.assertFalse(self.mock_mp.called, - "make_partitions mock was unexpectedly called.") - - def test_no_root_partition(self): - self.mock_ibd.side_effect = [True, False] - calls = [mock.call(self.dev), - mock.call(self.root_part)] - self.assertRaises(exception.InstanceDeployFailure, - utils.work_on_disk, self.dev, self.root_mb, - self.swap_mb, self.ephemeral_mb, - self.ephemeral_format, self.image_path, 'fake-uuid') - self.assertEqual(self.mock_ibd.call_args_list, calls) + self.mock_ibd.assert_called_once_with(self.root_part) self.mock_mp.assert_called_once_with(self.dev, self.root_mb, self.swap_mb, self.ephemeral_mb, self.configdrive_mb, commit=True, boot_option="netboot") def test_no_swap_partition(self): - self.mock_ibd.side_effect = [True, True, False] - calls = [mock.call(self.dev), - mock.call(self.root_part), + self.mock_ibd.side_effect = [True, False] + calls = [mock.call(self.root_part), mock.call(self.swap_part)] self.assertRaises(exception.InstanceDeployFailure, utils.work_on_disk, self.dev, self.root_mb, @@ -776,9 +928,8 @@ class WorkOnDiskTestCase(tests_base.TestCase): self.mock_mp.return_value = {'ephemeral': ephemeral_part, 'swap': swap_part, 'root': root_part} - self.mock_ibd.side_effect = [True, True, True, False] - calls = [mock.call(self.dev), - mock.call(root_part), + self.mock_ibd.side_effect = [True, True, False] + calls = [mock.call(root_part), mock.call(swap_part), mock.call(ephemeral_part)] self.assertRaises(exception.InstanceDeployFailure, @@ -804,9 +955,8 @@ class WorkOnDiskTestCase(tests_base.TestCase): self.mock_mp.return_value = {'swap': swap_part, 'configdrive': configdrive_part, 'root': root_part} - self.mock_ibd.side_effect = [True, True, True, False] - calls = [mock.call(self.dev), - mock.call(root_part), + self.mock_ibd.side_effect = [True, True, False] + calls = [mock.call(root_part), mock.call(swap_part), mock.call(configdrive_part)] self.assertRaises(exception.InstanceDeployFailure, @@ -1217,3 +1367,41 @@ class TrySetBootDeviceTestCase(db_base.DbTestCase): task, boot_devices.DISK, persistent=True) node_set_boot_device_mock.assert_called_once_with( task, boot_devices.DISK, persistent=True) + + +@mock.patch.object(utils, 'is_block_device') +@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 + image_path = '/tmp/xyz/image' + 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, image_path) 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 + image_path = '/tmp/xyz/image' + expected_dev = '/dev/fake' + mock_ibd.return_value = True + with utils._iscsi_setup_and_handle_errors(address, port, + iqn, lun, image_path) as dev: + self.assertEqual(expected_dev, dev) + + mock_ibd.assert_called_once_with(expected_dev) diff --git a/ironic/tests/drivers/test_iscsi_deploy.py b/ironic/tests/drivers/test_iscsi_deploy.py index 9e6f5c789f..379904fdfc 100644 --- a/ironic/tests/drivers/test_iscsi_deploy.py +++ b/ironic/tests/drivers/test_iscsi_deploy.py @@ -42,15 +42,18 @@ CONF = cfg.CONF INST_INFO_DICT = db_utils.get_test_pxe_instance_info() DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() +DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): def test_parse_instance_info_good(self): # make sure we get back the expected things - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=INST_INFO_DICT) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT + ) info = iscsi_deploy.parse_instance_info(node) self.assertIsNotNone(info.get('image_source')) self.assertIsNotNone(info.get('root_gb')) @@ -61,7 +64,10 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): # make sure error is raised when info is missing info = dict(INST_INFO_DICT) del info['image_source'] - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.MissingParameterValue, iscsi_deploy.parse_instance_info, node) @@ -70,7 +76,11 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): # make sure error is raised when info is missing info = dict(INST_INFO_DICT) del info['root_gb'] - node = obj_utils.create_test_node(self.context, instance_info=info) + + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.MissingParameterValue, iscsi_deploy.parse_instance_info, node) @@ -78,7 +88,10 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): def test_parse_instance_info_invalid_root_gb(self): info = dict(INST_INFO_DICT) info['root_gb'] = 'foobar' - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.InvalidParameterValue, iscsi_deploy.parse_instance_info, node) @@ -89,7 +102,10 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): info = dict(INST_INFO_DICT) info['ephemeral_gb'] = ephemeral_gb info['ephemeral_format'] = ephemeral_fmt - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) data = iscsi_deploy.parse_instance_info(node) self.assertEqual(ephemeral_gb, data.get('ephemeral_gb')) self.assertEqual(ephemeral_fmt, data.get('ephemeral_format')) @@ -98,7 +114,11 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): info = dict(INST_INFO_DICT) info['ephemeral_gb'] = 'foobar' info['ephemeral_format'] = 'exttest' - node = obj_utils.create_test_node(self.context, instance_info=info) + + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.InvalidParameterValue, iscsi_deploy.parse_instance_info, node) @@ -110,7 +130,10 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): info['ephemeral_gb'] = ephemeral_gb info['ephemeral_format'] = None self.config(default_ephemeral_format=ephemeral_fmt, group='pxe') - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) instance_info = iscsi_deploy.parse_instance_info(node) self.assertEqual(ephemeral_fmt, instance_info['ephemeral_format']) @@ -119,9 +142,12 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): for opt in ['true', 'TRUE', 'True', 't', 'on', 'yes', 'y', '1']: info['preserve_ephemeral'] = opt - node = obj_utils.create_test_node(self.context, - uuid=uuidutils.generate_uuid(), - instance_info=info) + + node = obj_utils.create_test_node( + self.context, uuid=uuidutils.generate_uuid(), + instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) data = iscsi_deploy.parse_instance_info(node) self.assertTrue(data.get('preserve_ephemeral')) @@ -130,16 +156,21 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): for opt in ['false', 'FALSE', 'False', 'f', 'off', 'no', 'n', '0']: info['preserve_ephemeral'] = opt - node = obj_utils.create_test_node(self.context, - uuid=uuidutils.generate_uuid(), - instance_info=info) + node = obj_utils.create_test_node( + self.context, uuid=uuidutils.generate_uuid(), + instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) data = iscsi_deploy.parse_instance_info(node) self.assertFalse(data.get('preserve_ephemeral')) def test_parse_instance_info_invalid_preserve_ephemeral(self): info = dict(INST_INFO_DICT) info['preserve_ephemeral'] = 'foobar' - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.InvalidParameterValue, iscsi_deploy.parse_instance_info, node) @@ -147,7 +178,10 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): def test_parse_instance_info_configdrive(self): info = dict(INST_INFO_DICT) info['configdrive'] = 'http://1.2.3.4/cd' - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) instance_info = iscsi_deploy.parse_instance_info(node) self.assertEqual('http://1.2.3.4/cd', instance_info['configdrive']) @@ -156,23 +190,31 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): info['image_source'] = 'file:///image.qcow2' info['kernel'] = 'file:///image.vmlinuz' info['ramdisk'] = 'file:///image.initrd' - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) iscsi_deploy.parse_instance_info(node) def test_parse_instance_info_nonglance_image_no_kernel(self): info = INST_INFO_DICT.copy() info['image_source'] = 'file:///image.qcow2' info['ramdisk'] = 'file:///image.initrd' - node = obj_utils.create_test_node(self.context, instance_info=info) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.assertRaises(exception.MissingParameterValue, iscsi_deploy.parse_instance_info, node) @mock.patch.object(image_service, 'get_image_service') def test_validate_image_properties_glance_image(self, image_service_mock): - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=INST_INFO_DICT, - driver_info=DRV_INFO_DICT) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) d_info = pxe._parse_deploy_info(node) image_service_mock.return_value.show.return_value = { 'properties': {'kernel_id': '1111', 'ramdisk_id': '2222'}, @@ -187,10 +229,12 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): @mock.patch.object(image_service, 'get_image_service') def test_validate_image_properties_glance_image_missing_prop(self, image_service_mock): - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=INST_INFO_DICT, - driver_info=DRV_INFO_DICT) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) d_info = pxe._parse_deploy_info(node) image_service_mock.return_value.show.return_value = { 'properties': {'kernel_id': '1111'}, @@ -239,10 +283,12 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): 'root_gb': 100, } image_service_show_mock.return_value = {'size': 1, 'properties': {}} - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=instance_info, - driver_info=DRV_INFO_DICT) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) d_info = pxe._parse_deploy_info(node) iscsi_deploy.validate_image_properties(self.context, d_info, ['kernel', 'ramdisk']) @@ -260,15 +306,38 @@ class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): } img_service_show_mock.side_effect = exception.ImageRefValidationFailed( image_href='http://ubuntu', reason='HTTPError') - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=instance_info, - driver_info=DRV_INFO_DICT) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) d_info = pxe._parse_deploy_info(node) self.assertRaises(exception.InvalidParameterValue, iscsi_deploy.validate_image_properties, self.context, d_info, ['kernel', 'ramdisk']) + def test_parse_instance_info_whole_disk_image(self): + driver_internal_info = dict(DRV_INTERNAL_INFO_DICT) + driver_internal_info['is_whole_disk_image'] = True + node = obj_utils.create_test_node( + self.context, instance_info=INST_INFO_DICT, + driver_internal_info=driver_internal_info, + ) + instance_info = iscsi_deploy.parse_instance_info(node) + self.assertIsNotNone(instance_info.get('image_source')) + self.assertIsNotNone(instance_info.get('root_gb')) + self.assertEqual(0, instance_info.get('swap_mb')) + self.assertEqual(0, instance_info.get('ephemeral_gb')) + self.assertIsNone(instance_info.get('configdrive')) + + def test_parse_instance_info_whole_disk_image_missing_root(self): + info = dict(INST_INFO_DICT) + del info['root_gb'] + node = obj_utils.create_test_node(self.context, instance_info=info) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, node) + class IscsiDeployPrivateMethodsTestCase(db_base.DbTestCase): @@ -278,6 +347,7 @@ class IscsiDeployPrivateMethodsTestCase(db_base.DbTestCase): 'driver': 'fake_pxe', 'instance_info': INST_INFO_DICT, 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, } mgr_utils.mock_the_extension_manager(driver="fake_pxe") self.node = obj_utils.create_test_node(self.context, **n) @@ -304,6 +374,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): 'driver': 'fake_pxe', 'instance_info': instance_info, 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, } mgr_utils.mock_the_extension_manager(driver="fake_pxe") self.node = obj_utils.create_test_node(self.context, **n) @@ -440,7 +511,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, 'InstanceImageCache') @mock.patch.object(manager_utils, 'node_power_action') - @mock.patch.object(deploy_utils, 'deploy') + @mock.patch.object(deploy_utils, 'deploy_partition_image') def test_continue_deploy_fail(self, deploy_mock, power_mock, mock_image_cache): kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} @@ -464,7 +535,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, 'InstanceImageCache') @mock.patch.object(manager_utils, 'node_power_action') - @mock.patch.object(deploy_utils, 'deploy') + @mock.patch.object(deploy_utils, 'deploy_partition_image') def test_continue_deploy_ramdisk_fails(self, deploy_mock, power_mock, mock_image_cache): kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789', @@ -488,7 +559,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): @mock.patch.object(iscsi_deploy, 'get_deploy_info') @mock.patch.object(iscsi_deploy, 'InstanceImageCache') @mock.patch.object(manager_utils, 'node_power_action') - @mock.patch.object(deploy_utils, 'deploy') + @mock.patch.object(deploy_utils, 'deploy_partition_image') def test_continue_deploy(self, deploy_mock, power_mock, mock_image_cache, mock_deploy_info, mock_log): kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} @@ -531,6 +602,47 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): mock_image_cache.assert_called_once_with() mock_image_cache.return_value.clean_up.assert_called_once_with() + @mock.patch.object(iscsi_deploy, 'LOG') + @mock.patch.object(iscsi_deploy, 'get_deploy_info') + @mock.patch.object(iscsi_deploy, 'InstanceImageCache') + @mock.patch.object(manager_utils, 'node_power_action') + @mock.patch.object(deploy_utils, 'deploy_disk_image') + def test_continue_deploy_whole_disk_image( + self, deploy_mock, power_mock, mock_image_cache, mock_deploy_info, + mock_log): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + mock_deploy_info.return_value = { + 'address': '123456', + 'image_path': (u'/var/lib/ironic/images/1be26c0b-03f2-4d2e-ae87-' + u'c02d7f33c123/disk'), + 'iqn': 'aaa-bbb', + 'lun': '1', + 'node_uuid': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + 'port': '3260', + 'root_mb': 102400, + } + log_params = mock_deploy_info.return_value.copy() + expected_dict = { + 'node': self.node.uuid, + 'params': log_params, + } + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + mock_log.isEnabledFor.return_value = True + iscsi_deploy.continue_deploy(task, **kwargs) + mock_log.debug.assert_called_once_with( + mock.ANY, expected_dict) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNone(task.node.last_error) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + def test_get_deploy_info_boot_option_default(self): instance_info = self.node.instance_info instance_info['deploy_key'] = 'key' @@ -588,8 +700,9 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): task, error=None, iqn='iqn-qweqwe', key='abcdef', address='1.2.3.4') self.assertEqual('some-root-uuid', ret_val) - self.assertEqual('some-root-uuid', - task.node.driver_internal_info['root_uuid']) + self.assertEqual( + 'some-root-uuid', + task.node.driver_internal_info['root_uuid_or_disk_id']) @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') def test_do_agent_iscsi_deploy_start_iscsi_failure(self, diff --git a/ironic/tests/drivers/test_pxe.py b/ironic/tests/drivers/test_pxe.py index 03dbef53b9..ebc559ae60 100644 --- a/ironic/tests/drivers/test_pxe.py +++ b/ironic/tests/drivers/test_pxe.py @@ -50,16 +50,21 @@ CONF = cfg.CONF INST_INFO_DICT = db_utils.get_test_pxe_instance_info() DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() +DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() class PXEValidateParametersTestCase(db_base.DbTestCase): def test__parse_deploy_info(self): # make sure we get back the expected things - node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=INST_INFO_DICT, - driver_info=DRV_INFO_DICT) + node = obj_utils.create_test_node( + self.context, + driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + info = pxe._parse_deploy_info(node) self.assertIsNotNone(info.get('deploy_ramdisk')) self.assertIsNotNone(info.get('deploy_kernel')) @@ -114,6 +119,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'driver': 'fake_pxe', 'instance_info': INST_INFO_DICT, 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, } mgr_utils.mock_the_extension_manager(driver="fake_pxe") self.node = obj_utils.create_test_node(self.context, **n) @@ -159,6 +165,25 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): self.assertEqual('instance_ramdisk_uuid', self.node.instance_info.get('ramdisk')) + @mock.patch.object(base_image_service.BaseImageService, '_show') + def test__get_image_info_whole_disk_image(self, show_mock): + properties = {'properties': None} + + expected_info = {'deploy_ramdisk': + (DRV_INFO_DICT['deploy_ramdisk'], + os.path.join(CONF.pxe.tftp_root, + self.node.uuid, + 'deploy_ramdisk')), + 'deploy_kernel': + (DRV_INFO_DICT['deploy_kernel'], + os.path.join(CONF.pxe.tftp_root, + self.node.uuid, + 'deploy_kernel'))} + show_mock.return_value = properties + self.node.driver_internal_info['is_whole_disk_image'] = True + image_info = pxe._get_image_info(self.node, self.context) + self.assertEqual(expected_info, image_info) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') @mock.patch.object(pxe_utils, '_build_pxe_config') def _test_build_pxe_config_options(self, build_pxe_mock, deploy_opts_mock, @@ -231,9 +256,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'ramdisk': ('ramdisk_id', os.path.join(root_dir, self.node.uuid, - 'ramdisk')) - } - + 'ramdisk'))} options = pxe._build_pxe_config_options(self.node, image_info, self.context) @@ -245,6 +268,66 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): def test__build_pxe_config_options_ipxe(self): self._test_build_pxe_config_options(ipxe_enabled=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') + @mock.patch.object(pxe_utils, '_build_pxe_config') + def _test_build_pxe_config_options_whole_disk_image(self, build_pxe_mock, + deploy_opts_mock, ipxe_enabled=False): + self.config(pxe_append_params='test_param', group='pxe') + # NOTE: right '/' should be removed from url string + self.config(api_url='http://192.168.122.184:6385/', group='conductor') + self.config(disk_devices='sda', group='pxe') + + fake_deploy_opts = {'iscsi_target_iqn': 'fake-iqn', + 'deployment_id': 'fake-deploy-id', + 'deployment_key': 'fake-deploy-key', + 'disk': 'fake-disk', + 'ironic_api_url': 'fake-api-url'} + + deploy_opts_mock.return_value = fake_deploy_opts + + tftp_server = CONF.pxe.tftp_server + + if ipxe_enabled: + http_url = 'http://192.1.2.3:1234' + self.config(ipxe_enabled=True, group='pxe') + self.config(http_url=http_url, group='pxe') + + deploy_kernel = os.path.join(http_url, self.node.uuid, + 'deploy_kernel') + deploy_ramdisk = os.path.join(http_url, self.node.uuid, + 'deploy_ramdisk') + root_dir = CONF.pxe.http_root + else: + deploy_kernel = os.path.join(CONF.pxe.tftp_root, self.node.uuid, + 'deploy_kernel') + deploy_ramdisk = os.path.join(CONF.pxe.tftp_root, self.node.uuid, + 'deploy_ramdisk') + root_dir = CONF.pxe.tftp_root + + expected_options = { + 'deployment_ari_path': deploy_ramdisk, + 'pxe_append_params': 'test_param', + 'deployment_aki_path': deploy_kernel, + 'tftp_server': tftp_server, + } + + expected_options.update(fake_deploy_opts) + + image_info = {'deploy_kernel': ('deploy_kernel', + os.path.join(root_dir, + self.node.uuid, + 'deploy_kernel')), + 'deploy_ramdisk': ('deploy_ramdisk', + os.path.join(root_dir, + self.node.uuid, + 'deploy_ramdisk')), + } + self.node.driver_internal_info['is_whole_disk_image'] = True + options = pxe._build_pxe_config_options(self.node, + image_info, + self.context) + self.assertEqual(expected_options, options) + def test_get_token_file_path(self): node_uuid = self.node.uuid self.assertEqual('/tftpboot/token-' + node_uuid, @@ -311,10 +394,12 @@ class PXEDriverTestCase(db_base.DbTestCase): mgr_utils.mock_the_extension_manager(driver="fake_pxe") instance_info = INST_INFO_DICT instance_info['deploy_key'] = 'fake-56789' - self.node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=instance_info, - driver_info=DRV_INFO_DICT) + self.node = obj_utils.create_test_node( + self.context, + driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT) self.port = obj_utils.create_test_port(self.context, node_id=self.node.id) self.config(group='conductor', api_url='http://127.0.0.1:1234/') @@ -338,6 +423,13 @@ class PXEDriverTestCase(db_base.DbTestCase): shared=True) as task: task.driver.deploy.validate(task) + @mock.patch.object(base_image_service.BaseImageService, '_show') + def test_validate_good_whole_disk_image(self, mock_glance): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.deploy.validate(task) + def test_validate_fail(self): info = dict(INST_INFO_DICT) del info['image_source'] @@ -570,7 +662,8 @@ class PXEDriverTestCase(db_base.DbTestCase): mock_get_cap.return_value = None self.node.provision_state = states.ACTIVE - self.node.driver_internal_info = {'root_uuid': 'abcd'} + self.node.driver_internal_info = {'root_uuid_or_disk_id': 'abcd', + 'is_whole_disk_image': False} self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: @@ -583,7 +676,7 @@ class PXEDriverTestCase(db_base.DbTestCase): task.node, None) mock_pxe_get_cfg.assert_called_once_with(task.node.uuid) - mock_switch.assert_called_once_with('/path', 'abcd', None) + mock_switch.assert_called_once_with('/path', 'abcd', None, False) @mock.patch.object(keystone, 'token_expires_soon') @mock.patch.object(deploy_utils, 'get_image_mb') @@ -712,7 +805,7 @@ class PXEDriverTestCase(db_base.DbTestCase): @mock.patch.object(deploy_utils, 'notify_deploy_complete') @mock.patch.object(deploy_utils, 'switch_pxe_config') @mock.patch.object(iscsi_deploy, 'InstanceImageCache') - @mock.patch.object(deploy_utils, 'deploy') + @mock.patch.object(deploy_utils, 'deploy_partition_image') def _test_continue_deploy(self, is_localboot, mock_deploy, mock_image_cache, mock_switch_config, notify_mock, mock_node_boot_dev, mock_clean_pxe): @@ -732,6 +825,7 @@ class PXEDriverTestCase(db_base.DbTestCase): root_uuid = "12345678-1234-1234-1234-1234567890abcxyz" mock_deploy.return_value = root_uuid boot_mode = None + is_whole_disk_image = False with task_manager.acquire(self.context, self.node.uuid) as task: task.driver.vendor._continue_deploy( @@ -741,7 +835,7 @@ class PXEDriverTestCase(db_base.DbTestCase): self.assertEqual(states.ACTIVE, self.node.provision_state) self.assertEqual(states.NOSTATE, self.node.target_provision_state) self.assertEqual(states.POWER_ON, self.node.power_state) - self.assertIn('root_uuid', self.node.driver_internal_info) + self.assertIn('root_uuid_or_disk_id', self.node.driver_internal_info) self.assertIsNone(self.node.last_error) self.assertFalse(os.path.exists(token_path)) mock_image_cache.assert_called_once_with() @@ -754,8 +848,70 @@ class PXEDriverTestCase(db_base.DbTestCase): mock_clean_pxe.assert_called_once_with(mock.ANY) self.assertFalse(mock_switch_config.called) else: - mock_switch_config.assert_called_once_with( - pxe_config_path, root_uuid, boot_mode) + mock_switch_config.assert_called_once_with(pxe_config_path, + root_uuid, + boot_mode, + is_whole_disk_image) + self.assertFalse(mock_node_boot_dev.called) + self.assertFalse(mock_clean_pxe.called) + + @mock.patch.object(pxe_utils, 'clean_up_pxe_config') + @mock.patch.object(manager_utils, 'node_set_boot_device') + @mock.patch.object(deploy_utils, 'notify_deploy_complete') + @mock.patch.object(deploy_utils, 'switch_pxe_config') + @mock.patch.object(iscsi_deploy, 'InstanceImageCache') + @mock.patch.object(deploy_utils, 'deploy_disk_image') + def _test_continue_deploy_whole_disk_image(self, is_localboot, + mock_deploy, + mock_image_cache, + mock_switch_config, + notify_mock, + mock_node_boot_dev, + mock_clean_pxe): + token_path = self._create_token_file() + + # set local boot + if is_localboot: + i_info = self.node.instance_info + i_info['capabilities'] = '{"boot_option": "local"}' + self.node.instance_info = i_info + + self.node.power_state = states.POWER_ON + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + boot_mode = None + is_whole_disk_image = True + disk_id = '0x12345678' + mock_deploy.return_value = disk_id + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.vendor._continue_deploy(task, address='123456', + iqn='aaa-bbb', + key='fake-56789') + + self.node.refresh() + self.assertEqual(states.ACTIVE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + self.assertEqual(states.POWER_ON, self.node.power_state) + self.assertIsNone(self.node.last_error) + self.assertFalse(os.path.exists(token_path)) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) + notify_mock.assert_called_once_with('123456') + if is_localboot: + mock_node_boot_dev.assert_called_once_with( + mock.ANY, boot_devices.DISK, persistent=True) + mock_clean_pxe.assert_called_once_with(mock.ANY) + self.assertFalse(mock_switch_config.called) + else: + mock_switch_config.assert_called_once_with(pxe_config_path, + disk_id, + boot_mode, + is_whole_disk_image) self.assertFalse(mock_node_boot_dev.called) self.assertFalse(mock_clean_pxe.called) @@ -765,6 +921,12 @@ class PXEDriverTestCase(db_base.DbTestCase): def test_continue_deploy_localboot(self): self._test_continue_deploy(True) + def test_continue_deploy_whole_disk_image(self): + self._test_continue_deploy_whole_disk_image(False) + + def test_continue_deploy_whole_disk_image_localboot(self): + self._test_continue_deploy_whole_disk_image(True) + def test_continue_deploy_invalid(self): self.node.power_state = states.POWER_ON self.node.provision_state = states.AVAILABLE @@ -821,10 +983,12 @@ class CleanUpTestCase(db_base.DbTestCase): mgr_utils.mock_the_extension_manager(driver="fake_pxe") instance_info = INST_INFO_DICT instance_info['deploy_key'] = 'fake-56789' - self.node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=instance_info, - driver_info=DRV_INFO_DICT) + self.node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) def test_clean_up(self, mock_image_info, mock_cache, mock_pxe_clean, mock_iscsi_clean, mock_unlink): @@ -866,10 +1030,12 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase): mgr_utils.mock_the_extension_manager(driver="fake_pxe") instance_info = INST_INFO_DICT instance_info['deploy_key'] = 'fake-56789' - self.node = obj_utils.create_test_node(self.context, - driver='fake_pxe', - instance_info=instance_info, - driver_info=DRV_INFO_DICT) + self.node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.port = obj_utils.create_test_port(self.context, node_id=self.node.id) @@ -945,9 +1111,12 @@ class TestAgentVendorPassthru(db_base.DbTestCase): mgr_utils.mock_the_extension_manager() self.driver = driver_factory.get_driver("fake") self.driver.vendor = pxe.VendorPassthru() - self.node = obj_utils.create_test_node(self.context, driver='fake', - instance_info=INST_INFO_DICT, - driver_info=DRV_INFO_DICT) + self.node = obj_utils.create_test_node( + self.context, driver='fake', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) self.node.driver_internal_info['agent_url'] = 'http://1.2.3.4:1234' self.task = mock.Mock(spec=task_manager.TaskManager) self.task.shared = False @@ -966,7 +1135,6 @@ class TestAgentVendorPassthru(db_base.DbTestCase): reboot_and_finish_deploy_mock): do_agent_iscsi_deploy_mock.return_value = 'some-root-uuid' - self.driver.vendor.continue_deploy(self.task) destroy_token_file_mock.assert_called_once_with(self.node) do_agent_iscsi_deploy_mock.assert_called_once_with( @@ -974,7 +1142,7 @@ class TestAgentVendorPassthru(db_base.DbTestCase): tftp_config = '/tftpboot/%s/config' % self.node.uuid switch_pxe_config_mock.assert_called_once_with(tftp_config, 'some-root-uuid', - None) + None, False) reboot_and_finish_deploy_mock.assert_called_once_with(self.task) @mock.patch.object(agent_base_vendor.BaseAgentVendor, diff --git a/ironic/tests/test_images.py b/ironic/tests/test_images.py index 60b706dea0..547ed20244 100644 --- a/ironic/tests/test_images.py +++ b/ironic/tests/test_images.py @@ -25,6 +25,7 @@ from oslo_config import cfg import six.moves.builtins as __builtin__ from ironic.common import exception +from ironic.common.glance_service import service_utils as glance_utils from ironic.common import image_service from ironic.common import images from ironic.common import utils @@ -214,6 +215,68 @@ class IronicImagesTestCase(base.TestCase): qemu_img_info_mock.assert_called_once_with('path') self.assertEqual(1, size) + @mock.patch.object(images, 'get_image_properties') + @mock.patch.object(glance_utils, 'is_glance_image') + def test_is_whole_disk_image_no_img_src(self, mock_igi, mock_gip): + instance_info = {'image_source': ''} + iwdi = images.is_whole_disk_image('context', instance_info) + self.assertIsNone(iwdi) + self.assertFalse(mock_igi.called) + self.assertFalse(mock_gip.called) + + @mock.patch.object(images, 'get_image_properties') + @mock.patch.object(glance_utils, 'is_glance_image') + def test_is_whole_disk_image_partition_image(self, mock_igi, mock_gip): + mock_igi.return_value = True + mock_gip.return_value = {'kernel_id': 'kernel', + 'ramdisk_id': 'ramdisk'} + instance_info = {'image_source': 'glance://partition_image'} + image_source = instance_info['image_source'] + is_whole_disk_image = images.is_whole_disk_image('context', + instance_info) + self.assertFalse(is_whole_disk_image) + mock_igi.assert_called_once_with(image_source) + mock_gip.assert_called_once_with('context', image_source) + + @mock.patch.object(images, 'get_image_properties') + @mock.patch.object(glance_utils, 'is_glance_image') + def test_is_whole_disk_image_whole_disk_image(self, mock_igi, mock_gip): + mock_igi.return_value = True + mock_gip.return_value = {} + instance_info = {'image_source': 'glance://whole_disk_image'} + image_source = instance_info['image_source'] + is_whole_disk_image = images.is_whole_disk_image('context', + instance_info) + self.assertTrue(is_whole_disk_image) + mock_igi.assert_called_once_with(image_source) + mock_gip.assert_called_once_with('context', image_source) + + @mock.patch.object(images, 'get_image_properties') + @mock.patch.object(glance_utils, 'is_glance_image') + def test_is_whole_disk_image_partition_non_glance(self, mock_igi, + mock_gip): + mock_igi.return_value = False + instance_info = {'image_source': 'partition_image', + 'kernel': 'kernel', + 'ramdisk': 'ramdisk'} + is_whole_disk_image = images.is_whole_disk_image('context', + instance_info) + self.assertFalse(is_whole_disk_image) + self.assertFalse(mock_gip.called) + mock_igi.assert_called_once_with(instance_info['image_source']) + + @mock.patch.object(images, 'get_image_properties') + @mock.patch.object(glance_utils, 'is_glance_image') + def test_is_whole_disk_image_whole_disk_non_glance(self, mock_igi, + mock_gip): + mock_igi.return_value = False + instance_info = {'image_source': 'whole_disk_image'} + is_whole_disk_image = images.is_whole_disk_image('context', + instance_info) + self.assertTrue(is_whole_disk_image) + self.assertFalse(mock_gip.called) + mock_igi.assert_called_once_with(instance_info['image_source']) + class FsImageTestCase(base.TestCase):