536 lines
18 KiB
Python
536 lines
18 KiB
Python
# Copyright (c) 2015 Mirantis, Inc.
|
|
# 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 Libvirt power manager and management interface.
|
|
|
|
Provides basic power control and management of virtual machines
|
|
via Libvirt API.
|
|
|
|
For use in dev and test environments.
|
|
|
|
Currently supported environments are:
|
|
Virtual Box
|
|
Virsh
|
|
VMware WS/ESX/Player
|
|
XenServer
|
|
OpenVZ
|
|
Microsoft Hyper-V
|
|
Virtuozzo
|
|
|
|
Currently supported transports are:
|
|
unix (open auth)
|
|
tcp (SASL auth)
|
|
tls (SASL auth)
|
|
ssh (SSH Key auth)
|
|
|
|
"""
|
|
|
|
import os
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from ironic.common import boot_devices
|
|
from ironic.common import exception as ir_exc
|
|
from ironic.common.i18n import _
|
|
from ironic.common import states
|
|
from ironic.conductor import task_manager
|
|
from ironic.drivers import base
|
|
from ironic.drivers import utils as driver_utils
|
|
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 isd_exc
|
|
|
|
libvirt = importutils.try_import('libvirt')
|
|
|
|
CONF = cfg.CONF
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DEFAULT_URI = 'qemu+unix:///system'
|
|
REQUIRED_PROPERTIES = {}
|
|
OTHER_PROPERTIES = {
|
|
'libvirt_uri': _("libvirt URI, default is qemu+unix:///system. Optional."),
|
|
'sasl_username': _("username to authenticate as. Optional."),
|
|
'sasl_password': _("password to use for SASL authentication. Optional."),
|
|
'ssh_key_filename': _("filename of private key "
|
|
"for authentication. Optional.")
|
|
}
|
|
|
|
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
|
COMMON_PROPERTIES.update(OTHER_PROPERTIES)
|
|
|
|
|
|
_BOOT_DEVICES_MAP = {
|
|
boot_devices.DISK: 'hd',
|
|
boot_devices.PXE: 'network',
|
|
boot_devices.CDROM: 'cdrom',
|
|
}
|
|
|
|
|
|
def _get_libvirt_connection(driver_info):
|
|
"""Get the libvirt connection.
|
|
|
|
:param driver_info: driver info
|
|
:returns: the active libvirt connection
|
|
:raises: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
|
|
uri = driver_info.get('libvirt_uri') or DEFAULT_URI
|
|
sasl_username = driver_info.get('sasl_username')
|
|
sasl_password = driver_info.get('sasl_password')
|
|
ssh_key_filename = driver_info.get('ssh_key_filename')
|
|
|
|
try:
|
|
if sasl_username and sasl_password:
|
|
def request_cred(credentials, user_data):
|
|
for credential in credentials:
|
|
if credential[0] == libvirt.VIR_CRED_AUTHNAME:
|
|
credential[4] = sasl_username
|
|
elif credential[0] == libvirt.VIR_CRED_PASSPHRASE:
|
|
credential[4] = sasl_password
|
|
return 0
|
|
auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_PASSPHRASE],
|
|
request_cred, None]
|
|
conn = libvirt.openAuth(uri, auth, 0)
|
|
elif ssh_key_filename:
|
|
uri += "?keyfile=%s&no_verify=1" % ssh_key_filename
|
|
conn = libvirt.open(uri)
|
|
else:
|
|
conn = libvirt.open(uri)
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
if conn is None:
|
|
raise isd_exc.LibvirtError(
|
|
err=_("Failed to open connection to %s") % uri)
|
|
return conn
|
|
|
|
|
|
def _get_domain_by_macs(task):
|
|
"""Get the domain the host uses to reference the node.
|
|
|
|
:param task: a TaskManager instance containing the node to act on
|
|
:returns: the libvirt domain object.
|
|
:raises: NodeNotFound if could not find a VM corresponding to any
|
|
of the provided MACs.
|
|
:raises: InvalidParameterValue if any connection parameters are
|
|
incorrect or if failed to connect to the Libvirt uri.
|
|
:raises: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
|
|
driver_info = _parse_driver_info(task.node)
|
|
conn = _get_libvirt_connection(driver_info)
|
|
macs = driver_utils.get_node_mac_addresses(task)
|
|
node_macs = {driver_utils.normalize_mac(mac)
|
|
for mac in macs}
|
|
|
|
full_node_list = conn.listAllDomains()
|
|
|
|
for domain in full_node_list:
|
|
LOG.debug("Checking Domain: %s's Mac address", domain.name())
|
|
parsed = ET.fromstring(domain.XMLDesc())
|
|
domain_macs = {driver_utils.normalize_mac(
|
|
el.attrib['address']) for el in parsed.iter('mac')}
|
|
|
|
found_macs = domain_macs & node_macs # this is intersection of sets
|
|
if found_macs:
|
|
LOG.debug("Found MAC addresses: %s "
|
|
"for node: %s", found_macs, driver_info['uuid'])
|
|
return domain
|
|
|
|
raise ir_exc.NodeNotFound(
|
|
_("Can't find domain with specified MACs: %(macs)s "
|
|
"for node %(node)s") %
|
|
{'macs': domain_macs, 'node': driver_info['uuid']})
|
|
|
|
|
|
def _parse_driver_info(node):
|
|
"""Gets the information needed for accessing the node.
|
|
|
|
:param node: the Node of interest.
|
|
:returns: dictionary of information.
|
|
:raises: MissingParameterValue if any required parameters are missing.
|
|
:raises: InvalidParameterValue if any required parameters are incorrect.
|
|
"""
|
|
|
|
info = node.driver_info or {}
|
|
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
|
|
if missing_info:
|
|
raise ir_exc.MissingParameterValue(_(
|
|
"LibvirtPowerDriver requires the following parameters to be set in"
|
|
"node's driver_info: %s.") % missing_info)
|
|
|
|
uri = info.get('libvirt_uri') or DEFAULT_URI
|
|
sasl_username = info.get('sasl_username')
|
|
sasl_password = info.get('sasl_password')
|
|
ssh_key_filename = info.get('ssh_key_filename')
|
|
|
|
if sasl_username and sasl_password and ssh_key_filename:
|
|
raise ir_exc.InvalidParameterValue(_(
|
|
"LibvirtPower requires one and only one of the authentication, "
|
|
"(sasl_username, sasl_password) or ssh_key_filename to be set."))
|
|
|
|
if ssh_key_filename and not os.path.isfile(ssh_key_filename):
|
|
raise ir_exc.InvalidParameterValue(_(
|
|
"SSH key file %s not found.") % ssh_key_filename)
|
|
|
|
res = {
|
|
'libvirt_uri': uri,
|
|
'uuid': node.uuid,
|
|
'sasl_username': sasl_username,
|
|
'sasl_password': sasl_password,
|
|
'ssh_key_filename': ssh_key_filename,
|
|
}
|
|
|
|
return res
|
|
|
|
|
|
def _power_on(domain):
|
|
"""Power ON this domain.
|
|
|
|
:param domain: libvirt domain object.
|
|
:returns: one of ironic.common.states POWER_ON or ERROR.
|
|
:raises: LibvirtError if failed to connect to start domain.
|
|
"""
|
|
|
|
current_pstate = _get_power_state(domain)
|
|
if current_pstate == states.POWER_ON:
|
|
return current_pstate
|
|
|
|
try:
|
|
domain.create()
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
current_pstate = _get_power_state(domain)
|
|
if current_pstate == states.POWER_ON:
|
|
return current_pstate
|
|
else:
|
|
return states.ERROR
|
|
|
|
|
|
def _power_off(domain):
|
|
"""Power OFF this domain.
|
|
|
|
:param domain: libvirt domain object.
|
|
:returns: one of ironic.common.states POWER_OFF or ERROR.
|
|
:raises: LibvirtError if failed to destroy domain.
|
|
"""
|
|
|
|
current_pstate = _get_power_state(domain)
|
|
if current_pstate == states.POWER_OFF:
|
|
return current_pstate
|
|
|
|
try:
|
|
domain.destroy()
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
current_pstate = _get_power_state(domain)
|
|
if current_pstate == states.POWER_OFF:
|
|
return current_pstate
|
|
else:
|
|
return states.ERROR
|
|
|
|
|
|
def _power_cycle(domain):
|
|
"""Power cycles a node.
|
|
|
|
:param domain: libvirt domain object.
|
|
:raises: PowerStateFailure if it failed to set power state to POWER_ON.
|
|
:raises: LibvirtError if failed to power cycle domain.
|
|
"""
|
|
|
|
try:
|
|
_power_off(domain)
|
|
state = _power_on(domain)
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
if state != states.POWER_ON:
|
|
raise ir_exc.PowerStateFailure(pstate=states.POWER_ON)
|
|
|
|
|
|
def _get_power_state(domain):
|
|
"""Get the current power state of domain.
|
|
|
|
:param domain: libvirt domain object.
|
|
:returns: power state. One of :class:`ironic.common.states`.
|
|
:raises: LibvirtErr if failed to get doamin status.
|
|
"""
|
|
|
|
try:
|
|
if domain.isActive():
|
|
return states.POWER_ON
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
return states.POWER_OFF
|
|
|
|
|
|
def _get_boot_device(domain):
|
|
"""Get the current boot device.
|
|
|
|
:param domain: libvirt domain object.
|
|
:returns: boot device.
|
|
"""
|
|
|
|
boot_element = ET.fromstring(domain.XMLDesc()).find('.//os/boot')
|
|
boot_dev = None
|
|
if boot_element is not None:
|
|
boot_dev = boot_element.attrib.get('dev')
|
|
|
|
return boot_dev
|
|
|
|
|
|
def _set_boot_device(conn, domain, device):
|
|
"""Set the boot device.
|
|
|
|
:param conn: active libvirt connection.
|
|
:param domain: libvirt domain object.
|
|
:raises: LibvirtError if failed update domain xml.
|
|
"""
|
|
|
|
parsed = ET.fromstring(domain.XMLDesc())
|
|
os = parsed.find('os')
|
|
boot_list = os.findall('boot')
|
|
|
|
# Clear boot list
|
|
for boot_el in boot_list:
|
|
os.remove(boot_el)
|
|
|
|
boot_el = ET.SubElement(os, 'boot')
|
|
boot_el.set('dev', device)
|
|
|
|
try:
|
|
conn.defineXML(ET.tostring(parsed))
|
|
except libvirt.libvirtError as e:
|
|
raise isd_exc.LibvirtError(err=e)
|
|
|
|
|
|
class LibvirtPower(base.PowerInterface):
|
|
"""Libvirt Power Interface.
|
|
|
|
This PowerInterface class provides a mechanism for controlling the power
|
|
state of virtual machines via libvirt.
|
|
|
|
"""
|
|
|
|
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 Libvirt 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 failed to connect to the libvirt socket.
|
|
:raises: MissingParameterValue if no ports are enrolled for the given
|
|
node.
|
|
"""
|
|
|
|
if not driver_utils.get_node_mac_addresses(task):
|
|
raise ir_exc.MissingParameterValue(
|
|
_("Node %s does not have any ports associated with it"
|
|
) % task.node.uuid)
|
|
|
|
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: NodeNotFound if could not find a VM corresponding to any
|
|
of the provided MACs.
|
|
:raises: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
|
|
domain = _get_domain_by_macs(task)
|
|
return _get_power_state(domain)
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def set_power_state(self, task, pstate, timeout=None):
|
|
"""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`.
|
|
:param timeout: timeout (in seconds). Unsupported by this interface.
|
|
: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: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
# TODO(rloo): Support timeouts!
|
|
if timeout is not None:
|
|
LOG.warning(
|
|
"The 'libvirt' Power Interface's 'set_power_state' method "
|
|
"doesn't support the 'timeout' parameter. Ignoring "
|
|
"timeout=%(timeout)s",
|
|
{'timeout': timeout})
|
|
|
|
domain = _get_domain_by_macs(task)
|
|
if pstate == states.POWER_ON:
|
|
state = _power_on(domain)
|
|
elif pstate == states.POWER_OFF:
|
|
state = _power_off(domain)
|
|
else:
|
|
raise ir_exc.InvalidParameterValue(
|
|
_("set_power_state called with invalid power state %s."
|
|
) % pstate)
|
|
|
|
if state != pstate:
|
|
raise ir_exc.PowerStateFailure(pstate=pstate)
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def reboot(self, task, timeout=None):
|
|
"""Cycles the power to the task's node.
|
|
|
|
Power cycles a node.
|
|
|
|
:param task: a TaskManager instance containing the node to act on.
|
|
:param timeout: timeout (in seconds). Unsupported by this interface.
|
|
: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: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
# TODO(rloo): Support timeouts!
|
|
if timeout is not None:
|
|
LOG.warning("The 'libvirt' Power Interface's 'reboot' method "
|
|
"doesn't support the 'timeout' parameter. Ignoring "
|
|
"timeout=%(timeout)s",
|
|
{'timeout': timeout})
|
|
|
|
domain = _get_domain_by_macs(task)
|
|
|
|
_power_cycle(domain)
|
|
|
|
state = _get_power_state(domain)
|
|
|
|
if state != states.POWER_ON:
|
|
raise ir_exc.PowerStateFailure(pstate=states.POWER_ON)
|
|
|
|
|
|
class LibvirtManagement(base.ManagementInterface):
|
|
|
|
def get_properties(self):
|
|
return COMMON_PROPERTIES
|
|
|
|
def validate(self, task):
|
|
"""Check that 'driver_info' contains Libvirt URI.
|
|
|
|
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 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: NodeNotFound if could not find a VM corresponding to any
|
|
of the provided MACs.
|
|
:raises: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
|
|
domain = _get_domain_by_macs(task)
|
|
driver_info = _parse_driver_info(task.node)
|
|
conn = _get_libvirt_connection(driver_info)
|
|
if device not in self.get_supported_boot_devices(task):
|
|
raise ir_exc.InvalidParameterValue(_(
|
|
"Invalid boot device %s specified.") % device)
|
|
|
|
boot_device_map = _BOOT_DEVICES_MAP
|
|
_set_boot_device(conn, domain, boot_device_map[device])
|
|
|
|
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: 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.
|
|
:raises: LibvirtError if failed to connect to the Libvirt uri.
|
|
"""
|
|
|
|
domain = _get_domain_by_macs(task)
|
|
|
|
response = {'boot_device': None, 'persistent': None}
|
|
response['boot_device'] = _get_boot_device(domain)
|
|
return response
|
|
|
|
def get_sensors_data(self, task):
|
|
"""Get sensors data.
|
|
|
|
Not implemented by this driver.
|
|
|
|
:param task: a TaskManager instance.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|