Seed code for ironic_virtmedia_driver
[ta/ironic-virtmedia-driver.git] / src / ironic_virtmedia_driver / virtmedia.py
diff --git a/src/ironic_virtmedia_driver/virtmedia.py b/src/ironic_virtmedia_driver/virtmedia.py
new file mode 100644 (file)
index 0000000..d42b4b5
--- /dev/null
@@ -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