Introduce libvirt power/mgmt driver

Libvirt has its own API. It allows to connect to different hypervisors
like xen, vmware, virtualbox, qemu, full list can be found at
https://libvirt.org/drivers.html.
It supports different type of transports like ssh, tcp, unix sockets.
This patch introduces new type of power and management drivers,
which use libvirt-python library to connect to hypervisor.

Change-Id: I2df214aab95c2f5d2505f5ad4ef9f3a542e44c6a
Depends-On: I12211db38a3fdb3b2d733e5769f2c052c32c4a75
Closes-Bug: #1523880
This commit is contained in:
vsaienko 2016-03-18 18:03:03 +02:00 committed by Vasyl Saienko
parent 5215fb6bb9
commit 296a45c8af
9 changed files with 1261 additions and 0 deletions

6
driver-requirements.txt Normal file
View File

@ -0,0 +1,6 @@
# This file lists all python libraries which are utilized by drivers,
# and may not be listed in global-requirements.
# libvirt driver requires libvirt-python library which is available on pypi
libvirt-python>=1.2.5 # LGPLv2+

View File

@ -29,3 +29,7 @@ class AMTConnectFailure(exception.IronicException):
class AMTFailure(exception.IronicException):
_msg_fmt = _("AMT call failed: %(cmd)s.")
class LibvirtError(exception.IronicException):
message = _("Libvirt call failed: %(err)s.")

View File

@ -0,0 +1,68 @@
# 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.
from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import fake
from ironic.drivers.modules import iscsi_deploy
from ironic.drivers.modules import pxe
from ironic_staging_drivers.libvirt import power
class FakeLibvirtFakeDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def __init__(self):
self.power = power.LibvirtPower()
self.deploy = fake.FakeDeploy()
self.management = power.LibvirtManagement()
class PXELibvirtAgentDriver(base.BaseDriver):
"""PXE + Agent + Libvirt driver.
NOTE: This driver is meant only for testing environments.
This driver implements the `core` functionality, combining
:class:`ironic.drivers.modules.power.LibvirtPower` (for power on/off and
reboot of virtual machines tunneled over Libvirt API), with
:class:`ironic.drivers.modules.agent.AgentDeploy` (for image
deployment). Implementations are in those respective classes; this class
is merely the glue between them.
"""
def __init__(self):
self.power = power.LibvirtPower()
self.boot = pxe.PXEBoot()
self.deploy = agent.AgentDeploy()
self.management = power.LibvirtManagement()
self.vendor = agent.AgentVendorInterface()
self.raid = agent.AgentRAID()
class PXELibvirtISCSIDriver(base.BaseDriver):
"""PXE + Libvirt + iSCSI driver.
This driver implements the `core` functionality, combining
:class:`ironic.drivers.modules.pxe.PXEBoot` for boot and
:class:`ironic_staging_drivers.libvirt.LibvirtPower` for power on/off and
:class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy` for image
deployment. Implementations are in those respective classes; this
class is merely the glue between them.
"""
def __init__(self):
self.power = power.LibvirtPower()
self.boot = pxe.PXEBoot()
self.deploy = iscsi_deploy.ISCSIDeploy()
self.management = power.LibvirtManagement()
self.vendor = iscsi_deploy.VendorPassthru()

View File

@ -0,0 +1,519 @@
# 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
import libvirt
from oslo_config import cfg
from oslo_log import log as logging
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 ironic_staging_drivers.common import exception as isd_exc
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):
"""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: LibvirtError if failed to connect to the Libvirt uri.
"""
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):
"""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: LibvirtError if failed to connect to the Libvirt uri.
"""
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()

View File

