# 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. """ Ironic oVirt power manager and management interface. Provides basic power control and management of virtual machines via oVirt sdk API. For use in dev and test environments. """ from ironic.common import boot_devices from ironic.common import exception from ironic.common.i18n import _ from ironic.common import states from ironic.conductor import task_manager from ironic.drivers import base from oslo_config import cfg from oslo_log import log as logging from oslo_utils import importutils from ironic_staging_drivers.common import exception as staging_exception ovirtsdk = importutils.try_import('ovirtsdk4') if ovirtsdk: import ovirtsdk4 as sdk import ovirtsdk4.types as otypes IRONIC_TO_OVIRT_DEVICE_MAPPING = { boot_devices.PXE: 'network', boot_devices.DISK: 'hd', boot_devices.CDROM: 'cdrom', } OVIRT_TO_IRONIC_DEVICE_MAPPING = {v: k for k, v in IRONIC_TO_OVIRT_DEVICE_MAPPING.items()} OVIRT_TO_IRONIC_POWER_MAPPING = { 'down': states.POWER_OFF, 'error': states.ERROR, 'image_locked': states.POWER_OFF, 'migrating': states.POWER_ON, 'not_responding': states.ERROR, 'paused': states.ERROR, 'powering_down': states.POWER_OFF, 'powering_up': states.POWER_ON, 'reboot_in_progress': states.POWER_ON, 'wait_for_launch': states.POWER_ON, 'up': states.POWER_ON } opts = [ cfg.StrOpt('address', default='127.0.0.1', help='oVirt address'), cfg.StrOpt('username', default='admin@internal', help='oVirt username'), cfg.StrOpt('password', help='oVirt password'), cfg.StrOpt('insecure', default=False, help='Skips verification of the oVirt host certificate'), cfg.StrOpt('ca_file', help='oVirt path to a CA file'), ] CONF = cfg.CONF CONF.register_opts(opts, group='ovirt') LOG = logging.getLogger(__name__) PROPERTIES = { 'ovirt_address': _("Address of the oVirt Manager"), 'ovirt_username': _("oVirt username"), 'ovirt_password': _("oVirt password"), 'ovirt_insecure': _("Skips oVirt host certificate's verification"), 'ovirt_ca_file': _("oVirt path to a CA file"), 'ovirt_vm_name': _("Name of the VM in oVirt. Required."), } def _parse_driver_info(node): """Gets the driver specific node driver info. This method validates whether the 'driver_info' property of the supplied node contains the required information for this driver. :param node: an Ironic Node object. :returns: a dict containing information from driver_info (or where applicable, config values). :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. """ conf_info = {attr: getattr(CONF.ovirt, attr) for attr in CONF.ovirt} node_info = node.driver_info or {} driver_info = {} for prop in PROPERTIES: node_value = node_info.get(prop) conf_value = conf_info.get(prop.replace('ovirt_', '')) value = node_value if node_value is not None else conf_value if value is None and prop not in ['ovirt_ca_file', 'ovirt_insecure']: msg = _("%s is not set in either the configuration or " "in the node's driver_info") % prop raise exception.MissingParameterValue(msg) else: driver_info[prop] = value insecure = driver_info['ovirt_insecure'] ovirt_ca_file = driver_info['ovirt_ca_file'] if not insecure and ovirt_ca_file is None: msg = _("Missing ovirt_ca_file in the node's driver_info") raise exception.MissingParameterValue(msg) return driver_info def _getvm(driver_info): address = driver_info['ovirt_address'] username = driver_info['ovirt_username'] password = driver_info['ovirt_password'] insecure = driver_info['ovirt_insecure'] ca_file = driver_info['ovirt_ca_file'] name = driver_info['ovirt_vm_name'].encode('ascii', 'ignore') url = "https://%s/ovirt-engine/api" % address try: connection = sdk.Connection(url=url, username=username, password=password, insecure=insecure, ca_file=ca_file) vms_service = connection.system_service().vms_service() vmsearch = vms_service.list(search='name=%s' % name) except sdk.Error as e: LOG.error("Could not fetch information about VM vm %(name)s, " "got error: %(error)s", {'name': name, 'error': e}) raise staging_exception.oVirtError(err=e) if vmsearch: return vms_service.vm_service(vmsearch[0].id) else: raise staging_exception.oVirtError(_("VM with name " "%s was not found") % name) class oVirtPower(base.PowerInterface): def get_properties(self): return PROPERTIES def validate(self, task): """Check if node.driver_info contains ovirt_vm_name. :param task: a TaskManager instance. :raises: MissingParameterValue, if some of the required parameters are missing in the node's driver_info. :raises: InvalidParameterValue, if some of the parameters have invalid values in the node's driver_info. """ _parse_driver_info(task.node) def get_power_state(self, task): """Gets the current power state. :param task: a TaskManager instance. :returns: one of :mod:`ironic.common.states` :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. """ driver_info = _parse_driver_info(task.node) vm_name = driver_info['ovirt_vm_name'] vm = _getvm(driver_info) status = vm.get().status.value if status not in OVIRT_TO_IRONIC_POWER_MAPPING: msg = ("oVirt returned unknown state for node %(node)s " "and vm %(vm)s") LOG.error(msg, {'node': task.node.uuid, 'vm': vm_name}) return states.ERROR else: return OVIRT_TO_IRONIC_POWER_MAPPING[status] @task_manager.require_exclusive_lock def set_power_state(self, task, target_state, timeout=None): """Turn the current power state on or off. :param task: a TaskManager instance. :param target_state: The desired power state POWER_ON, POWER_OFF or REBOOT from :mod:`ironic.common.states`. :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info OR if an invalid power state was specified. """ driver_info = _parse_driver_info(task.node) vm_name = driver_info['ovirt_vm_name'] vm = _getvm(driver_info) try: if target_state == states.POWER_OFF: vm.stop() elif target_state == states.POWER_ON: vm.start() elif target_state == states.REBOOT: status = vm.get().status.value if status == 'down': vm.start() else: vm.reboot() else: msg = _("'set_power_state' called with invalid power " "state '%s'") % target_state raise exception.InvalidParameterValue(msg) except sdk.Error as e: LOG.error("Could not change status of VM vm %(name)s " "got error: %(error)s", {'name': vm_name, 'error': e}) raise staging_exception.oVirtError(err=e) @task_manager.require_exclusive_lock def reboot(self, task, timeout=None): """Reboot the node. :param task: a TaskManager instance. :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. :raises: ovirtsdk4.Error, if error encountered from oVirt operation. """ self.set_power_state(task, states.REBOOT, timeout=timeout) class oVirtManagement(base.ManagementInterface): def get_properties(self): return PROPERTIES def validate(self, task): """Check that 'driver_info' contains ovirt_vm_name. Validates whether the 'driver_info' property of the supplied task's node contains the required credentials information. :param task: a task from TaskManager. :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. """ _parse_driver_info(task.node) def get_supported_boot_devices(self, task): """Get a list of the supported boot devices. :returns: A list with the supported boot devices defined in :mod:`ironic.common.boot_devices`. """ return sorted(list(IRONIC_TO_OVIRT_DEVICE_MAPPING)) def get_boot_device(self, task): """Get the current boot device for a node. :param task: a task from TaskManager. :returns: a dictionary containing: 'boot_device': one of the ironic.common.boot_devices or None 'persistent': True if boot device is persistent, False otherwise :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. :raises: oVirtError, if error encountered from oVirt operation. """ driver_info = _parse_driver_info(task.node) vm = _getvm(driver_info) boot_dev = vm.os.boot[0].get_dev() persistent = True ironic_boot_dev = OVIRT_TO_IRONIC_DEVICE_MAPPING.get(boot_dev) if not ironic_boot_dev: persistent = False msg = _("oVirt returned unknown boot device '%(device)s' " "for node %(node)s") LOG.error(msg, {'device': boot_dev, 'node': task.node.uuid}) raise staging_exception.oVirtError(msg.format(device=boot_dev, node=task.node.uuid)) return {'boot_device': ironic_boot_dev, 'persistent': persistent} @task_manager.require_exclusive_lock def set_boot_device(self, task, device, persistent=False): """Set the boot device for a node. :param task: a task from TaskManager. :param device: ironic.common.boot_devices :param persistent: This argument is ignored. :raises: MissingParameterValue, if some required parameter(s) are missing in the node's driver_info. :raises: InvalidParameterValue, if some parameter(s) have invalid value(s) in the node's driver_info. """ try: boot_dev = IRONIC_TO_OVIRT_DEVICE_MAPPING[device] except KeyError: raise exception.InvalidParameterValue(_( "Invalid boot device %s specified.") % device) driver_info = _parse_driver_info(task.node) vm = _getvm(driver_info) try: boot = otypes.Boot(devices=[otypes.BootDevice(boot_dev)]) bootos = otypes.OperatingSystem(boot=boot) vm.update(otypes.Vm(os=bootos)) except sdk.Error as e: LOG.error("Setting boot device failed for node %(node_id)s " "with error: %(error)s", {'node_id': task.node.uuid, 'error': e}) raise staging_exception.oVirtError(err=e) def get_sensors_data(self, task): """Get sensors data. :param task: a TaskManager instance. :raises: FailedToGetSensorData when getting the sensor data fails. :raises: FailedToParseSensorData when parsing sensor data fails. :returns: returns a consistent format dict of sensor data grouped by sensor type, which can be processed by Ceilometer. """ raise NotImplementedError()