X-Git-Url: https://gerrit.akraino.org/r/gitweb?p=ta%2Fironic-virtmedia-driver.git;a=blobdiff_plain;f=src%2Fironic_virtmedia_driver%2Fvirtmedia.py;fp=src%2Fironic_virtmedia_driver%2Fvirtmedia.py;h=d42b4b58b11d54cf4cf9b945836fe2c3d5ee3a53;hp=0000000000000000000000000000000000000000;hb=be2fb9459b65bb8b07b23adebadc16530d26e231;hpb=aaec6775b13b0c694235c80e6dfbca8696307af9 diff --git a/src/ironic_virtmedia_driver/virtmedia.py b/src/ironic_virtmedia_driver/virtmedia.py new file mode 100644 index 0000000..d42b4b5 --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia.py @@ -0,0 +1,447 @@ +# Copyright 2019 Nokia +# +# 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. +# + +import os +import shutil +import tempfile +import tarfile + +from ironic_lib import metrics_utils +from ironic_lib import utils as ironic_utils +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 _, _translators +from ironic.common import images +from ironic.common import states +from ironic.common import utils +from ironic.conductor import utils as manager_utils +from ironic_virtmedia_driver.conf import CONF +from ironic.drivers import base +from ironic.drivers.modules import deploy_utils +from ironic_virtmedia_driver import virtmedia_exception + +LOG = logging.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + +REQUIRED_PROPERTIES = { + 'virtmedia_deploy_iso': _("Deployment ISO image file name. " + "Required."), +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES + + +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.remote_image_share_root): + error_msgs.append( + _("Value '%s' for remote_image_share_root isn't a directory " + "or doesn't exist.") % + CONF.remote_image_share_root) + 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['virtmedia_deploy_iso'] = d_info.get('virtmedia_deploy_iso') + error_msg = _("Error validating 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['virtmedia_deploy_iso']): + deploy_iso = os.path.join(CONF.remote_image_share_root, + deploy_info['virtmedia_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_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(deploy_utils.get_image_instance_info(node)) + deploy_info.update(_parse_driver_info(node)) + + return deploy_info + +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.name + +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 _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.name + + +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: VirtmediaOperationError, if copying floppy image file failed. + """ + floppy_filename = _get_floppy_image_name(task.node) + floppy_fullpathname = os.path.join( + CONF.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: + shutil.copyfile(vfat_image_tmpfile_obj.name, + floppy_fullpathname) + except IOError as e: + operation = _("Copying floppy image file") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=e) + + return floppy_filename + +def _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename): + """ Quanta HW cannot attach 2 Virtual media at the moment. + Preparing CD which has floppy content at the end of it as + 64K block tar file. + """ + boot_iso_full_path = CONF.remote_image_share_root + bootable_iso_filename + floppy_image_full_path = CONF.remote_image_share_root + floppy_image_filename + tar_file_path = CONF.remote_image_share_root + floppy_image_filename + '.tar.gz' + + # Prepare a temporary Tar file + tar = tarfile.open(tar_file_path, "w:gz") + tar.add(floppy_image_full_path, arcname=os.path.basename(floppy_image_full_path)) + tar.close() + + # Using dd append Tar to iso and remove Tar file + ironic_utils.dd(tar_file_path, boot_iso_full_path, 'bs=64k', 'conv=notrunc,sync', 'oflag=append') + + os.remove(tar_file_path) + +def _remove_share_file(share_filename): + """Remove given file from the share file system. + + :param share_filename: a file name to be removed. + """ + share_fullpathname = os.path.join( + CONF.remote_image_share_root, share_filename) + LOG.debug(_translators.log_info("_remove_share_file: Unlinking %s"), share_fullpathname) + ironic_utils.unlink_without_raise(share_fullpathname) + +class VirtmediaBoot(base.BootInterface): + """Implementation of a boot interface using Virtual Media.""" + + def __init__(self): + """Constructor of VirtualMediaBoot. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + super(VirtmediaBoot, self).__init__() + + def get_properties(self): + return COMMON_PROPERTIES + + @METRICS.timer('VirtualMediaBoot.validate') + 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: 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. + """ + d_info = _parse_deploy_info(task.node) + if task.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'] + deploy_utils.validate_image_properties(task.context, d_info, + props) + + @METRICS.timer('VirtualMediaBoot.prepare_ramdisk') + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the deploy ramdisk using virtual media. + + Prepares the options for the deployment ramdisk, sets the node to boot + from virtual media cdrom. + + :param task: a TaskManager instance containing the node to act on. + :param ramdisk_params: the options to be passed to the deploy ramdisk. + :raises: ImageRefValidationFailed if no image service can handle + specified href. + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: InvalidParameterValue if the validation of the + PowerInterface or ManagementInterface fails. + :raises: VirtmediaOperationError, if some operation fails. + """ + + # NOTE(TheJulia): If this method is being called by something + # aside from deployment and clean, such as conductor takeover, we + # should treat this as a no-op and move on otherwise we would modify + # the state of the node due to virtual media operations. + + if (task.node.provision_state != states.DEPLOYING and + task.node.provision_state != states.CLEANING): + return + + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + ramdisk_params['BOOTIF'] = deploy_nic_mac + os_net_config = task.node.driver_info.get('os_net_config') + if os_net_config: + ramdisk_params['os_net_config'] = os_net_config + + self._setup_deploy_iso(task, ramdisk_params) + + @METRICS.timer('VirtualMediaBoot.clean_up_ramdisk') + def clean_up_ramdisk(self, task): + """Cleans up the boot of ironic ramdisk. + + This method cleans up the environment that was setup for booting the + deploy ramdisk. + + :param task: a task from TaskManager. + :returns: None + :raises: VirtmediaOperationError if operation failed. + """ + self._cleanup_vmedia_boot(task) + + @METRICS.timer('VirtualMediaBoot.prepare_instance') + def prepare_instance(self, task): + """Prepares the boot of instance. + + This method prepares the boot of the instance after reading + relevant information from the node's database. + + :param task: a task from TaskManager. + :returns: None + """ + self._cleanup_vmedia_boot(task) + + node = task.node + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if deploy_utils.get_boot_option(node) == "local" or iwdi: + manager_utils.node_set_boot_device(task, boot_devices.DISK, + persistent=True) + else: + driver_internal_info = node.driver_internal_info + root_uuid_or_disk_id = driver_internal_info['root_uuid_or_disk_id'] + self._configure_vmedia_boot(task, root_uuid_or_disk_id) + + @METRICS.timer('VirtualMediaBoot.clean_up_instance') + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the environment that was setup for booting + the instance. + + :param task: a task from TaskManager. + :returns: None + :raises: VirtmediaOperationError if operation failed. + """ + _remove_share_file(_get_boot_iso_name(task.node)) + driver_internal_info = task.node.driver_internal_info + driver_internal_info.pop('root_uuid_or_disk_id', None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + self._cleanup_vmedia_boot(task) + + def _configure_vmedia_boot(self, task, root_uuid_or_disk_id): + """Configure vmedia boot for the node.""" + return + + def _set_deploy_boot_device(self, task): + """Set the boot device for deployment""" + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + + def _setup_deploy_iso(self, task, ramdisk_options): + """Attaches virtual media and sets it as boot device. + + This method attaches the given deploy ISO as virtual media, prepares the + arguments for ramdisk in virtual media floppy. + + :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: VirtmediaOperationError, if some operation on failed. + :raises: InvalidParameterValue if the validation of the + PowerInterface or ManagementInterface fails. + """ + d_info = task.node.driver_info + + deploy_iso_href = d_info['virtmedia_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.remote_image_share_root, deploy_iso_file) + images.fetch(task.context, deploy_iso_href, deploy_iso_fullpathname) + + self._setup_vmedia_for_boot(task, deploy_iso_file, ramdisk_options) + self._set_deploy_boot_device(task) + + def _setup_vmedia_for_boot(self, 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: VirtmediaOperationError, if attaching a virtual media failed. + """ + LOG.info(_translators.log_info("Setting up node %s to boot from virtual media"), + task.node.uuid) + + self._detach_virtual_cd(task) + self._detach_virtual_fd(task) + + floppy_image_filename = None + if parameters: + floppy_image_filename = _prepare_floppy_image(task, parameters) + self._attach_virtual_fd(task, floppy_image_filename) + + if floppy_image_filename: + _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename) + + self._attach_virtual_cd(task, bootable_iso_filename) + + def _cleanup_vmedia_boot(self, task): + """Cleans a node after a virtual media boot. + + This method cleans up a node after a virtual media boot. + It deletes floppy and cdrom images if they exist 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: VirtmediaOperationError if ejecting virtual media failed. + """ + LOG.debug("Cleaning up node %s after virtual media boot", task.node.uuid) + + node = task.node + self._detach_virtual_cd(task) + self._detach_virtual_fd(task) + + _remove_share_file(_get_floppy_image_name(node)) + _remove_share_file(_get_deploy_iso_name(node)) + + def _attach_virtual_cd(self, task, 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: VirtmediaOperationError if attaching virtual media failed. + """ + return + + def _detach_virtual_cd(self, task): + """Detaches virtual cdrom on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if eject virtual cdrom failed. + """ + return + + def _attach_virtual_fd(self, task, floppy_image_filename): + """Attaches virtual floppy on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if insert virtual floppy failed. + """ + return + + def _detach_virtual_fd(self, task): + """Detaches virtual media floppy on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if eject virtual media floppy failed. + """ + return