@ -0,0 +1,651 @@
# Copyright (c) 2016 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.
"""Test class for Ironic libvirt driver."""
import tempfile
import mock
from ironic.common import boot_devices
from ironic.common import driver_factory
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import utils as driver_utils
from ironic_staging_drivers.common import exception as isd_exc
from ironic_staging_drivers.libvirt import power
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
def _get_test_libvirt_driver_info(auth_type='ssh_key'):
if auth_type == 'ssh_key':
return {
'libvirt_uri': 'qemu+ssh://test@test/',
'ssh_key_filename': '/test/key/file'
}
elif auth_type == 'sasl':
return {
'libvirt_uri': 'test+tcp://localhost:5000/test',
'sasl_username': 'admin',
'sasl_password': 'admin'
}
elif auth_type == 'no_uri':
return {'ssh_key_filename': '/test/key/file'}
elif auth_type == 'socket':
return {'libvirt_uri': 'qemu+unix:///system?'
'socket=/opt/libvirt/run/libvirt-sock'}
return{
'libvirt_uri': 'qemu+ssh://test@test/',
'ssh_key_filename': '/test/key/file',
'sasl_username': 'admin',
'sasl_password': 'admin'
}
class FakeLibvirtDomain(object):
def __init__(self, uuid=None):
self.uuid = uuid
def name(self):
return 'test_libvirt_domain'
def XMLDesc(self, boot_dev=power._BOOT_DEVICES_MAP[boot_devices.PXE]):
return(
"""<domain type='qemu' id='4'>
<name>test_libvirt_domain</name>
<uuid>1be26c0b-03f2-4d2e-ae87-c02d7f33c123</uuid>
<bootloader>/usr/bin/pygrub</bootloader>
<os>
<type arch='x86_64' machine='pc-1.0'>hvm</type>
<boot dev='%(boot_dev)s'/>
<bios useserial='yes'/>
</os>
<memory>512000</memory>
<vcpu>1</vcpu>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>restart</on_crash>
<devices>
<interface type='bridge'>
<source bridge='br0'/>
<mac address='00:16:3e:49:1d:11'/>
<script path='vif-bridge'/>
</interface>
<graphics type='vnc' port='5900'/>
<console tty='/dev/pts/4'/>
<interface type='network'>
<mac address='52:54:00:5c:b7:df'/>
<source network='brbm'/>
<virtualport type='openvswitch'>
<parameters interfaceid='5c20239f'/>
</virtualport>
<model type='e1000'/>
</interface>
</devices>
</domain>""") % {'boot_dev': boot_dev}
class FakeConnection(object):
def listAllDomains(self):
return [FakeLibvirtDomain()]
class LibvirtValidateParametersTestCase(db_base.DbTestCase):
def test__parse_driver_info_good_ssh_key(self):
d_info = _get_test_libvirt_driver_info('ssh_key')
key_path = tempfile.mkdtemp() + '/test.key'
with open(key_path, 'wt'):
d_info['ssh_key_filename'] = key_path
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=d_info)
info = power._parse_driver_info(node)
self.assertEqual('qemu+ssh://test@test/', info.get('libvirt_uri'))
self.assertEqual(key_path, info.get('ssh_key_filename'))
self.assertEqual(node['uuid'], info.get('uuid'))
def test__parse_driver_info_no_ssh_key(self):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('ssh_key'))
self.assertRaises(exception.InvalidParameterValue,
power._parse_driver_info,
node)
def test__parse_driver_info_good_sasl_cred(self):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('sasl'))
info = power._parse_driver_info(node)
self.assertEqual('test+tcp://localhost:5000/test',
info.get('libvirt_uri'))
self.assertEqual('admin', info.get('sasl_username'))
self.assertEqual('admin', info.get('sasl_password'))
self.assertEqual(node['uuid'], info.get('uuid'))
def test__parse_driver_info_sasl_and_ssh_key(self):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('ssh_sasl'))
self.assertRaises(exception.InvalidParameterValue,
power._parse_driver_info,
node)
class LibvirtPrivateMethodsTestCase(db_base.DbTestCase):
@mock.patch.object(power.libvirt, 'openAuth', autospec=True)
def test__get_libvirt_connection_sasl_auth(self, libvirt_open_mock):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('sasl'))
power._get_libvirt_connection(node['driver_info'])
libvirt_open_mock.assert_called_once_with(
'test+tcp://localhost:5000/test',
[[power.libvirt.VIR_CRED_AUTHNAME,
power.libvirt.VIR_CRED_PASSPHRASE],
mock.ANY, # Inline cred function
None], 0)
@mock.patch.object(power.libvirt, 'open', autospec=True)
def test__get_libvirt_connection_ssh(self, libvirt_open_mock):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('ssh_key'))
power._get_libvirt_connection(node['driver_info'])
libvirt_open_mock.assert_called_once_with(
'qemu+ssh://test@test/?keyfile=/test/key/file&no_verify=1')
@mock.patch.object(power.libvirt, 'open', autospec=True)
def test__get_libvirt_connection_socket(self, libvirt_open_mock):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('socket'))
power._get_libvirt_connection(node['driver_info'])
libvirt_open_mock.assert_called_once_with(
'qemu+unix:///system?socket=/opt/libvirt/run/libvirt-sock')
@mock.patch.object(power.libvirt, 'open',
side_effect=power.libvirt.libvirtError('Error'))
def test__get_libvirt_connection_error_conn(self, libvirt_open_mock):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('socket'))
self.assertRaises(isd_exc.LibvirtError,
power._get_libvirt_connection,
node['driver_info'])
@mock.patch.object(power.libvirt, 'open',
return_value=None)
def test__get_libvirt_connection_error_none_conn(self, libvirt_open_mock):
node = obj_utils.get_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('socket'))
self.assertRaises(isd_exc.LibvirtError,
power._get_libvirt_connection,
node['driver_info'])
@mock.patch.object(power, '_get_libvirt_connection',
return_value=FakeConnection())
def test__get_domain_by_macs(self, libvirt_conn_mock):
mgr_utils.mock_the_extension_manager(driver="fake_libvirt_fake")
driver_factory.get_driver("fake_libvirt_fake")
node = obj_utils.create_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('socket'))
obj_utils.create_test_port(self.context,
node_id=node.id,
address='00:16:3e:49:1d:11')
with task_manager.acquire(self.context, node.uuid,
shared=True) as task:
domain = power._get_domain_by_macs(task)
self.assertEqual('test_libvirt_domain', domain.name())
@mock.patch.object(power, '_get_libvirt_connection',
return_value=FakeConnection())
def test__get_domain_by_macs_not_found(self, libvirt_conn_mock):
mgr_utils.mock_the_extension_manager(driver="fake_libvirt_fake")
driver_factory.get_driver("fake_libvirt_fake")
node = obj_utils.create_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('socket'))
obj_utils.create_test_port(self.context,
node_id=node.id,
address='00:17:3a:50:12:12')
with task_manager.acquire(self.context, node.uuid,
shared=True) as task:
self.assertRaises(exception.NodeNotFound,
power._get_domain_by_macs, task)
def test__get_power_state_on(self):
domain_mock = mock.Mock()
domain_mock.isActive = mock.MagicMock(return_value=True)
state = power._get_power_state(domain_mock)
domain_mock.isActive.assert_called_once_with()
self.assertEqual(states.POWER_ON, state)
def test__get_power_state_off(self):
domain_mock = mock.Mock()
domain_mock.isActive = mock.Mock(return_value=False)
state = power._get_power_state(domain_mock)
domain_mock.isActive.assert_called_once_with()
self.assertEqual(states.POWER_OFF, state)
def test__get_power_state_error(self):
domain_mock = mock.Mock()
domain_mock.isActive = mock.MagicMock(
side_effect=power.libvirt.libvirtError('Test'))
self.assertRaises(isd_exc.LibvirtError,
power._get_power_state,
domain_mock)
@mock.patch.object(power, '_power_off', autospec=True)
@mock.patch.object(power, '_power_on', return_value=states.POWER_ON)
def test__power_cycle(self, power_on_mock, power_off_mock):
power._power_cycle('fake domain')
power_on_mock.assert_called_once_with('fake domain')
power_off_mock.assert_called_once_with('fake domain')
@mock.patch.object(power, '_power_off', autospec=True)
@mock.patch.object(power, '_power_on', return_value=states.POWER_OFF)
def test__power_cycle_failure(self, power_on_mock, power_off_mock):
self.assertRaises(exception.PowerStateFailure,
power._power_cycle,
'fake domain')
power_off_mock.assert_called_once_with('fake domain')
@mock.patch.object(power, '_power_off', autospec=True)
@mock.patch.object(power, '_power_on',
side_effect=power.libvirt.libvirtError('Test'))
def test__power_cycle_error_conn(self, power_on_mock, power_off_mock):
self.assertRaises(isd_exc.LibvirtError,
power._power_cycle,
'fake domain')
power_off_mock.assert_called_once_with('fake domain')
@mock.patch.object(power, '_get_power_state',
return_value=states.POWER_ON)
def test__power_on_on(self, get_power_mock):
state = power._power_on('fake domain')
get_power_mock.assert_called_once_with('fake domain')
self.assertEqual(states.POWER_ON, state)
@mock.patch.object(power, '_get_power_state',
side_effect=[states.POWER_OFF, states.POWER_ON])
def test__power_on_off(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.create = mock.Mock()
state = power._power_on(domain_mock)
get_power_mock.assert_called_with(domain_mock)
domain_mock.create.assert_called_once_with()
self.assertEqual(states.POWER_ON, state)
@mock.patch.object(power, '_get_power_state',
side_effect=[states.POWER_OFF, states.POWER_OFF])
def test__power_on_error_state(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.create = mock.Mock()
state = power._power_on(domain_mock)
get_power_mock.assert_called_with(domain_mock)
domain_mock.create.assert_called_once_with()
self.assertEqual(states.ERROR, state)
@mock.patch.object(power, '_get_power_state',
return_value=states.POWER_OFF)
def test__power_on_error(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.create = mock.Mock(
side_effect=power.libvirt.libvirtError('Test'))
self.assertRaises(isd_exc.LibvirtError,
power._power_on,
domain_mock)
get_power_mock.assert_called_with(domain_mock)
@mock.patch.object(power, '_get_power_state',
return_value=states.POWER_OFF)
def test__power_off_off(self, get_power_mock):
state = power._power_off('fake domain')
get_power_mock.assert_called_once_with('fake domain')
self.assertEqual(states.POWER_OFF, state)
@mock.patch.object(power, '_get_power_state',
side_effect=[states.POWER_ON, states.POWER_OFF])
def test__power_off_on(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.destroy = mock.Mock()
state = power._power_off(domain_mock)
get_power_mock.assert_called_with(domain_mock)
domain_mock.destroy.assert_called_once_with()
self.assertEqual(states.POWER_OFF, state)
@mock.patch.object(power, '_get_power_state',
side_effect=[states.POWER_ON, states.POWER_ON])
def test__power_off_error_state(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.destroy = mock.Mock()
state = power._power_off(domain_mock)
get_power_mock.assert_called_with(domain_mock)
domain_mock.destroy.assert_called_once_with()
self.assertEqual(states.ERROR, state)
@mock.patch.object(power, '_get_power_state',
return_value=states.POWER_ON)
def test__power_off_error(self, get_power_mock):
domain_mock = mock.Mock()
domain_mock.destroy = mock.Mock(
side_effect=power.libvirt.libvirtError('Test'))
self.assertRaises(isd_exc.LibvirtError,
power._power_off,
domain_mock)
get_power_mock.assert_called_with(domain_mock)
def test__get_boot_device(self):
domain = FakeLibvirtDomain()
boot_dev = power._get_boot_device(domain)
self.assertEqual(power._BOOT_DEVICES_MAP[boot_devices.PXE],
boot_dev)
def test__set_boot_device(self):
conn = mock.Mock(defineXML=mock.Mock())
domain = FakeLibvirtDomain()
power._set_boot_device(
conn, domain, power._BOOT_DEVICES_MAP[boot_devices.DISK])
conn.defineXML.assert_called_once_with(mock.ANY)
def test__set_boot_device_error(self):
conn = mock.Mock(defineXML=mock.Mock(
side_effect=power.libvirt.libvirtError('Test')))
domain = FakeLibvirtDomain()
self.assertRaises(isd_exc.LibvirtError,
power._set_boot_device,
conn, domain,
power._BOOT_DEVICES_MAP[boot_devices.DISK])
class LibvirtPowerTestCase(db_base.DbTestCase):
def setUp(self):
super(LibvirtPowerTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver="fake_libvirt_fake")
driver_factory.get_driver("fake_libvirt_fake")
self.node = obj_utils.create_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('sasl'))
obj_utils.create_test_port(self.context,
node_id=self.node.id,
address='52:54:00:5c:b7:df')
def test_get_properties(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
properties = task.driver.management.get_properties()
self.assertIn('libvirt_uri', properties)
self.assertIn('sasl_username', properties)
self.assertIn('sasl_password', properties)
self.assertIn('ssh_key_filename', properties)
@mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True)
def test_validate(self, get_node_macs_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.validate(task)
get_node_macs_mock.assert_called_once_with(task)
@mock.patch.object(power.driver_utils,
'get_node_mac_addresses', autospec=True)
def test_validate_conn_miss_mac(self, get_node_mac_mock):
get_node_mac_mock.return_value = None
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.power.validate,
task)
get_node_mac_mock.assert_called_once_with(task)
@mock.patch.object(power, '_get_power_state', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_get_power_state(self, get_domain_mock, get_power_state):
domain = FakeLibvirtDomain()
get_domain_mock.return_value = domain
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.get_power_state(task)
get_domain_mock.assert_called_once_with(task)
get_power_state.assert_called_once_with(domain)
@mock.patch.object(power, '_power_on', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_power_state_on(self, get_domain_mock, power_on_mock):
domain = FakeLibvirtDomain()
get_domain_mock.return_value = domain
power_on_mock.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.set_power_state(task, states.POWER_ON)
get_domain_mock.assert_called_once_with(task)
power_on_mock.assert_called_once_with(domain)
@mock.patch.object(power, '_power_off', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_power_state_off(self, get_domain_mock, power_off_mock):
domain = FakeLibvirtDomain()
get_domain_mock.return_value = domain
power_off_mock.return_value = states.POWER_OFF
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.set_power_state(task, states.POWER_OFF)
get_domain_mock.assert_called_once_with(task)
power_off_mock.assert_called_once_with(domain)
@mock.patch.object(power, '_power_on', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_power_state_on_failure(self, get_domain_mock,
power_on_mock):
domain = FakeLibvirtDomain()
get_domain_mock.return_value = domain
power_on_mock.return_value = states.POWER_OFF
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.driver.power.set_power_state,
task, states.POWER_ON)
get_domain_mock.assert_called_once_with(task)
power_on_mock.assert_called_once_with(domain)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_power_state_invalid_state(self, get_domain_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.power.set_power_state,
task, 'wrong_state')
get_domain_mock.assert_called_once_with(task)
class LibvirtManagementTestCase(db_base.DbTestCase):
def setUp(self):
super(LibvirtManagementTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver="fake_libvirt_fake")
driver_factory.get_driver("fake_libvirt_fake")
self.node = obj_utils.create_test_node(
self.context,
driver='fake_libvirt_fake',
driver_info=_get_test_libvirt_driver_info('sasl'))
obj_utils.create_test_port(self.context,
node_id=self.node.id,
address='52:54:00:5c:b7:df')
def test_get_properties(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
properties = task.driver.management.get_properties()
self.assertIn('libvirt_uri', properties)
self.assertIn('sasl_username', properties)
self.assertIn('sasl_password', properties)
self.assertIn('ssh_key_filename', properties)
def test_get_supported_boot_devices(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
devices = task.driver.management.get_supported_boot_devices(task)
self.assertIn(boot_devices.PXE, devices)
self.assertIn(boot_devices.DISK, devices)
self.assertIn(boot_devices.CDROM, devices)
@mock.patch.object(power, '_parse_driver_info', autospec=True)
def test_validate(self, parse_info_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.management.validate(task)
parse_info_mock.assert_called_once_with(task.node)
@mock.patch.object(power, '_get_domain_by_macs',
return_value=FakeLibvirtDomain())
def test_get_boot_device_ok(self, get_domain_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
result = task.driver.management.get_boot_device(task)
get_domain_mock.assert_called_once_with(task)
self.assertEqual(power._BOOT_DEVICES_MAP[boot_devices.PXE],
result['boot_device'])
self.assertIsNone(result['persistent'])
@mock.patch.object(power, '_get_boot_device', return_value=None)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_get_boot_device_invalid(self, get_domain_mock, get_boot_dev_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
result = task.driver.management.get_boot_device(task)
self.assertIsNone(result['boot_device'])
self.assertIsNone(result['persistent'])
@mock.patch.object(power, '_set_boot_device', autospec=True)
@mock.patch.object(power, '_get_libvirt_connection', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_boot_device_ok(self, get_domain_mock, get_conn_mock,
set_boot_dev_mock):
fake_domain = FakeLibvirtDomain()
get_domain_mock.return_value = fake_domain
get_conn_mock.return_value = 'fake conn'
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.management.set_boot_device(task, boot_devices.PXE)
get_domain_mock.assert_called_once_with(task)
get_conn_mock.assert_called_once_with(
{'uuid': self.node.uuid,
'libvirt_uri': 'test+tcp://localhost:5000/test',
'sasl_password': 'admin',
'sasl_username': 'admin',
'ssh_key_filename': None})
set_boot_dev_mock.assert_called_once_with(
'fake conn', fake_domain,
power._BOOT_DEVICES_MAP[boot_devices.PXE])
@mock.patch.object(power, '_get_libvirt_connection', autospec=True)
@mock.patch.object(power, '_get_domain_by_macs', autospec=True)
def test_set_boot_device_wrong(self, get_domain_mock, get_conn_mock):
fake_domain = FakeLibvirtDomain()
get_domain_mock.return_value = fake_domain
get_conn_mock.return_value = 'fake conn'
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.management.set_boot_device,
task, boot_devices.BIOS)
get_domain_mock.assert_called_once_with(task)
get_conn_mock.assert_called_once_with(
{'uuid': self.node.uuid,
'libvirt_uri': 'test+tcp://localhost:5000/test',
'sasl_password': 'admin',
'sasl_username': 'admin',
'ssh_key_filename': None})

View File

@ -0,0 +1,7 @@
---
features:
- Addition of the Libvirt power management driver.
This driver uses python-libvirt library to connect
to hypervisor. It is fast enough and may be used
for scale testing.

View File

@ -32,6 +32,9 @@ ironic.drivers =
fake_amt_fake = ironic_staging_drivers.amt.drivers:FakeAMTFakeDriver
pxe_amt_iscsi = ironic_staging_drivers.amt.drivers:PXEAndAMTISCSIDriver
pxe_amt_agent = ironic_staging_drivers.amt.drivers:PXEAndAMTAgentDriver
pxe_libvirt_agent = ironic_staging_drivers.libvirt:PXELibvirtAgentDriver
pxe_libvirt_iscsi = ironic_staging_drivers.libvirt:PXELibvirtISCSIDriver
fake_libvirt_fake = ironic_staging_drivers.libvirt:FakeLibvirtFakeDriver
[build_sphinx]
source-dir = doc/source

View File

@ -16,3 +16,6 @@ testtools>=1.4.0 # MIT
os-testr>=0.4.1 # Apache-2.0
reno>=0.1.1 # Apache2
mock>=1.2 # BSD
# libvirt driver requires libvirt-python
libvirt-python>=1.2.5 # LGPLv2+