diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 68cd5d6f76..fb9b4dd631 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -1009,6 +1009,34 @@ #sensor_method=ipmitool +# +# Options defined in ironic.drivers.modules.irmc.deploy +# + +# Ironic conductor node's "NFS" or "CIFS" root path (string +# value) +#remote_image_share_root=/remote_image_share_root + +# IP of remote image server (string value) +#remote_image_server= + +# Share type of virtual media, either "NFS" or "CIFS" (string +# value) +#remote_image_share_type=CIFS + +# share name of remote_image_server (string value) +#remote_image_share_name=share + +# User name of remote_image_server (string value) +#remote_image_user_name= + +# Password of remote_image_user_name (string value) +#remote_image_user_password= + +# Domain name of remote_image_user_name (string value) +#remote_image_user_domain= + + [keystone] # @@ -1340,7 +1368,7 @@ #rpc_conn_pool_size=30 # Qpid broker hostname. (string value) -#qpid_hostname=localhost +#qpid_hostname=ironic # Qpid broker port. (integer value) #qpid_port=5672 @@ -1419,7 +1447,7 @@ # The RabbitMQ broker address where a single node is used. # (string value) -#rabbit_host=localhost +#rabbit_host=ironic # The RabbitMQ broker port where a single node is used. # (integer value) diff --git a/ironic/common/exception.py b/ironic/common/exception.py index f4cfc4d83c..ea05956486 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -553,6 +553,10 @@ class IRMCOperationError(IronicException): message = _('iRMC %(operation)s failed. Reason: %(error)s') +class IRMCSharedFileSystemNotMounted(IronicException): + message = _("iRMC shared file system '%(share)s' is not mounted.") + + class VirtualBoxOperationFailed(IronicException): message = _("VirtualBox operation '%(operation)s' failed. " "Error: %(error)s") diff --git a/ironic/common/glance_service/service_utils.py b/ironic/common/glance_service/service_utils.py index 887db11c86..5b8ab57870 100644 --- a/ironic/common/glance_service/service_utils.py +++ b/ironic/common/glance_service/service_utils.py @@ -27,7 +27,7 @@ import six import six.moves.urllib.parse as urlparse from ironic.common import exception - +from ironic.common import image_service CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -245,3 +245,18 @@ def is_glance_image(image_href): return False return (image_href.startswith('glance://') or uuidutils.is_uuid_like(image_href)) + + +def is_image_href_ordinary_file_name(image_href): + """judge if image_href is a ordinary file name. + + This method judges if image_href is a ordinary file name or not, + which is a file supposed to be stored in share file system. + The ordinary file name is neither glance image href + nor image service href. + + :returns: True if image_href is ordinary file name, False otherwise. + """ + return not (is_glance_image(image_href) or + urlparse.urlparse(image_href).scheme.lower() in + image_service.protocol_mapping) diff --git a/ironic/drivers/irmc.py b/ironic/drivers/irmc.py new file mode 100755 index 0000000000..012eedb7ba --- /dev/null +++ b/ironic/drivers/irmc.py @@ -0,0 +1,73 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +iRMC Driver for managing FUJITSU PRIMERGY BX S4 or RX S8 generation +of FUJITSU PRIMERGY servers, and above servers. +""" + +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.drivers import base +from ironic.drivers.modules import agent +from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import deploy +from ironic.drivers.modules.irmc import management +from ironic.drivers.modules.irmc import power + + +class IRMCVirtualMediaIscsiDriver(base.BaseDriver): + """iRMC Driver using SCCI. + + This driver implements the `core` functionality using + :class:ironic.drivers.modules.irmc.power.IRMCPower for power management. + and + :class:ironic.drivers.modules.irmc.deploy.IRMCVirtualMediaIscsiDeploy for + deploy. + """ + + def __init__(self): + if not importutils.try_import('scciclient.irmc.scci'): + raise exception.DriverLoadError( + driver=self.__class__.__name__, + reason=_("Unable to import python-scciclient library")) + + self.power = power.IRMCPower() + self.deploy = deploy.IRMCVirtualMediaIscsiDeploy() + self.console = ipmitool.IPMIShellinaboxConsole() + self.management = management.IRMCManagement() + self.vendor = deploy.VendorPassthru() + + +class IRMCVirtualMediaAgentDriver(base.BaseDriver): + """iRMC Driver using SCCI. + + This driver implements the `core` functionality using + :class:ironic.drivers.modules.irmc.power.IRMCPower for power management + and + :class:ironic.drivers.modules.irmc.deploy.IRMCVirtualMediaAgentDriver for + deploy. + """ + + def __init__(self): + if not importutils.try_import('scciclient.irmc.scci'): + raise exception.DriverLoadError( + driver=self.__class__.__name__, + reason=_("Unable to import python-scciclient library")) + + self.power = power.IRMCPower() + self.deploy = deploy.IRMCVirtualMediaAgentDeploy() + self.console = ipmitool.IPMIShellinaboxConsole() + self.management = management.IRMCManagement() + self.vendor = agent.AgentVendorInterface() diff --git a/ironic/drivers/modules/irmc/deploy.py b/ironic/drivers/modules/irmc/deploy.py new file mode 100644 index 0000000000..5370d32d8c --- /dev/null +++ b/ironic/drivers/modules/irmc/deploy.py @@ -0,0 +1,797 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +iRMC Deploy Driver +""" + +import os +import tempfile + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _ +from ironic.common.i18n import _LE +from ironic.common.i18n import _LI +from ironic.common import images +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers import base +from ironic.drivers.modules import agent +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules import iscsi_deploy + +scci = importutils.try_import('scciclient.irmc.scci') + +CONF = cfg.CONF + +try: + if CONF.debug: + scci.DEBUG = True +except Exception: + pass + +opts = [ + cfg.StrOpt('remote_image_share_root', + default='/remote_image_share_root', + help='Ironic conductor node\'s "NFS" or "CIFS" root path'), + cfg.StrOpt('remote_image_server', + help='IP of remote image server'), + cfg.StrOpt('remote_image_share_type', + default='CIFS', + help='Share type of virtual media, either "NFS" or "CIFS"'), + cfg.StrOpt('remote_image_share_name', + default='share', + help='share name of remote_image_server'), + cfg.StrOpt('remote_image_user_name', + help='User name of remote_image_server'), + cfg.StrOpt('remote_image_user_password', + help='Password of remote_image_user_name'), + cfg.StrOpt('remote_image_user_domain', + default='', + help='Domain name of remote_image_user_name'), +] + +CONF.register_opts(opts, group='irmc') + +LOG = logging.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'irmc_deploy_iso': _("Deployment ISO image file name. " + "Required."), +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES + +CONF.import_opt('pxe_append_params', 'ironic.drivers.modules.iscsi_deploy', + group='pxe') + + +def _parse_config_option(): + """Parse config file options. + + This method checks config file options validity. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + error_msgs = [] + if not os.path.isdir(CONF.irmc.remote_image_share_root): + error_msgs.append( + _("Value '%s' for remote_image_share_root isn't a directory " + "or doesn't exist.") % + CONF.irmc.remote_image_share_root) + if CONF.irmc.remote_image_share_type.lower() not in ('nfs', 'cifs'): + error_msgs.append( + _("Value '%s' for remote_image_share_type is not supported " + "value either 'NFS' or 'CIFS'.") % + CONF.irmc.remote_image_share_type) + if error_msgs: + msg = (_("The following errors were encountered while parsing " + "config file:%s") % error_msgs) + raise exception.InvalidParameterValue(msg) + + +def _parse_driver_info(node): + """Gets the driver specific Node deployment info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required or optional information properly + for this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: the driver_info values of the node. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + d_info = node.driver_info + deploy_info = {} + + deploy_info['irmc_deploy_iso'] = d_info.get('irmc_deploy_iso') + error_msg = _("Error validating iRMC virtual media deploy. Some parameters" + " were missing in node's driver_info") + deploy_utils.check_for_missing_params(deploy_info, error_msg) + + if service_utils.is_image_href_ordinary_file_name( + deploy_info['irmc_deploy_iso']): + deploy_iso = os.path.join(CONF.irmc.remote_image_share_root, + deploy_info['irmc_deploy_iso']) + if not os.path.isfile(deploy_iso): + msg = (_("Deploy ISO file, %(deploy_iso)s, " + "not found for node: %(node)s.") % + {'deploy_iso': deploy_iso, 'node': node.uuid}) + raise exception.InvalidParameterValue(msg) + + return deploy_info + + +def _parse_instance_info(node): + """Gets the instance specific Node deployment info. + + This method validates whether the 'instance_info' property of the + supplied node contains the required or optional information properly + for this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: the instance_info values of the node. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + i_info = node.instance_info + deploy_info = {} + + if i_info.get('irmc_boot_iso'): + deploy_info['irmc_boot_iso'] = i_info['irmc_boot_iso'] + + if service_utils.is_image_href_ordinary_file_name( + deploy_info['irmc_boot_iso']): + boot_iso = os.path.join(CONF.irmc.remote_image_share_root, + deploy_info['irmc_boot_iso']) + + if not os.path.isfile(boot_iso): + msg = (_("Boot ISO file, %(boot_iso)s, " + "not found for node: %(node)s.") % + {'boot_iso': boot_iso, 'node': node.uuid}) + raise exception.InvalidParameterValue(msg) + + return deploy_info + + +def _parse_deploy_info(node): + """Gets the instance and driver specific Node deployment info. + + This method validates whether the 'instance_info' and 'driver_info' + property of the supplied node contains the required information for + this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: a dict with the instance_info and driver_info values. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + deploy_info = {} + deploy_info.update(iscsi_deploy.parse_instance_info(node)) + deploy_info.update(_parse_driver_info(node)) + deploy_info.update(_parse_instance_info(node)) + + return deploy_info + + +def _reboot_into_deploy_iso(task, ramdisk_options): + """Reboots the node into a given deploy ISO. + + This method attaches the given deploy ISO as virtual media, prepares the + arguments for ramdisk in virtual media floppy, and then reboots the node. + + :param task: a TaskManager instance containing the node to act on. + :param ramdisk_options: the options to be passed to the ramdisk in virtual + media floppy. + :raises: ImageRefValidationFailed if no image service can handle specified + href. + :raises: ImageCreationFailed, if it failed while creating the floppy image. + :raises: IRMCOperationError, if some operation on iRMC failed. + :raises: InvalidParameterValue if the validation of the + PowerInterface or ManagementInterface fails. + """ + d_info = task.node.driver_info + + deploy_iso_href = d_info['irmc_deploy_iso'] + if service_utils.is_image_href_ordinary_file_name(deploy_iso_href): + deploy_iso_file = deploy_iso_href + else: + deploy_iso_file = _get_deploy_iso_name(task.node) + deploy_iso_fullpathname = os.path.join( + CONF.irmc.remote_image_share_root, deploy_iso_file) + images.fetch(task.context, deploy_iso_href, deploy_iso_fullpathname) + + setup_vmedia_for_boot(task, deploy_iso_file, ramdisk_options) + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + manager_utils.node_power_action(task, states.REBOOT) + + +def _get_deploy_iso_name(node): + """Returns the deploy ISO file name for a given node. + + :param node: the node for which ISO file name is to be provided. + """ + return "deploy-%s.iso" % node.uuid + + +def _get_boot_iso_name(node): + """Returns the boot ISO file name for a given node. + + :param node: the node for which ISO file name is to be provided. + """ + return "boot-%s.iso" % node.uuid + + +def _prepare_boot_iso(task, root_uuid): + """Prepare a boot ISO to boot the node. + + :param task: a TaskManager instance containing the node to act on. + :param root_uuid: the uuid of the root partition. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + :raises: ImageCreationFailed, if creating boot ISO + for BIOS boot_mode failed. + """ + deploy_info = _parse_deploy_info(task.node) + driver_internal_info = task.node.driver_internal_info + + # fetch boot iso + if deploy_info.get('irmc_boot_iso'): + boot_iso_href = deploy_info['irmc_boot_iso'] + if service_utils.is_image_href_ordinary_file_name(boot_iso_href): + driver_internal_info['irmc_boot_iso'] = boot_iso_href + else: + boot_iso_filename = _get_boot_iso_name(task.node) + boot_iso_fullpathname = os.path.join( + CONF.irmc.remote_image_share_root, boot_iso_filename) + images.fetch(task.context, boot_iso_href, boot_iso_fullpathname) + + driver_internal_info['irmc_boot_iso'] = boot_iso_filename + + # create boot iso + else: + image_href = deploy_info['image_source'] + image_props = ['kernel_id', 'ramdisk_id'] + image_properties = images.get_image_properties( + task.context, image_href, image_props) + kernel_href = (task.node.instance_info.get('kernel') or + image_properties['kernel_id']) + ramdisk_href = (task.node.instance_info.get('ramdisk') or + image_properties['ramdisk_id']) + + deploy_iso_filename = _get_deploy_iso_name(task.node) + deploy_iso = ('file://' + os.path.join( + CONF.irmc.remote_image_share_root, deploy_iso_filename)) + boot_mode = deploy_utils.get_boot_mode_for_deploy(task.node) + kernel_params = CONF.pxe.pxe_append_params + + boot_iso_filename = _get_boot_iso_name(task.node) + boot_iso_fullpathname = os.path.join( + CONF.irmc.remote_image_share_root, boot_iso_filename) + + images.create_boot_iso(task.context, boot_iso_fullpathname, + kernel_href, ramdisk_href, + deploy_iso, root_uuid, + kernel_params, boot_mode) + + driver_internal_info['irmc_boot_iso'] = boot_iso_filename + + # save driver_internal_info['irmc_boot_iso'] + task.node.driver_internal_info = driver_internal_info + task.node.save() + + +def _get_floppy_image_name(node): + """Returns the floppy image name for a given node. + + :param node: the node for which image name is to be provided. + """ + return "image-%s.img" % node.uuid + + +def _prepare_floppy_image(task, params): + """Prepares the floppy image for passing the parameters. + + This method prepares a temporary vfat filesystem image, which + contains the parameters to be passed to the ramdisk. + Then it uploads the file NFS or CIFS server. + + :param task: a TaskManager instance containing the node to act on. + :param params: a dictionary containing 'parameter name'->'value' mapping + to be passed to the deploy ramdisk via the floppy image. + :returns: floppy image filename + :raises: ImageCreationFailed, if it failed while creating the floppy image. + :raises: IRMCOperationError, if copying floppy image file failed. + """ + floppy_filename = _get_floppy_image_name(task.node) + floppy_fullpathname = os.path.join( + CONF.irmc.remote_image_share_root, floppy_filename) + + with tempfile.NamedTemporaryFile() as vfat_image_tmpfile_obj: + images.create_vfat_image(vfat_image_tmpfile_obj.name, + parameters=params) + try: + utils.execute('cp', vfat_image_tmpfile_obj.name, + floppy_fullpathname, check_exit_code=[0]) + except Exception as e: + operation = _("Copying floppy image file") + raise exception.IRMCOperationError( + operation=operation, error=e) + + return floppy_filename + + +def setup_vmedia_for_boot(task, bootable_iso_filename, parameters=None): + """Sets up the node to boot from the boot ISO image. + + This method attaches a boot_iso on the node and passes + the required parameters to it via a virtual floppy image. + + :param task: a TaskManager instance containing the node to act on. + :param bootable_iso_filename: a bootable ISO image to attach to. + The iso file should be present in NFS/CIFS server. + :param parameters: the parameters to pass in a virtual floppy image + in a dictionary. This is optional. + :raises: ImageCreationFailed, if it failed while creating a floppy image. + :raises: IRMCOperationError, if attaching a virtual media failed. + """ + LOG.info(_LI("Setting up node %s to boot from virtual media"), + task.node.uuid) + + if parameters: + floppy_image_filename = _prepare_floppy_image(task, parameters) + _attach_virtual_fd(task.node, floppy_image_filename) + + _attach_virtual_cd(task.node, bootable_iso_filename) + + +def _cleanup_vmedia_boot(task): + """Cleans a node after a virtual media boot. + + This method cleans up a node after a virtual media boot. + It deletes a floppy image if it exists in NFS/CIFS server. + It also ejects both the virtual media cdrom and the virtual media floppy. + + :param task: a TaskManager instance containing the node to act on. + :raises: IRMCOperationError if ejecting virtual media failed. + """ + LOG.debug("Cleaning up node %s after virtual media boot", task.node.uuid) + + node = task.node + _detach_virtual_cd(node) + _detach_virtual_fd(node) + + _remove_share_file(_get_floppy_image_name(node)) + _remove_share_file(_get_deploy_iso_name(node)) + + +def _remove_share_file(share_filename): + """remove a file in the share file system. + + :param share_filename: a file name to be removed. + """ + share_fullpathname = os.path.join( + CONF.irmc.remote_image_share_name, share_filename) + utils.unlink_without_raise(share_fullpathname) + + +def _attach_virtual_cd(node, bootable_iso_filename): + """Attaches the given url as virtual media on the node. + + :param node: an ironic node object. + :param bootable_iso_filename: a bootable ISO image to attach to. + The iso file should be present in NFS/CIFS server. + :raises: IRMCOperationError if attaching virtual media failed. + """ + try: + irmc_client = irmc_common.get_irmc_client(node) + + cd_set_params = scci.get_virtual_cd_set_params_cmd( + CONF.irmc.remote_image_server, + CONF.irmc.remote_image_user_domain, + scci.get_share_type(CONF.irmc.remote_image_share_type), + CONF.irmc.remote_image_share_name, + bootable_iso_filename, + CONF.irmc.remote_image_user_name, + CONF.irmc.remote_image_user_password) + + irmc_client(cd_set_params, async=False) + irmc_client(scci.MOUNT_CD, async=False) + + except scci.SCCIClientError as irmc_exception: + LOG.exception(_LE("Error while inserting virtual cdrom " + "from node %(uuid)s. Error: %(error)s"), + {'uuid': node.uuid, 'error': irmc_exception}) + operation = _("Inserting virtual cdrom") + raise exception.IRMCOperationError(operation=operation, + error=irmc_exception) + + LOG.info(_LI("Attached virtual cdrom successfully" + " for node %s"), node.uuid) + + +def _detach_virtual_cd(node): + """Detaches virtual cdrom on the node. + + :param node: an ironic node object. + :raises: IRMCOperationError if eject virtual cdrom failed. + """ + try: + irmc_client = irmc_common.get_irmc_client(node) + + irmc_client(scci.UNMOUNT_CD) + + except scci.SCCIClientError as irmc_exception: + LOG.exception(_LE("Error while ejecting virtual cdrom " + "from node %(uuid)s. Error: %(error)s"), + {'uuid': node.uuid, 'error': irmc_exception}) + operation = _("Ejecting virtual cdrom") + raise exception.IRMCOperationError(operation=operation, + error=irmc_exception) + + LOG.info(_LI("Detached virtual cdrom successfully" + " for node %s"), node.uuid) + + +def _attach_virtual_fd(node, floppy_image_filename): + """Attaches virtual floppy on the node. + + :param node: an ironic node object. + :raises: IRMCOperationError if insert virtual floppy failed. + """ + try: + irmc_client = irmc_common.get_irmc_client(node) + + fd_set_params = scci.get_virtual_fd_set_params_cmd( + CONF.irmc.remote_image_server, + CONF.irmc.remote_image_user_domain, + scci.get_share_type(CONF.irmc.remote_image_share_type), + CONF.irmc.remote_image_share_name, + floppy_image_filename, + CONF.irmc.remote_image_user_name, + CONF.irmc.remote_image_user_password) + + irmc_client(fd_set_params, async=False) + irmc_client(scci.MOUNT_FD, async=False) + + except scci.SCCIClientError as irmc_exception: + LOG.exception(_LE("Error while inserting virtual floppy " + "from node %(uuid)s. Error: %(error)s"), + {'uuid': node.uuid, 'error': irmc_exception}) + operation = _("Inserting virtual floppy") + raise exception.IRMCOperationError(operation=operation, + error=irmc_exception) + + LOG.info(_LI("Attached virtual floppy successfully" + " for node %s"), node.uuid) + + +def _detach_virtual_fd(node): + """Detaches virtual media on the node. + + :param node: an ironic node object. + :raises: IRMCOperationError if eject virtual media failed. + """ + try: + irmc_client = irmc_common.get_irmc_client(node) + + irmc_client(scci.UNMOUNT_FD) + + except scci.SCCIClientError as irmc_exception: + LOG.exception(_LE("Error while ejecting virtual floppy " + "from node %(uuid)s. Error: %(error)s"), + {'uuid': node.uuid, 'error': irmc_exception}) + operation = _("Ejecting virtual floppy") + raise exception.IRMCOperationError(operation=operation, + error=irmc_exception) + + LOG.info(_LI("Detached virtual floppy successfully" + " for node %s"), node.uuid) + + +def _check_share_fs_mounted(): + """Check if Share File System (NFS or CIFS) is mounted. + + :raises: InvalidParameterValue, if config option has invalid value. + :raises: IRMCSharedFileSystemNotMounted, if shared file system is + not mounted. + """ + _parse_config_option() + if not os.path.ismount(CONF.irmc.remote_image_share_root): + raise exception.IRMCSharedFileSystemNotMounted( + share=CONF.irmc.remote_image_share_root) + + +class IRMCVirtualMediaIscsiDeploy(base.DeployInterface): + """Interface for iSCSI deploy-related actions.""" + + def __init__(self): + """Constructor of IRMCVirtualMediaIscsiDeploy. + + :raises: IRMCSharedFileSystemNotMounted, if shared file system is + not mounted. + :raises: InvalidParameterValue, if config option has invalid value. + """ + _check_share_fs_mounted() + super(IRMCVirtualMediaIscsiDeploy, self).__init__() + + def get_properties(self): + return COMMON_PROPERTIES + + def validate(self, task): + """Validate the deployment information for the task's node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue, if config option has invalid value. + :raises: IRMCSharedFileSystemNotMounted, if shared file system is + not mounted. + :raises: InvalidParameterValue, if some information is invalid. + :raises: MissingParameterValue if 'kernel_id' and 'ramdisk_id' are + missing in the Glance image, or if 'kernel' and 'ramdisk' are + missing in the Non Glance image. + """ + _check_share_fs_mounted() + iscsi_deploy.validate(task) + + d_info = _parse_deploy_info(task.node) + if 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) + deploy_utils.validate_capabilities(task.node) + + @task_manager.require_exclusive_lock + def deploy(self, task): + """Start deployment of the task's node. + + Fetches the instance image, prepares the options for the deployment + ramdisk, sets the node to boot from virtual media cdrom, and reboots + the given node. + + :param task: a TaskManager instance containing the node to act on. + :returns: deploy state DEPLOYWAIT. + :raises: InstanceDeployFailure, if image size if greater than root + partition. + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: IRMCOperationError, if some operation on iRMC fails. + """ + node = task.node + manager_utils.node_power_action(task, states.POWER_OFF) + + iscsi_deploy.cache_instance_image(task.context, node) + iscsi_deploy.check_image_size(task) + + deploy_ramdisk_opts = iscsi_deploy.build_deploy_ramdisk_options(node) + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + deploy_ramdisk_opts['BOOTIF'] = deploy_nic_mac + + _reboot_into_deploy_iso(task, deploy_ramdisk_opts) + + return states.DEPLOYWAIT + + @task_manager.require_exclusive_lock + def tear_down(self, task): + """Tear down a previous deployment on the task's node. + + Power off the node. All actual clean-up is done in the clean_up() + method which should be called separately. + + :param task: a TaskManager instance containing the node to act on. + :returns: deploy state DELETED. + """ + _remove_share_file(_get_boot_iso_name(task.node)) + driver_internal_info = task.node.driver_internal_info + driver_internal_info.pop('irmc_boot_iso', None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + manager_utils.node_power_action(task, states.POWER_OFF) + return states.DELETED + + def prepare(self, task): + """Prepare the deployment environment for the task's node. + + If preparation of the deployment environment ahead of time is possible, + this method should be implemented by the driver. + + If implemented, this method must be idempotent. It may be called + multiple times for the same node on the same conductor, and it may be + called by multiple conductors in parallel. Therefore, it must not + require an exclusive lock. + + This method is called before `deploy`. + + :param task: a TaskManager instance containing the node to act on. + """ + pass + + def clean_up(self, task): + """Clean up the deployment environment for the task's node. + + Unlinks instance image and triggers image cache cleanup. + + :param task: a TaskManager instance containing the node to act on. + """ + _cleanup_vmedia_boot(task) + iscsi_deploy.destroy_images(task.node.uuid) + + def take_over(self, task): + pass + + +class IRMCVirtualMediaAgentDeploy(base.DeployInterface): + + def __init__(self): + """Constructor of IRMCVirtualMediaAgentDeploy. + + :raises: IRMCSharedFileSystemNotMounted, if shared file system is + not mounted. + :raises: InvalidParameterValue, if config option has invalid value. + """ + _check_share_fs_mounted() + super(IRMCVirtualMediaAgentDeploy, self).__init__() + + """Interface for Agent deploy-related actions.""" + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific Node deployment info. + + :param task: a TaskManager instance + :raises: IRMCSharedFileSystemNotMounted, if shared file system is + not mounted. + :raises: InvalidParameterValue, if config option has invalid value. + :raises: MissingParameterValue if some parameters are missing. + """ + _check_share_fs_mounted() + _parse_driver_info(task.node) + deploy_utils.validate_capabilities(task.node) + + @task_manager.require_exclusive_lock + def deploy(self, task): + """Perform a deployment to a node. + + Prepares the options for the agent ramdisk and sets the node to boot + from virtual media cdrom. + + :param task: a TaskManager instance. + :returns: states.DEPLOYWAIT + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: IRMCOperationError, if some operation on iRMC fails. + """ + deploy_ramdisk_opts = agent.build_agent_options(task.node) + _reboot_into_deploy_iso(task, deploy_ramdisk_opts) + + return states.DEPLOYWAIT + + @task_manager.require_exclusive_lock + def tear_down(self, task): + """Tear down a previous deployment on the task's node. + + :param task: a TaskManager instance. + :returns: states.DELETED + """ + manager_utils.node_power_action(task, states.POWER_OFF) + return states.DELETED + + def prepare(self, task): + """Prepare the deployment environment for this node. + + :param task: a TaskManager instance. + """ + node = task.node + node.instance_info = agent.build_instance_info_for_deploy(task) + node.save() + + def clean_up(self, task): + """Clean up the deployment environment for this node. + + Ejects the attached virtual media from the iRMC and also removes + the floppy image from the share file system, if it exists. + + :param task: a TaskManager instance. + """ + _cleanup_vmedia_boot(task) + + def take_over(self, task): + """Take over management of this node from a dead conductor. + + :param task: a TaskManager instance. + """ + pass + + +class VendorPassthru(base.VendorInterface): + """Vendor-specific interfaces for iRMC deploy drivers.""" + + def get_properties(self): + return COMMON_PROPERTIES + + def validate(self, task, method, **kwargs): + """Validate vendor-specific actions. + + Checks if a valid vendor passthru method was passed and validates + the parameters for the vendor passthru method. + + :param task: a TaskManager instance containing the node to act on. + :param method: method to be validated. + :param kwargs: kwargs containing the vendor passthru method's + parameters. + :raises: MissingParameterValue, if some required parameters were not + passed. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + iscsi_deploy.get_deploy_info(task.node, **kwargs) + + @base.passthru(['POST']) + @task_manager.require_exclusive_lock + def pass_deploy_info(self, task, **kwargs): + """Continues the iSCSI deployment from where ramdisk left off. + + Continues the iSCSI deployment from the conductor node, finds the + boot ISO to boot the node, and sets the node to boot from boot ISO. + + :param task: a TaskManager instance containing the node to act on. + :param kwargs: kwargs containing parameters for iSCSI deployment. + :raises: InvalidState + """ + node = task.node + task.process_event('resume') + + root_dict = iscsi_deploy.continue_deploy(task, **kwargs) + root_uuid = root_dict.get('root uuid') + + try: + _cleanup_vmedia_boot(task) + _prepare_boot_iso(task, root_uuid) + setup_vmedia_for_boot(task, + node.driver_internal_info['irmc_boot_iso']) + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + + address = kwargs.get('address') + deploy_utils.notify_ramdisk_to_proceed(address) + + LOG.info(_LI('Deployment to node %s done'), node.uuid) + + task.process_event('done') + except Exception as e: + LOG.exception(_LE('Deploy failed for instance %(instance)s. ' + 'Error: %(error)s'), + {'instance': node.instance_uuid, 'error': e}) + msg = _('Failed to continue iSCSI deployment.') + deploy_utils.set_failed_state(task, msg) diff --git a/ironic/drivers/modules/irmc/power.py b/ironic/drivers/modules/irmc/power.py index 4b4ecf90b9..de9cd8ae36 100644 --- a/ironic/drivers/modules/irmc/power.py +++ b/ironic/drivers/modules/irmc/power.py @@ -18,14 +18,17 @@ from oslo_config import cfg from oslo_log import log as logging from oslo_utils import importutils +from ironic.common import boot_devices from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common import states from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils from ironic.drivers import base from ironic.drivers.modules import ipmitool from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import deploy as irmc_deploy scci = importutils.try_import('scciclient.irmc.scci') @@ -39,6 +42,28 @@ if scci: states.REBOOT: scci.POWER_RESET} +def _attach_boot_iso_if_needed(task): + """Attaches boot ISO for a deployed node if it exists. + + This method checks the instance info of the bare metal node for a + boot ISO. If the instance info has a value of key 'irmc_boot_iso', + it indicates that 'boot_option' is 'netboot'. Threfore it attaches + the boot ISO on the bare metal node and then sets the node to boot from + virtual media cdrom. + + :param task: a TaskManager instance containing the node to act on. + :raises: IRMCOperationError if attaching virtual media failed. + :raises: InvalidParameterValue if the validation of the + ManagementInterface fails. + """ + d_info = task.node.driver_internal_info + node_state = task.node.provision_state + + if 'irmc_boot_iso' in d_info and node_state == states.ACTIVE: + irmc_deploy.setup_vmedia_for_boot(task, d_info['irmc_boot_iso']) + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + + def _set_power_state(task, target_state): """Turns the server power on/off or do a reboot. @@ -53,6 +78,9 @@ def _set_power_state(task, target_state): node = task.node irmc_client = irmc_common.get_irmc_client(node) + if target_state in (states.POWER_ON, states.REBOOT): + _attach_boot_iso_if_needed(task) + try: irmc_client(STATES_MAP[target_state]) diff --git a/ironic/tests/drivers/irmc/test_deploy.py b/ironic/tests/drivers/irmc/test_deploy.py new file mode 100644 index 0000000000..590e231228 --- /dev/null +++ b/ironic/tests/drivers/irmc/test_deploy.py @@ -0,0 +1,1148 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for iRMC Deploy Driver +""" + +import os +import tempfile + +import mock +from oslo_config import cfg +import six + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _ +from ironic.common import images +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import deploy as irmc_deploy +from ironic.drivers.modules import iscsi_deploy +from ironic.tests.conductor import utils as mgr_utils +from ironic.tests.db import base as db_base +from ironic.tests.db import utils as db_utils +from ironic.tests.objects import utils as obj_utils + + +if six.PY3: + import io + file = io.BytesIO + + +INFO_DICT = db_utils.get_test_irmc_info() +CONF = cfg.CONF + + +class IRMCDeployPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCDeployPrivateMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='iscsi_irmc') + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + CONF.irmc.remote_image_share_root = '/remote_image_share_root' + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + CONF.irmc.remote_image_user_domain = 'local' + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option(self, isdir_mock): + isdir_mock.return_value = True + + result = irmc_deploy._parse_config_option() + + isdir_mock.assert_called_once_with('/remote_image_share_root') + self.assertIsNone(result) + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option_non_existed_root(self, isdir_mock): + CONF.irmc.remote_image_share_root = '/non_existed_root' + isdir_mock.return_value = False + + self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_config_option) + isdir_mock.assert_called_once_with('/non_existed_root') + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option_wrong_share_type(self, isdir_mock): + CONF.irmc.remote_image_share_type = 'NTFS' + isdir_mock.return_value = True + + self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_config_option) + isdir_mock.assert_called_once_with('/remote_image_share_root') + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_driver_info_in_share(self, isfile_mock): + """With required 'irmc_deploy_iso' in share.""" + isfile_mock.return_value = True + self.node.driver_info['irmc_deploy_iso'] = 'deploy.iso' + driver_info_expected = {'irmc_deploy_iso': 'deploy.iso'} + + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + isfile_mock.assert_called_once_with( + '/remote_image_share_root/deploy.iso') + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__parse_driver_info_not_in_share( + self, is_image_href_ordinary_file_name_mock): + """With required 'irmc_deploy_iso' not in share.""" + self.node.driver_info[ + 'irmc_deploy_iso'] = 'bc784057-a140-4130-add3-ef890457e6b3' + driver_info_expected = {'irmc_deploy_iso': + 'bc784057-a140-4130-add3-ef890457e6b3'} + is_image_href_ordinary_file_name_mock.return_value = False + + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_driver_info_with_deploy_iso_invalid(self, isfile_mock): + """With required 'irmc_deploy_iso' non existed.""" + isfile_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_deploy_iso'] = 'deploy.iso' + error_msg = (_("Deploy ISO file, %(deploy_iso)s, " + "not found for node: %(node)s.") % + {'deploy_iso': '/remote_image_share_root/deploy.iso', + 'node': task.node.uuid}) + + e = self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_driver_info, + task.node) + self.assertEqual(error_msg, str(e)) + + def test__parse_driver_info_with_deploy_iso_missing(self): + """With required 'irmc_deploy_iso' empty.""" + self.node.driver_info['irmc_deploy_iso'] = None + + error_msg = ("Error validating iRMC virtual media deploy. Some" + " parameters were missing in node's driver_info." + " Missing are: ['irmc_deploy_iso']") + e = self.assertRaises(exception.MissingParameterValue, + irmc_deploy._parse_driver_info, + self.node) + self.assertEqual(error_msg, str(e)) + + def test__parse_instance_info_with_boot_iso_file_name_ok(self): + """With optional 'irmc_boot_iso' file name.""" + CONF.irmc.remote_image_share_root = '/etc' + self.node.instance_info['irmc_boot_iso'] = 'hosts' + instance_info_expected = {'irmc_boot_iso': 'hosts'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_without_boot_iso_ok(self): + """With optional no 'irmc_boot_iso' file name.""" + CONF.irmc.remote_image_share_root = '/etc' + + self.node.instance_info['irmc_boot_iso'] = None + instance_info_expected = {} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_uuid_ok(self): + """With optional 'irmc_boot_iso' glance uuid.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'bc784057-a140-4130-add3-ef890457e6b3' + instance_info_expected = {'irmc_boot_iso': + 'bc784057-a140-4130-add3-ef890457e6b3'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_glance_ok(self): + """With optional 'irmc_boot_iso' glance url.""" + self.node.instance_info['irmc_boot_iso'] = ( + 'glance://bc784057-a140-4130-add3-ef890457e6b3') + instance_info_expected = { + 'irmc_boot_iso': 'glance://bc784057-a140-4130-add3-ef890457e6b3', + } + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_http_ok(self): + """With optional 'irmc_boot_iso' http url.""" + self.node.driver_info[ + 'irmc_deploy_iso'] = 'http://irmc_boot_iso' + driver_info_expected = {'irmc_deploy_iso': 'http://irmc_boot_iso'} + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + self.assertEqual(driver_info_expected, driver_info_actual) + + def test__parse_instance_info_with_boot_iso_https_ok(self): + """With optional 'irmc_boot_iso' https url.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'https://irmc_boot_iso' + instance_info_expected = {'irmc_boot_iso': 'https://irmc_boot_iso'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_file_url_ok(self): + """With optional 'irmc_boot_iso' file url.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'file://irmc_boot_iso' + instance_info_expected = {'irmc_boot_iso': 'file://irmc_boot_iso'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_instance_info_with_boot_iso_invalid(self, isfile_mock): + CONF.irmc.remote_image_share_root = '/etc' + isfile_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.instance_info['irmc_boot_iso'] = 'hosts~non~existed' + + error_msg = (_("Boot ISO file, %(boot_iso)s, " + "not found for node: %(node)s.") % + {'boot_iso': '/etc/hosts~non~existed', + 'node': task.node.uuid}) + + e = self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_instance_info, + task.node) + self.assertEqual(error_msg, str(e)) + + @mock.patch.object(iscsi_deploy, 'parse_instance_info', spec_set=True, + autospec=True) + def test__parse_deploy_info_ok(self, instance_info_mock): + CONF.irmc.remote_image_share_root = '/etc' + instance_info_mock.return_value = {'a': 'b'} + driver_info_expected = {'a': 'b', + 'irmc_deploy_iso': 'hosts', + 'irmc_boot_iso': 'fstab'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_deploy_iso'] = 'hosts' + task.node.instance_info['irmc_boot_iso'] = 'fstab' + driver_info_actual = irmc_deploy._parse_deploy_info(task.node) + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + def test__reboot_into_deploy_iso_with_file(self, + fetch_mock, + setup_vmedia_mock, + set_boot_device_mock, + node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['irmc_deploy_iso'] = 'deploy_iso_filename' + ramdisk_opts = {'a': 'b'} + irmc_deploy._reboot_into_deploy_iso(task, ramdisk_opts) + + self.assertFalse(fetch_mock.called) + + setup_vmedia_mock.assert_called_once_with( + task, + 'deploy_iso_filename', + ramdisk_opts) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.CDROM) + node_power_action_mock.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__reboot_into_deploy_iso_with_image_service( + self, + is_image_href_ordinary_file_name_mock, + fetch_mock, + setup_vmedia_mock, + set_boot_device_mock, + node_power_action_mock): + CONF.irmc.remote_image_share_root = '/' + is_image_href_ordinary_file_name_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['irmc_deploy_iso'] = 'glance://deploy_iso' + ramdisk_opts = {'a': 'b'} + irmc_deploy._reboot_into_deploy_iso(task, ramdisk_opts) + + fetch_mock.assert_called_once_with( + task.context, + 'glance://deploy_iso', + "/deploy-%s.iso" % self.node.uuid) + + setup_vmedia_mock.assert_called_once_with( + task, + "deploy-%s.iso" % self.node.uuid, + ramdisk_opts) + set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + + def test__get_deploy_iso_name(self): + actual = irmc_deploy._get_deploy_iso_name(self.node) + expected = "deploy-%s.iso" % self.node.uuid + self.assertEqual(expected, actual) + + def test__get_boot_iso_name(self): + actual = irmc_deploy._get_boot_iso_name(self.node) + expected = "boot-%s.iso" % self.node.uuid + self.assertEqual(expected, actual) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__prepare_boot_iso_file(self, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + deploy_info_mock.return_value = {'irmc_boot_iso': 'irmc_boot.iso'} + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + deploy_info_mock.assert_called_once_with(task.node) + self.assertFalse(fetch_mock.called) + self.assertFalse(image_props_mock.called) + self.assertFalse(boot_mode_mock.called) + self.assertFalse(create_boot_iso_mock.called) + task.node.refresh() + self.assertEqual('irmc_boot.iso', + task.node.driver_internal_info['irmc_boot_iso']) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__prepare_boot_iso_fetch_ok(self, + is_image_href_ordinary_file_name_mock, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + + CONF.irmc.remote_image_share_root = '/' + image = '733d1c44-a2ea-414b-aca7-69decf20d810' + is_image_href_ordinary_file_name_mock.return_value = False + deploy_info_mock.return_value = {'irmc_boot_iso': image} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['irmc_boot_iso'] = image + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + deploy_info_mock.assert_called_once_with(task.node) + fetch_mock.assert_called_once_with( + task.context, + image, + "/boot-%s.iso" % self.node.uuid) + self.assertFalse(image_props_mock.called) + self.assertFalse(boot_mode_mock.called) + self.assertFalse(create_boot_iso_mock.called) + task.node.refresh() + self.assertEqual("boot-%s.iso" % self.node.uuid, + task.node.driver_internal_info['irmc_boot_iso']) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__prepare_boot_iso_create_ok(self, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + CONF.pxe.pxe_append_params = 'kernel-params' + + deploy_info_mock.return_value = {'image_source': 'image-uuid'} + image_props_mock.return_value = {'kernel_id': 'kernel_uuid', + 'ramdisk_id': 'ramdisk_uuid'} + + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + boot_mode_mock.return_value = 'uefi' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + self.assertFalse(fetch_mock.called) + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', ['kernel_id', 'ramdisk_id']) + create_boot_iso_mock.assert_called_once_with( + task.context, + '/remote_image_share_root/' + + "boot-%s.iso" % self.node.uuid, + 'kernel_uuid', 'ramdisk_uuid', + 'file:///remote_image_share_root/' + + "deploy-%s.iso" % self.node.uuid, + 'root-uuid', 'kernel-params', 'uefi') + task.node.refresh() + self.assertEqual("boot-%s.iso" % self.node.uuid, + task.node.driver_internal_info['irmc_boot_iso']) + + def test__get_floppy_image_name(self): + actual = irmc_deploy._get_floppy_image_name(self.node) + expected = "image-%s.img" % self.node.uuid + self.assertEqual(expected, actual) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image(self, + tempfile_mock, + create_vfat_image_mock, + execute_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock() + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + tempfile_mock.side_effect = iter([mock_image_file_handle]) + + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._prepare_floppy_image(task, deploy_args) + + create_vfat_image_mock.assert_called_once_with( + 'image-tmp-file', parameters=deploy_args) + execute_mock.assert_called_once_with( + 'cp', + 'image-tmp-file', + '/remote_image_share_root/' + "image-%s.img" % self.node.uuid, + check_exit_code=[0]) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image_exception(self, + tempfile_mock, + create_vfat_image_mock, + execute_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock() + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + tempfile_mock.side_effect = iter([mock_image_file_handle]) + + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + execute_mock.side_effect = Exception("fake error") + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_deploy._prepare_floppy_image, + task, + deploy_args) + + create_vfat_image_mock.assert_called_once_with( + 'image-tmp-file', parameters=deploy_args) + execute_mock.assert_called_once_with( + 'cp', + 'image-tmp-file', + '/remote_image_share_root/' + "image-%s.img" % self.node.uuid, + check_exit_code=[0]) + + @mock.patch.object(irmc_deploy, '_attach_virtual_cd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_attach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_floppy_image', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_with_parameters(self, + _prepare_floppy_image_mock, + _attach_virtual_fd_mock, + _attach_virtual_cd_mock): + parameters = {'a': 'b'} + iso_filename = 'deploy_iso_or_boot_iso' + _prepare_floppy_image_mock.return_value = 'floppy_file_name' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy.setup_vmedia_for_boot(task, iso_filename, parameters) + _prepare_floppy_image_mock.assert_called_once_with(task, + parameters) + _attach_virtual_fd_mock.assert_called_once_with(task.node, + 'floppy_file_name') + _attach_virtual_cd_mock.assert_called_once_with(task.node, + iso_filename) + + @mock.patch.object(irmc_deploy, '_attach_virtual_cd', autospec=True) + def test_setup_vmedia_for_boot_without_parameters( + self, + _attach_virtual_cd_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy.setup_vmedia_for_boot(task, 'bootable_iso_filename') + _attach_virtual_cd_mock.assert_called_once_with( + task.node, + 'bootable_iso_filename') + + @mock.patch.object(irmc_deploy, '_get_deploy_iso_name', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_get_floppy_image_name', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_remove_share_file', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_cd', spec_set=True, + autospec=True) + def test__cleanup_vmedia_boot_ok(self, + _detach_virtual_cd_mock, + _detach_virtual_fd_mock, + _remove_share_file_mock, + _get_floppy_image_name_mock, + _get_deploy_iso_name_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._cleanup_vmedia_boot(task) + + _detach_virtual_cd_mock.assert_called_once_with(task.node) + _detach_virtual_fd_mock.assert_called_once_with(task.node) + _get_floppy_image_name_mock.assert_called_once_with(task.node) + _get_deploy_iso_name_mock.assert_called_once_with(task.node) + self.assertTrue(_remove_share_file_mock.call_count, 2) + _remove_share_file_mock.assert_has_calls( + [mock.call(_get_floppy_image_name_mock(task.node)), + mock.call(_get_deploy_iso_name_mock(task.node))]) + + @mock.patch.object(utils, 'unlink_without_raise', spec_set=True, + autospec=True) + def test__remove_share_file(self, unlink_without_raise_mock): + CONF.irmc.remote_image_share_name = '/' + + irmc_deploy._remove_share_file("boot.iso") + + unlink_without_raise_mock.assert_called_once_with('/boot.iso') + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_cd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_deploy.scci.get_virtual_cd_set_params_cmd = ( + mock.MagicMock(sepc_set=[])) + cd_set_params = (irmc_deploy.scci + .get_virtual_cd_set_params_cmd.return_value) + + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_user_domain = 'local' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + + irmc_deploy.scci.get_share_type.return_value = 0 + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._attach_virtual_cd(task.node, 'iso_filename') + + get_irmc_client_mock.assert_called_once_with(task.node) + (irmc_deploy.scci.get_virtual_cd_set_params_cmd + .assert_called_once_with)('10.20.30.40', + 'local', + 0, + 'share', + 'iso_filename', + 'admin', + 'admin0') + irmc_client.assert_has_calls( + [mock.call(cd_set_params, async=False), + mock.call(irmc_deploy.scci.MOUNT_CD, async=False)]) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_cd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._attach_virtual_cd, + task.node, + 'iso_filename') + get_irmc_client_mock.assert_called_once_with(task.node) + self.assertEqual("iRMC Inserting virtual cdrom failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_cd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._detach_virtual_cd(task.node) + + irmc_client.assert_called_once_with(irmc_deploy.scci.UNMOUNT_CD) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_cd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._detach_virtual_cd, + task.node) + self.assertEqual("iRMC Ejecting virtual cdrom failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_fd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_deploy.scci.get_virtual_fd_set_params_cmd = ( + mock.MagicMock(sepc_set=[])) + fd_set_params = (irmc_deploy.scci + .get_virtual_fd_set_params_cmd.return_value) + + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_user_domain = 'local' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + + irmc_deploy.scci.get_share_type.return_value = 0 + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._attach_virtual_fd(task.node, + 'floppy_image_filename') + + get_irmc_client_mock.assert_called_once_with(task.node) + (irmc_deploy.scci.get_virtual_fd_set_params_cmd + .assert_called_once_with)('10.20.30.40', + 'local', + 0, + 'share', + 'floppy_image_filename', + 'admin', + 'admin0') + irmc_client.assert_has_calls( + [mock.call(fd_set_params, async=False), + mock.call(irmc_deploy.scci.MOUNT_FD, async=False)]) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_fd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._attach_virtual_fd, + task.node, + 'iso_filename') + get_irmc_client_mock.assert_called_once_with(task.node) + self.assertEqual("iRMC Inserting virtual floppy failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_fd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._detach_virtual_fd(task.node) + + irmc_client.assert_called_once_with(irmc_deploy.scci.UNMOUNT_FD) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_fd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._detach_virtual_fd, + task.node) + self.assertEqual("iRMC Ejecting virtual floppy failed. " + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_deploy, '_parse_config_option', spec_set=True, + autospec=True) + def test__check_share_fs_mounted_ok(self, parse_conf_mock): + # Note(naohirot): mock.patch.stop() and mock.patch.start() don't work. + # therefor monkey patching is used to + # irmc_deploy._check_share_fs_mounted. + # irmc_deploy._check_share_fs_mounted is mocked in + # third_party_driver_mocks.py. + # irmc_deploy._check_share_fs_mounted_orig is the real function. + CONF.irmc.remote_image_share_root = '/' + CONF.irmc.remote_image_share_type = 'nfs' + result = irmc_deploy._check_share_fs_mounted_orig() + + parse_conf_mock.assert_called_once_with() + self.assertIsNone(result) + + @mock.patch.object(irmc_deploy, '_parse_config_option', spec_set=True, + autospec=True) + def test__check_share_fs_mounted_exception(self, parse_conf_mock): + # Note(naohirot): mock.patch.stop() and mock.patch.start() don't work. + # therefor monkey patching is used to + # irmc_deploy._check_share_fs_mounted. + # irmc_deploy._check_share_fs_mounted is mocked in + # third_party_driver_mocks.py. + # irmc_deploy._check_share_fs_mounted_orig is the real function. + CONF.irmc.remote_image_share_root = '/etc' + CONF.irmc.remote_image_share_type = 'cifs' + + self.assertRaises(exception.IRMCSharedFileSystemNotMounted, + irmc_deploy._check_share_fs_mounted_orig) + parse_conf_mock.assert_called_once_with() + + +class IRMCVirtualMediaIscsiDeployTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaIscsiDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + def test_validate_glance_image(self, + validate_mock, + deploy_info_mock, + is_glance_image_mock, + validate_prop_mock, + validate_capabilities_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + deploy_info_mock.return_value = d_info + is_glance_image_mock.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with( + task.context, d_info, ['kernel_id', 'ramdisk_id']) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_check_share_fs_mounted', spec_set=True, + autospec=True) + def test_validate_non_glance_image(self, + _check_share_fs_mounted_mock, + validate_mock, + deploy_info_mock, + is_glance_image_mock, + validate_prop_mock, + validate_capabilities_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + deploy_info_mock.return_value = d_info + is_glance_image_mock.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + + _check_share_fs_mounted_mock.assert_called_once_with() + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with( + task.context, d_info, ['kernel', 'ramdisk']) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_deploy, '_reboot_into_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_single_nic_with_vif_port_id', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'check_image_size', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'cache_instance_image', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_deploy(self, + node_power_action_mock, + cache_instance_image_mock, + check_image_size_mock, + build_deploy_ramdisk_options_mock, + get_single_nic_with_vif_port_id_mock, + _reboot_into_deploy_iso_mock): + bootif = get_single_nic_with_vif_port_id_mock.return_value + build_deploy_ramdisk_options_mock.return_value = bootif + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.deploy(task) + + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + cache_instance_image_mock.assert_called_once_with( + task.context, task.node) + check_image_size_mock.assert_called_once_with(task) + build_deploy_ramdisk_options_mock.assert_called_once_with( + task.node) + get_single_nic_with_vif_port_id_mock.assert_called_once_with( + task) + _reboot_into_deploy_iso_mock.assert_called_once_with( + task, bootif) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_remove_share_file', spec_set=True, + autospec=True) + def test_tear_down(self, _remove_share_file_mock, node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['irmc_boot_iso'] = 'glance://deploy_iso' + task.node.driver_internal_info['irmc_boot_iso'] = 'irmc_boot.iso' + + returned_state = task.driver.deploy.tear_down(task) + + _remove_share_file_mock.assert_called_once_with( + irmc_deploy._get_boot_iso_name(task.node)) + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + self.assertFalse( + task.node.driver_internal_info.get('irmc_boot_iso')) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(iscsi_deploy, 'destroy_images', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_clean_up(self, _cleanup_vmedia_boot_mock, destroy_images_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + destroy_images_mock.assert_called_once_with(task.node.uuid) + + +class IRMCVirtualMediaAgentDeployTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaAgentDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="agent_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='agent_irmc', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, _parse_driver_info_mock, + validate_capabilities_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + _parse_driver_info_mock.assert_called_once_with(task.node) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_deploy, '_reboot_into_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(agent, 'build_agent_options', spec_set=True, + autospec=True) + def test_deploy(self, build_agent_options_mock, + _reboot_into_deploy_iso_mock): + deploy_ramdisk_opts = build_agent_options_mock.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.deploy(task) + build_agent_options_mock.assert_called_once_with(task.node) + _reboot_into_deploy_iso_mock.assert_called_once_with( + task, deploy_ramdisk_opts) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down(self, node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(agent, 'build_instance_info_for_deploy', spec_set=True, + autospec=True) + def test_prepare(self, build_instance_info_for_deploy_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.save = mock.MagicMock(sepc_set=[]) + task.driver.deploy.prepare(task) + build_instance_info_for_deploy_mock.assert_called_once_with( + task) + task.node.save.assert_called_once_with() + + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_clean_up(self, _cleanup_vmedia_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + + +class VendorPassthruTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(VendorPassthruTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + CONF.irmc.remote_image_share_root = '/remote_image_share_root' + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + CONF.irmc.remote_image_user_domain = 'local' + + @mock.patch.object(iscsi_deploy, 'get_deploy_info') + def test_validate(self, get_deploy_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.validate(task, method=None, a=1) + get_deploy_info_mock.assert_called_once_with(task.node, a=1) + + @mock.patch.object(deploy_utils, 'set_failed_state', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_ok(self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + set_failed_state_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root_uuid'} + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info['irmc_boot_iso'] = 'irmc_boot.iso' + task.driver.vendor.pass_deploy_info(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with( + task, method='pass_deploy_info', address='123456') + + _prepare_boot_iso_mock.assert_called_once_with( + task, 'root_uuid') + setup_vmedia_for_boot_mock.assert_called_once_with( + task, 'irmc_boot.iso') + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM) + notify_ramdisk_to_proceed_mock.assert_called_once_with( + '123456') + + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + self.assertFalse(set_failed_state_mock.called) + + @mock.patch.object(deploy_utils, 'set_failed_state', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_fail(self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + set_failed_state_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + + self.node.provision_state = states.AVAILABLE + self.node.target_provision_state = states.NOSTATE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidState, + task.driver.vendor.pass_deploy_info, + task, **kwargs) + + self.assertEqual(states.AVAILABLE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + self.assertFalse(_cleanup_vmedia_boot_mock.called) + self.assertFalse(continue_deploy_mock.called) + self.assertFalse(_prepare_boot_iso_mock.called) + self.assertFalse(setup_vmedia_for_boot_mock.called) + self.assertFalse(node_set_boot_device_mock.called) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + self.assertFalse(set_failed_state_mock.called) + + @mock.patch.object(deploy_utils, 'set_failed_state', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info__prepare_boot_exception( + self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + set_failed_state_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root_uuid'} + _prepare_boot_iso_mock.side_effect = Exception("fake error") + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_deploy_info(task, **kwargs) + + continue_deploy_mock.assert_called_once_with( + task, method='pass_deploy_info', address='123456') + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + _prepare_boot_iso_mock.assert_called_once_with( + task, 'root_uuid') + self.assertFalse(setup_vmedia_for_boot_mock.called) + self.assertFalse(node_set_boot_device_mock.called) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + + msg = _('Failed to continue iSCSI deployment.') + set_failed_state_mock.assert_called_once_with(task, msg) diff --git a/ironic/tests/drivers/irmc/test_power.py b/ironic/tests/drivers/irmc/test_power.py index efb51dd726..df71016bdf 100644 --- a/ironic/tests/drivers/irmc/test_power.py +++ b/ironic/tests/drivers/irmc/test_power.py @@ -18,10 +18,13 @@ Test class for iRMC Power Driver import mock from oslo_config import cfg +from ironic.common import boot_devices from ironic.common import exception from ironic.common import states from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import deploy as irmc_deploy from ironic.drivers.modules.irmc import power as irmc_power from ironic.tests.conductor import utils as mgr_utils from ironic.tests.db import base as db_base @@ -45,13 +48,17 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): driver_info=driver_info, instance_uuid='instance_uuid_123') - def test__set_power_state_power_on_ok(self, - get_irmc_client_mock): + @mock.patch.object(irmc_power, '_attach_boot_iso_if_needed') + def test__set_power_state_power_on_ok( + self, + _attach_boot_iso_if_needed_mock, + get_irmc_client_mock): irmc_client = get_irmc_client_mock.return_value target_state = states.POWER_ON with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: irmc_power._set_power_state(task, target_state) + _attach_boot_iso_if_needed_mock.assert_called_once_with(task) irmc_client.assert_called_once_with(irmc_power.scci.POWER_ON) def test__set_power_state_power_off_ok(self, @@ -63,13 +70,17 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): irmc_power._set_power_state(task, target_state) irmc_client.assert_called_once_with(irmc_power.scci.POWER_OFF) - def test__set_power_state_power_reboot_ok(self, - get_irmc_client_mock): + @mock.patch.object(irmc_power, '_attach_boot_iso_if_needed') + def test__set_power_state_power_reboot_ok( + self, + _attach_boot_iso_if_needed_mock, + get_irmc_client_mock): irmc_client = get_irmc_client_mock.return_value target_state = states.REBOOT with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: irmc_power._set_power_state(task, target_state) + _attach_boot_iso_if_needed_mock.assert_called_once_with(task) irmc_client.assert_called_once_with(irmc_power.scci.POWER_RESET) def test__set_power_state_invalid_target_state(self, @@ -94,6 +105,41 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): task, states.POWER_ON) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed( + self, + setup_vmedia_mock, + set_boot_device_mock, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.ACTIVE + task.node.driver_internal_info['irmc_boot_iso'] = 'boot-iso' + irmc_power._attach_boot_iso_if_needed(task) + setup_vmedia_mock.assert_called_once_with(task, 'boot-iso') + set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed_on_rebuild( + self, + setup_vmedia_mock, + set_boot_device_mock, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info['irmc_boot_iso'] = 'boot-iso' + irmc_power._attach_boot_iso_if_needed(task) + self.assertFalse(setup_vmedia_mock.called) + self.assertFalse(set_boot_device_mock.called) + class IRMCPowerTestCase(db_base.DbTestCase): def setUp(self): diff --git a/ironic/tests/drivers/test_irmc.py b/ironic/tests/drivers/test_irmc.py new file mode 100644 index 0000000000..e388ed703b --- /dev/null +++ b/ironic/tests/drivers/test_irmc.py @@ -0,0 +1,100 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for iRMC Deploy Driver +""" + +import mock +import testtools + +from ironic.common import exception +from ironic.drivers import irmc + + +class IRMCVirtualMediaIscsiTestCase(testtools.TestCase): + + def setUp(self): + irmc.deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaIscsiTestCase, self).setUp() + + @mock.patch.object(irmc.importutils, 'try_import', spec_set=True, + autospec=True) + def test___init___share_fs_mounted_ok(self, + mock_try_import): + mock_try_import.return_value = True + + driver = irmc.IRMCVirtualMediaIscsiDriver() + + self.assertIsInstance(driver.power, irmc.power.IRMCPower) + self.assertIsInstance(driver.deploy, + irmc.deploy.IRMCVirtualMediaIscsiDeploy) + self.assertIsInstance(driver.console, + irmc.ipmitool.IPMIShellinaboxConsole) + self.assertIsInstance(driver.management, + irmc.management.IRMCManagement) + self.assertIsInstance(driver.vendor, irmc.deploy.VendorPassthru) + + @mock.patch.object(irmc.importutils, 'try_import') + def test___init___try_import_exception(self, mock_try_import): + mock_try_import.return_value = False + + self.assertRaises(exception.DriverLoadError, + irmc.IRMCVirtualMediaIscsiDriver) + + @mock.patch.object(irmc.deploy.IRMCVirtualMediaIscsiDeploy, '__init__', + spec_set=True, autospec=True) + def test___init___share_fs_not_mounted_exception(self, __init___mock): + __init___mock.side_effect = exception.IRMCSharedFileSystemNotMounted() + + self.assertRaises(exception.IRMCSharedFileSystemNotMounted, + irmc.IRMCVirtualMediaIscsiDriver) + + +class IRMCVirtualMediaAgentTestCase(testtools.TestCase): + + def setUp(self): + irmc.deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaAgentTestCase, self).setUp() + + @mock.patch.object(irmc.importutils, 'try_import', spec_set=True, + autospec=True) + def test___init___share_fs_mounted_ok(self, + mock_try_import): + mock_try_import.return_value = True + + driver = irmc.IRMCVirtualMediaAgentDriver() + + self.assertIsInstance(driver.power, irmc.power.IRMCPower) + self.assertIsInstance(driver.deploy, + irmc.deploy.IRMCVirtualMediaAgentDeploy) + self.assertIsInstance(driver.console, + irmc.ipmitool.IPMIShellinaboxConsole) + self.assertIsInstance(driver.management, + irmc.management.IRMCManagement) + self.assertIsInstance(driver.vendor, irmc.agent.AgentVendorInterface) + + @mock.patch.object(irmc.importutils, 'try_import') + def test___init___try_import_exception(self, mock_try_import): + mock_try_import.return_value = False + + self.assertRaises(exception.DriverLoadError, + irmc.IRMCVirtualMediaAgentDriver) + + @mock.patch.object(irmc.deploy.IRMCVirtualMediaAgentDeploy, '__init__', + spec_set=True, autospec=True) + def test___init___share_fs_not_mounted_exception(self, __init___mock): + __init___mock.side_effect = exception.IRMCSharedFileSystemNotMounted() + + self.assertRaises(exception.IRMCSharedFileSystemNotMounted, + irmc.IRMCVirtualMediaAgentDriver) diff --git a/ironic/tests/drivers/third_party_driver_mock_specs.py b/ironic/tests/drivers/third_party_driver_mock_specs.py index 6ad08b5080..9adc7051ce 100644 --- a/ironic/tests/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/drivers/third_party_driver_mock_specs.py @@ -89,10 +89,17 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( 'POWER_OFF', 'POWER_ON', 'POWER_RESET', + 'MOUNT_CD', + 'UNMOUNT_CD', + 'MOUNT_FD', + 'UNMOUNT_FD', 'SCCIClientError', + 'get_share_type', 'get_client', 'get_report', 'get_sensor_data', + 'get_virtual_cd_set_params_cmd', + 'get_virtual_fd_set_params_cmd', ) # seamicro diff --git a/ironic/tests/drivers/third_party_driver_mocks.py b/ironic/tests/drivers/third_party_driver_mocks.py index fdb083c662..909ea0a0e8 100644 --- a/ironic/tests/drivers/third_party_driver_mocks.py +++ b/ironic/tests/drivers/third_party_driver_mocks.py @@ -163,7 +163,11 @@ if not scciclient: spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC, POWER_OFF=mock.sentinel.POWER_OFF, POWER_ON=mock.sentinel.POWER_ON, - POWER_RESET=mock.sentinel.POWER_RESET) + POWER_RESET=mock.sentinel.POWER_RESET, + MOUNT_CD=mock.sentinel.MOUNT_CD, + UNMOUNT_CD=mock.sentinel.UNMOUNT_CD, + MOUNT_FD=mock.sentinel.MOUNT_FD, + UNMOUNT_FD=mock.sentinel.UNMOUNT_FD) # if anything has loaded the iRMC driver yet, reload it now that the @@ -171,6 +175,17 @@ if not scciclient: if 'ironic.drivers.modules.irmc' in sys.modules: six.moves.reload_module(sys.modules['ironic.drivers.modules.irmc']) + +# install mock object to prevent 'iscsi_irmc' and 'agent_irmc' from +# checking whether NFS/CIFS share file system is mounted or not. +irmc_deploy = importutils.import_module( + 'ironic.drivers.modules.irmc.deploy') +irmc_deploy._check_share_fs_mounted_orig = irmc_deploy._check_share_fs_mounted +irmc_deploy._check_share_fs_mounted_patcher = mock.patch( + 'ironic.drivers.modules.irmc.deploy._check_share_fs_mounted') +irmc_deploy._check_share_fs_mounted_patcher.return_value = None + + pyremotevbox = importutils.try_import('pyremotevbox') if not pyremotevbox: pyremotevbox = mock.MagicMock(spec_set=mock_specs.PYREMOTEVBOX_SPEC) diff --git a/ironic/tests/test_glance_service.py b/ironic/tests/test_glance_service.py index c68f60ca15..5ec6251417 100644 --- a/ironic/tests/test_glance_service.py +++ b/ironic/tests/test_glance_service.py @@ -809,6 +809,20 @@ class TestServiceUtils(base.TestCase): image_href = None self.assertFalse(service_utils.is_glance_image(image_href)) + def test_is_image_href_ordinary_file_name_true(self): + image = "deploy.iso" + result = service_utils.is_image_href_ordinary_file_name(image) + self.assertTrue(result) + + def test_is_image_href_ordinary_file_name_false(self): + for image in ('733d1c44-a2ea-414b-aca7-69decf20d810', + 'glance://deploy_iso', + 'http://deploy_iso', + 'https://deploy_iso', + 'file://deploy_iso',): + result = service_utils.is_image_href_ordinary_file_name(image) + self.assertFalse(result) + class TestGlanceAPIServers(base.TestCase): diff --git a/setup.cfg b/setup.cfg index b14254b6e4..c42f9bd580 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ ironic.dhcp = ironic.drivers = agent_ilo = ironic.drivers.ilo:IloVirtualMediaAgentDriver agent_ipmitool = ironic.drivers.agent:AgentAndIPMIToolDriver + agent_irmc = ironic.drivers.irmc:IRMCVirtualMediaAgentDriver agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver agent_ssh = ironic.drivers.agent:AgentAndSSHDriver agent_vbox = ironic.drivers.agent:AgentAndVirtualBoxDriver @@ -58,6 +59,7 @@ ironic.drivers = fake_ucs = ironic.drivers.fake:FakeUcsDriver fake_wol = ironic.drivers.fake:FakeWakeOnLanDriver iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver + iscsi_irmc = ironic.drivers.irmc:IRMCVirtualMediaIscsiDriver pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver