Merge "Ovirt support"

This commit is contained in:
Zuul 2017-11-27 11:30:49 +00:00 committed by Gerrit Code Review
commit 5c3dc1dded
8 changed files with 499 additions and 0 deletions

View File

@ -37,3 +37,7 @@ class LibvirtError(exception.IronicException):
class InvalidIPMITimestamp(exception.IronicException):
pass
class oVirtError(exception.IronicException):
message = _("oVirt call failed: %(err)s.")

View File

@ -0,0 +1,37 @@
# -*- encoding: utf-8 -*-
#
# 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.
"""
PXE Driver and supporting meta-classes.
"""
from ironic.drivers import generic
from ironic_staging_drivers.ovirt import ovirt
class oVirtHardware(generic.GenericHardware):
"""oVirt hardware type.
Uses oVirt for power and management.
"""
@property
def supported_management_interfaces(self):
"""List of supported management interfaces."""
return [ovirt.oVirtManagement]
@property
def supported_power_interfaces(self):
"""List of supported power interfaces."""
return [ovirt.oVirtPower]

View File

@ -0,0 +1,3 @@
OVIRT_DEB_PACKAGES="libcurl4-openssl-dev libssl-dev libxml2-dev"
install_package $OVIRT_DEB_PACKAGES

View File

@ -0,0 +1,339 @@
# 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']:
raise exception.MissingParameterValue(
_("%(prop)s is not set either in the configuration or"
"in the node's driver_info"))
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:
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.
"""
driver_info = _parse_driver_info(task.node)
vm_name = driver_info['ovirt_vm_name']
vm = _getvm(driver_info)
try:
vm.reboot()
except sdk.Error as e:
LOG.error("Could not restart VM vm %(name)s "
"got error: %(error)s", {'name': vm_name, 'error': e})
raise staging_exception.oVirtError(err=e)
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()

View File

@ -0,0 +1 @@
ovirt-engine-sdk-python>=4.0.0 # Apache-2.0

View File

@ -0,0 +1,112 @@
# 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 oVirt driver module."""
import time
from ironic.common import boot_devices
from ironic.common import states
from ironic.conductor import task_manager
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
import mock
from ironic_staging_drivers.ovirt import ovirt as ovirt_power
def _ovirt_info():
driver_info = {'ovirt_address': '127.0.0.1',
'ovirt_username': 'jhendrix@internal',
'ovirt_password': 'changeme',
'ovirt__insecure': True,
'ovirt_ca_file': None,
'ovirt_vm_name': 'jimi'}
return driver_info
@mock.patch.object(time, 'sleep', lambda *_: None)
class oVirtDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(oVirtDriverTestCase, self).setUp()
self.config(enabled_power_interfaces='staging-ovirt',
enabled_management_interfaces='staging-ovirt')
namespace = 'ironic.hardware.types'
mgr_utils.mock_the_extension_manager(driver='staging-ovirt',
namespace=namespace)
self.node = obj_utils.create_test_node(self.context,
driver='staging-ovirt',
driver_info=_ovirt_info())
self.port = obj_utils.create_test_port(self.context,
node_id=self.node.id)
def test__parse_parameters(self):
params = ovirt_power._parse_driver_info(self.node)
self.assertEqual('127.0.0.1', params['ovirt_address'])
self.assertEqual('jhendrix@internal', params['ovirt_username'])
self.assertEqual('changeme', params['ovirt_password'])
self.assertEqual('jimi', params['ovirt_vm_name'])
def test_get_properties(self):
expected = list(ovirt_power.PROPERTIES.keys())
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
driver_properties = [prop for prop in task.driver.get_properties()
if prop in expected]
self.assertEqual(sorted(expected), sorted(driver_properties))
@mock.patch.object(ovirt_power.oVirtPower, 'set_power_state',
autospec=True, spec_set=True)
def test_set_power_state_power_on(self, mock_power):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.power.set_power_state(task, states.POWER_ON)
mock_power.assert_called_once_with(task.driver.power, task,
states.POWER_ON)
@mock.patch.object(ovirt_power.oVirtPower, 'set_power_state',
autospec=True, spec_set=True)
def test_set_power_state_power_off(self, mock_power):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.power.set_power_state(task, states.POWER_OFF)
mock_power.assert_called_once_with(task.driver.power, task,
states.POWER_OFF)
def test_get_supported_power_states(self):
with task_manager.acquire(
self.context, self.node.uuid, shared=True) as task:
pstates = task.driver.power.get_supported_power_states(task)
self.assertEqual([states.POWER_ON, states.POWER_OFF,
states.REBOOT], pstates)
def test_get_supported_boot_devices(self):
with task_manager.acquire(
self.context, self.node.uuid, shared=True) as task:
bdevices = task.driver.management.get_supported_boot_devices(task)
self.assertEqual([boot_devices.CDROM, boot_devices.DISK,
boot_devices.PXE], bdevices)
@mock.patch.object(ovirt_power.oVirtManagement, 'get_boot_device',
return_value='hd')
def test_get_boot_device(self, mock_management):
with task_manager.acquire(self.context, self.node.uuid) as task:
boot_dev = task.driver.management.get_boot_device(task)
self.assertEqual('hd', boot_dev)
@mock.patch.object(ovirt_power.oVirtManagement, 'set_boot_device',
autospec=True, spec_set=True)
def test_set_boot_device(self, mock_power):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.management.set_boot_device(task, boot_devices.DISK)
mock_power.assert_called_once_with(task.driver.management, task,
boot_devices.DISK)

View File

@ -49,11 +49,13 @@ ironic.hardware.interfaces.deploy =
ironic.hardware.interfaces.management =
staging-amt = ironic_staging_drivers.amt.management:AMTManagement
staging-libvirt = ironic_staging_drivers.libvirt.power:LibvirtManagement
staging-ovirt = ironic_staging_drivers.ovirt.ovirt:oVirtManagement
ironic.hardware.interfaces.power =
staging-amt = ironic_staging_drivers.amt.power:AMTPower
staging-iboot = ironic_staging_drivers.iboot.power:IBootPower
staging-libvirt = ironic_staging_drivers.libvirt.power:LibvirtPower
staging-ovirt = ironic_staging_drivers.ovirt.ovirt:oVirtPower
staging-wol = ironic_staging_drivers.wol.power:WakeOnLanPower
ironic.hardware.interfaces.vendor =
@ -65,6 +67,7 @@ ironic.hardware.types =
staging-iboot = ironic_staging_drivers.iboot:IBootHardware
staging-nm = ironic_staging_drivers.intel_nm:IntelNMHardware
staging-libvirt = ironic_staging_drivers.libvirt:LibvirtHardware
staging-ovirt = ironic_staging_drivers.ovirt:oVirtHardware
staging-wol = ironic_staging_drivers.wol:WOLHardware
[build_sphinx]