# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # 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 SSH power manager. Provides basic power control of virtual machines via SSH. For use in dev and test environments. Currently supported environments are: Virtual Box (vbox) Virsh (virsh) VMware (vmware) Parallels (parallels) """ import os from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils import retrying 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.i18n import _LW from ironic.common import states from ironic.common import utils from ironic.conductor import task_manager from ironic.drivers import base from ironic.drivers import utils as driver_utils libvirt_opts = [ cfg.StrOpt('libvirt_uri', default='qemu:///system', help=_('libvirt URI.')), cfg.IntOpt('get_vm_name_attempts', default=3, help=_("Number of attempts to try to get VM name used by the " "host that corresponds to a node's MAC address.")), cfg.IntOpt('get_vm_name_retry_interval', default=3, help=_("Number of seconds to wait between attempts to get " "VM name used by the host that corresponds to a " "node's MAC address.")), ] CONF = cfg.CONF CONF.register_opts(libvirt_opts, group='ssh') LOG = logging.getLogger(__name__) REQUIRED_PROPERTIES = { 'ssh_address': _("IP address or hostname of the node to ssh into. " "Required."), 'ssh_username': _("username to authenticate as. Required."), 'ssh_virt_type': _("virtualization software to use; one of vbox, virsh, " "vmware, parallels. Required.") } OTHER_PROPERTIES = { 'ssh_key_contents': _("private key(s). One of this, ssh_key_filename, " "or ssh_password must be specified."), 'ssh_key_filename': _("(list of) filename(s) of optional private key(s) " "for authentication. One of this, ssh_key_contents, " "or ssh_password must be specified."), 'ssh_password': _("password to use for authentication or for unlocking a " "private key. One of this, ssh_key_contents, or " "ssh_key_filename must be specified."), 'ssh_port': _("port on the node to connect to; default is 22. Optional.") } COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OTHER_PROPERTIES) # NOTE(dguerri) Generic boot device map. Virtualisation types that don't define # a more specific one, will use this. # This is left for compatibility with other modules and is still valid for # virsh and vmware. _BOOT_DEVICES_MAP = { boot_devices.DISK: 'hd', boot_devices.PXE: 'network', boot_devices.CDROM: 'cdrom', } def _get_boot_device_map(virt_type): if virt_type in ('virsh', 'vmware'): return _BOOT_DEVICES_MAP elif virt_type == 'vbox': return { boot_devices.DISK: 'disk', boot_devices.PXE: 'net', boot_devices.CDROM: 'dvd', } elif virt_type == 'parallels': return { boot_devices.DISK: 'hdd0', boot_devices.PXE: 'net0', boot_devices.CDROM: 'cdrom0', } else: raise exception.InvalidParameterValue(_( "SSHPowerDriver '%(virt_type)s' is not a valid virt_type.") % {'virt_type': virt_type}) def _get_command_sets(virt_type): """Retrieves the virt_type-specific commands to control power Required commands are as follows: base_cmd: Used by most sub-commands as the primary executable list_all: Lists all VMs (by virt_type identifier) that can be managed. One name per line, must not be quoted. list_running: Lists all running VMs (by virt_type identifier). One name per line, can be quoted. start_cmd / stop_cmd: Starts or stops the identified VM get_node_macs: Retrieves all MACs for an identified VM. One MAC per line, any standard format (see _normalize_mac) get_boot_device / set_boot_device: Gets or sets the primary boot device """ if virt_type == 'vbox': return { 'base_cmd': 'LC_ALL=C /usr/bin/VBoxManage', 'start_cmd': 'startvm {_NodeName_}', 'stop_cmd': 'controlvm {_NodeName_} poweroff', 'reboot_cmd': 'controlvm {_NodeName_} reset', 'list_all': "list vms|awk -F'\"' '{print $2}'", 'list_running': 'list runningvms', 'get_node_macs': ( "showvminfo --machinereadable {_NodeName_} | " "awk -F '\"' '/macaddress/{print $2}'"), 'set_boot_device': ( '{_BaseCmd_} modifyvm {_NodeName_} ' '--boot1 {_BootDevice_}'), 'get_boot_device': ( "{_BaseCmd_} showvminfo " "--machinereadable {_NodeName_} | " "awk -F '\"' '/boot1/{print $2}'"), } elif virt_type == 'vmware': return { 'base_cmd': 'LC_ALL=C /bin/vim-cmd', 'start_cmd': 'vmsvc/power.on {_NodeName_}', 'stop_cmd': 'vmsvc/power.off {_NodeName_}', 'reboot_cmd': 'vmsvc/power.reboot {_NodeName_}', 'list_all': "vmsvc/getallvms | awk '$1 ~ /^[0-9]+$/ {print $1}'", # NOTE(arata): In spite of its name, list_running_cmd shows a # single vmid, not a list. But it is OK. 'list_running': ( "vmsvc/power.getstate {_NodeName_} | " "grep 'Powered on' >/dev/null && " "echo '\"{_NodeName_}\"' || true"), # NOTE(arata): `true` is needed to handle a false vmid, which can # be returned by list_cmd. In that case, get_node_macs # returns an empty list rather than fails with # non-zero status code. 'get_node_macs': ( "vmsvc/device.getdevices {_NodeName_} | " "grep macAddress | awk -F '\"' '{print $2}' || true"), } elif virt_type == "virsh": # NOTE(NobodyCam): changes to the virsh commands will impact CI # see https://review.openstack.org/83906 # Change-Id: I160e4202952b7551b855dc7d91784d6a184cb0ed # for more detail. virsh_cmds = { 'base_cmd': 'LC_ALL=C /usr/bin/virsh', 'start_cmd': 'start {_NodeName_}', 'stop_cmd': 'destroy {_NodeName_}', 'reboot_cmd': 'reset {_NodeName_}', 'list_all': "list --all | tail -n +2 | awk -F\" \" '{print $2}'", 'list_running': ( "list --all|grep running | " "awk -v qc='\"' -F\" \" '{print qc$2qc}'"), 'get_node_macs': ( "dumpxml {_NodeName_} | " "awk -F \"'\" '/mac address/{print $2}'| tr -d ':'"), 'set_boot_device': ( "EDITOR=\"sed -i '//d;" "/<\/os>/i\'\" " "{_BaseCmd_} edit {_NodeName_}"), 'get_boot_device': ( "{_BaseCmd_} dumpxml {_NodeName_} | " "awk '/boot dev=/ { gsub( \".*dev=\" Q, \"\" ); " "gsub( Q \".*\", \"\" ); print; }' " "Q=\"'\" RS=\"[<>]\" | " "head -1"), } if CONF.ssh.libvirt_uri: virsh_cmds['base_cmd'] += ' --connect %s' % CONF.ssh.libvirt_uri return virsh_cmds elif virt_type == 'parallels': return { 'base_cmd': 'LC_ALL=C /usr/bin/prlctl', 'start_cmd': 'start {_NodeName_}', 'stop_cmd': 'stop {_NodeName_} --kill', 'reboot_cmd': 'reset {_NodeName_}', 'list_all': "list -a -o name |tail -n +2", 'list_running': 'list -o name |tail -n +2', 'get_node_macs': ( "list -j -i \"{_NodeName_}\" | " "awk -F'\"' '/\"mac\":/ {print $4}' | " "sed 's/\\(..\\)\\(..\\)\\(..\\)\\(..\\)\\(..\\)\\(..\\)/" "\\1:\\2:\\3:\\4:\\5\\6/' | " "tr '[:upper:]' '[:lower:]'"), 'set_boot_device': ( "{_BaseCmd_} set {_NodeName_} " "--device-bootorder \"{_BootDevice_}\""), 'get_boot_device': ( "{_BaseCmd_} list -i {_NodeName_} | " "awk '/^Boot order:/ {print $3}'"), } else: raise exception.InvalidParameterValue(_( "SSHPowerDriver '%(virt_type)s' is not a valid virt_type, ") % {'virt_type': virt_type}) def _normalize_mac(mac): return mac.replace('-', '').replace(':', '').lower() def _get_boot_device(ssh_obj, driver_info): """Get the current boot device. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :raises: SSHCommandFailed on an error from ssh. :raises: NotImplementedError if the virt_type does not support getting the boot device. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. """ cmd_to_exec = driver_info['cmd_set'].get('get_boot_device') if cmd_to_exec: boot_device_map = _get_boot_device_map(driver_info['virt_type']) node_name = _get_hosts_name_for_node(ssh_obj, driver_info) base_cmd = driver_info['cmd_set']['base_cmd'] cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) cmd_to_exec = cmd_to_exec.replace('{_BaseCmd_}', base_cmd) stdout, stderr = _ssh_execute(ssh_obj, cmd_to_exec) return next((dev for dev, hdev in boot_device_map.items() if hdev == stdout), None) else: raise NotImplementedError() def _set_boot_device(ssh_obj, driver_info, device): """Set the boot device. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :param device: the boot device. :raises: SSHCommandFailed on an error from ssh. :raises: NotImplementedError if the virt_type does not support setting the boot device. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. """ cmd_to_exec = driver_info['cmd_set'].get('set_boot_device') if cmd_to_exec: node_name = _get_hosts_name_for_node(ssh_obj, driver_info) base_cmd = driver_info['cmd_set']['base_cmd'] cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) cmd_to_exec = cmd_to_exec.replace('{_BootDevice_}', device) cmd_to_exec = cmd_to_exec.replace('{_BaseCmd_}', base_cmd) _ssh_execute(ssh_obj, cmd_to_exec) else: raise NotImplementedError() def _ssh_execute(ssh_obj, cmd_to_exec): """Executes a command via ssh. Executes a command via ssh and returns a list of the lines of the output from the command. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param cmd_to_exec: command to execute. :returns: list of the lines of output from the command. :raises: SSHCommandFailed on an error from ssh. """ try: output_list = processutils.ssh_execute(ssh_obj, cmd_to_exec)[0].split('\n') except Exception as e: LOG.error(_LE("Cannot execute SSH cmd %(cmd)s. Reason: %(err)s."), {'cmd': cmd_to_exec, 'err': e}) raise exception.SSHCommandFailed(cmd=cmd_to_exec) return output_list def _parse_driver_info(node): """Gets the information needed for accessing the node. :param node: the Node of interest. :returns: dictionary of information. :raises: InvalidParameterValue if any required parameters are incorrect. :raises: MissingParameterValue if any required parameters are missing. """ info = node.driver_info or {} missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] if missing_info: raise exception.MissingParameterValue(_( "SSHPowerDriver requires the following parameters to be set in " "node's driver_info: %s.") % missing_info) address = info.get('ssh_address') username = info.get('ssh_username') password = info.get('ssh_password') try: port = int(info.get('ssh_port', 22)) except ValueError: raise exception.InvalidParameterValue(_( "SSHPowerDriver requires ssh_port to be integer value")) key_contents = info.get('ssh_key_contents') key_filename = info.get('ssh_key_filename') virt_type = info.get('ssh_virt_type') # NOTE(deva): we map 'address' from API to 'host' for common utils res = { 'host': address, 'username': username, 'port': port, 'virt_type': virt_type, 'uuid': node.uuid } cmd_set = _get_command_sets(virt_type) res['cmd_set'] = cmd_set # Only one credential may be set (avoids complexity around having # precedence etc). if len([v for v in (password, key_filename, key_contents) if v]) != 1: raise exception.InvalidParameterValue(_( "SSHPowerDriver requires one and only one of password, " "key_contents and key_filename to be set.")) if password: res['password'] = password elif key_contents: res['key_contents'] = key_contents else: if not os.path.isfile(key_filename): raise exception.InvalidParameterValue(_( "SSH key file %s not found.") % key_filename) res['key_filename'] = key_filename return res def _get_power_status(ssh_obj, driver_info): """Returns a node's current power state. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :returns: one of ironic.common.states POWER_OFF, POWER_ON. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. """ power_state = None node_name = _get_hosts_name_for_node(ssh_obj, driver_info) # Get a list of vms running on the host. If the command supports # it, explicitly specify the desired node." cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], driver_info['cmd_set']['list_running']) cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) running_list = _ssh_execute(ssh_obj, cmd_to_exec) # Command should return a list of running vms. If the current node is # not listed then we can assume it is not powered on. quoted_node_name = '"%s"' % node_name for node in running_list: if not node: continue # 'node' here is a formatted output from the virt cli's. The # node name is either an exact match or quoted (optionally with # other information, e.g. vbox returns '"NodeName" {}') if (quoted_node_name in node) or (node_name == node): power_state = states.POWER_ON break if not power_state: power_state = states.POWER_OFF return power_state def _get_connection(node): """Returns an SSH client connected to a node. :param node: the Node. :returns: paramiko.SSHClient, an active ssh connection. """ return utils.ssh_connect(_parse_driver_info(node)) def _get_hosts_name_for_node(ssh_obj, driver_info): """Get the name the host uses to reference the node. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :returns: the name of the node. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs """ @retrying.retry( retry_on_result=lambda v: v is None, retry_on_exception=lambda _: False, # Do not retry on SSHCommandFailed stop_max_attempt_number=CONF.ssh.get_vm_name_attempts, wait_fixed=CONF.ssh.get_vm_name_retry_interval * 1000) def _with_retries(): matched_name = None cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], driver_info['cmd_set']['list_all']) full_node_list = _ssh_execute(ssh_obj, cmd_to_exec) LOG.debug("Retrieved Node List: %s" % repr(full_node_list)) # for each node check Mac Addresses for node in full_node_list: if not node: continue LOG.debug("Checking Node: %s's Mac address." % node) cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], driver_info['cmd_set']['get_node_macs']) cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node) hosts_node_mac_list = _ssh_execute(ssh_obj, cmd_to_exec) for host_mac in hosts_node_mac_list: if not host_mac: continue for node_mac in driver_info['macs']: if _normalize_mac(host_mac) in _normalize_mac(node_mac): LOG.debug("Found Mac address: %s" % node_mac) matched_name = node break if matched_name: break if matched_name: break return matched_name try: return _with_retries() except retrying.RetryError: raise exception.NodeNotFound( _("SSH driver was not able to find a VM with any of the " "specified MACs: %(macs)s for node %(node)s.") % {'macs': driver_info['macs'], 'node': driver_info['uuid']}) def _power_on(ssh_obj, driver_info): """Power ON this node. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :returns: one of ironic.common.states POWER_ON or ERROR. """ current_pstate = _get_power_status(ssh_obj, driver_info) if current_pstate == states.POWER_ON: _power_off(ssh_obj, driver_info) node_name = _get_hosts_name_for_node(ssh_obj, driver_info) cmd_to_power_on = "%s %s" % (driver_info['cmd_set']['base_cmd'], driver_info['cmd_set']['start_cmd']) cmd_to_power_on = cmd_to_power_on.replace('{_NodeName_}', node_name) _ssh_execute(ssh_obj, cmd_to_power_on) current_pstate = _get_power_status(ssh_obj, driver_info) if current_pstate == states.POWER_ON: return current_pstate else: return states.ERROR def _power_off(ssh_obj, driver_info): """Power OFF this node. :param ssh_obj: paramiko.SSHClient, an active ssh connection. :param driver_info: information for accessing the node. :returns: one of ironic.common.states POWER_OFF or ERROR. """ current_pstate = _get_power_status(ssh_obj, driver_info) if current_pstate == states.POWER_OFF: return current_pstate node_name = _get_hosts_name_for_node(ssh_obj, driver_info) cmd_to_power_off = "%s %s" % (driver_info['cmd_set']['base_cmd'], driver_info['cmd_set']['stop_cmd']) cmd_to_power_off = cmd_to_power_off.replace('{_NodeName_}', node_name) _ssh_execute(ssh_obj, cmd_to_power_off) current_pstate = _get_power_status(ssh_obj, driver_info) if current_pstate == states.POWER_OFF: return current_pstate else: return states.ERROR class SSHPower(base.PowerInterface): """SSH Power Interface. This PowerInterface class provides a mechanism for controlling the power state of virtual machines via SSH. NOTE: This driver supports VirtualBox and Virsh commands. NOTE: This driver does not currently support multi-node operations. """ def get_properties(self): return COMMON_PROPERTIES def validate(self, task): """Check that the node's 'driver_info' is valid. Check that the node's 'driver_info' contains the requisite fields and that an SSH connection to the node can be established. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue if any connection parameters are incorrect or if ssh failed to connect to the node. :raises: MissingParameterValue if no ports are enrolled for the given node. """ if not driver_utils.get_node_mac_addresses(task): raise exception.MissingParameterValue( _("Node %s does not have any port associated with it." ) % task.node.uuid) try: _get_connection(task.node) except exception.SSHConnectFailed as e: raise exception.InvalidParameterValue(_("SSH connection cannot" " be established: %s") % e) def get_power_state(self, task): """Get the current power state of the task's node. Poll the host for the current power state of the task's node. :param task: a TaskManager instance containing the node to act on. :returns: power state. One of :class:`ironic.common.states`. :raises: InvalidParameterValue if any connection parameters are incorrect. :raises: MissingParameterValue when a required parameter is missing :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. :raises: SSHCommandFailed on an error from ssh. :raises: SSHConnectFailed if ssh failed to connect to the node. """ driver_info = _parse_driver_info(task.node) driver_info['macs'] = driver_utils.get_node_mac_addresses(task) ssh_obj = _get_connection(task.node) return _get_power_status(ssh_obj, driver_info) @task_manager.require_exclusive_lock def set_power_state(self, task, pstate): """Turn the power on or off. Set the power state of the task's node. :param task: a TaskManager instance containing the node to act on. :param pstate: Either POWER_ON or POWER_OFF from :class: `ironic.common.states`. :raises: InvalidParameterValue if any connection parameters are incorrect, or if the desired power state is invalid. :raises: MissingParameterValue when a required parameter is missing :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. :raises: PowerStateFailure if it failed to set power state to pstate. :raises: SSHCommandFailed on an error from ssh. :raises: SSHConnectFailed if ssh failed to connect to the node. """ driver_info = _parse_driver_info(task.node) driver_info['macs'] = driver_utils.get_node_mac_addresses(task) ssh_obj = _get_connection(task.node) if pstate == states.POWER_ON: state = _power_on(ssh_obj, driver_info) elif pstate == states.POWER_OFF: state = _power_off(ssh_obj, driver_info) else: raise exception.InvalidParameterValue( _("set_power_state called with invalid power state %s." ) % pstate) if state != pstate: raise exception.PowerStateFailure(pstate=pstate) @task_manager.require_exclusive_lock def reboot(self, task): """Cycles the power to the task's node. Power cycles a node. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue if any connection parameters are incorrect. :raises: MissingParameterValue when a required parameter is missing :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. :raises: PowerStateFailure if it failed to set power state to POWER_ON. :raises: SSHCommandFailed on an error from ssh. :raises: SSHConnectFailed if ssh failed to connect to the node. """ driver_info = _parse_driver_info(task.node) driver_info['macs'] = driver_utils.get_node_mac_addresses(task) ssh_obj = _get_connection(task.node) # _power_on will turn the power off if it's already on. state = _power_on(ssh_obj, driver_info) if state != states.POWER_ON: raise exception.PowerStateFailure(pstate=states.POWER_ON) class SSHManagement(base.ManagementInterface): def get_properties(self): return COMMON_PROPERTIES def validate(self, task): """Check that 'driver_info' contains SSH credentials. Validates whether the 'driver_info' property of the supplied task's node contains the required credentials information. :param task: a task from TaskManager. :raises: InvalidParameterValue if any connection parameters are incorrect. :raises: MissingParameterValue if a required parameter is missing """ _parse_driver_info(task.node) def get_supported_boot_devices(self, task): """Get a list of the supported boot devices. :param task: a task from TaskManager. :returns: A list with the supported boot devices defined in :mod:`ironic.common.boot_devices`. """ return list(_BOOT_DEVICES_MAP.keys()) @task_manager.require_exclusive_lock def set_boot_device(self, task, device, persistent=False): """Set the boot device for the task's node. Set the boot device to use on next reboot of the node. :param task: a task from TaskManager. :param device: the boot device, one of :mod:`ironic.common.boot_devices`. :param persistent: Boolean value. True if the boot device will persist to all future boots, False if not. Default: False. Ignored by this driver. :raises: InvalidParameterValue if an invalid boot device is specified or if any connection parameters are incorrect. :raises: MissingParameterValue if a required parameter is missing :raises: SSHConnectFailed if ssh failed to connect to the node. :raises: SSHCommandFailed on an error from ssh. :raises: NotImplementedError if the virt_type does not support setting the boot device. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. """ node = task.node driver_info = _parse_driver_info(node) if device not in self.get_supported_boot_devices(task): raise exception.InvalidParameterValue(_( "Invalid boot device %s specified.") % device) driver_info['macs'] = driver_utils.get_node_mac_addresses(task) ssh_obj = _get_connection(node) boot_device_map = _get_boot_device_map(driver_info['virt_type']) try: _set_boot_device(ssh_obj, driver_info, boot_device_map[device]) except NotImplementedError: with excutils.save_and_reraise_exception(): LOG.error(_LE("Failed to set boot device for node %(node)s, " "virt_type %(vtype)s does not support this " "operation"), {'node': node.uuid, 'vtype': driver_info['virt_type']}) def get_boot_device(self, task): """Get the current boot device for the task's node. Provides the current boot device of the node. Be aware that not all drivers support this. :param task: a task from TaskManager. :raises: InvalidParameterValue if any connection parameters are incorrect. :raises: MissingParameterValue if a required parameter is missing :raises: SSHConnectFailed if ssh failed to connect to the node. :raises: SSHCommandFailed on an error from ssh. :raises: NodeNotFound if could not find a VM corresponding to any of the provided MACs. :returns: a dictionary containing: :boot_device: the boot device, one of :mod:`ironic.common.boot_devices` or None if it is unknown. :persistent: Whether the boot device will persist to all future boots or not, None if it is unknown. """ node = task.node driver_info = _parse_driver_info(node) driver_info['macs'] = driver_utils.get_node_mac_addresses(task) ssh_obj = _get_connection(node) response = {'boot_device': None, 'persistent': None} try: response['boot_device'] = _get_boot_device(ssh_obj, driver_info) except NotImplementedError: LOG.warning(_LW("Failed to get boot device for node %(node)s, " "virt_type %(vtype)s does not support this " "operation"), {'node': node.uuid, 'vtype': driver_info['virt_type']}) return response def get_sensors_data(self, task): """Get sensors data. Not implemented by this driver. :param task: a TaskManager instance. """ raise NotImplementedError()