Merge "Add VirtualBox drivers and its modules"
This commit is contained in:
commit
53e09e0688
@ -1306,3 +1306,14 @@
|
||||
#swift_max_retries=2
|
||||
|
||||
|
||||
[virtualbox]
|
||||
|
||||
#
|
||||
# Options defined in ironic.drivers.modules.virtualbox
|
||||
#
|
||||
|
||||
# Port on which VirtualBox web service is listening. (integer
|
||||
# value)
|
||||
#port=18083
|
||||
|
||||
|
||||
|
@ -512,3 +512,8 @@ class FileSystemNotSupported(IronicException):
|
||||
|
||||
class IRMCOperationError(IronicException):
|
||||
message = _('iRMC %(operation)s failed. Reason: %(error)s')
|
||||
|
||||
|
||||
class VirtualBoxOperationFailed(IronicException):
|
||||
message = _("VirtualBox operation '%(operation)s' failed. "
|
||||
"Error: %(error)s")
|
||||
|
@ -12,11 +12,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo.utils import importutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules import agent
|
||||
from ironic.drivers.modules import ipminative
|
||||
from ironic.drivers.modules import ipmitool
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.drivers.modules import virtualbox
|
||||
|
||||
|
||||
class AgentAndIPMIToolDriver(base.BaseDriver):
|
||||
@ -76,3 +81,27 @@ class AgentAndSSHDriver(base.BaseDriver):
|
||||
self.deploy = agent.AgentDeploy()
|
||||
self.management = ssh.SSHManagement()
|
||||
self.vendor = agent.AgentVendorInterface()
|
||||
|
||||
|
||||
class AgentAndVirtualBoxDriver(base.BaseDriver):
|
||||
"""Agent + VirtualBox driver.
|
||||
|
||||
NOTE: This driver is meant only for testing environments.
|
||||
|
||||
This driver implements the `core` functionality, combining
|
||||
:class:`ironic.drivers.modules.virtualbox.VirtualBoxPower` (for power
|
||||
on/off and reboot of VirtualBox virtual machines), 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):
|
||||
if not importutils.try_import('pyremotevbox'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_("Unable to import pyremotevbox library"))
|
||||
self.power = virtualbox.VirtualBoxPower()
|
||||
self.deploy = agent.AgentDeploy()
|
||||
self.management = virtualbox.VirtualBoxManagement()
|
||||
self.vendor = agent.AgentVendorInterface()
|
||||
|
@ -36,6 +36,7 @@ from ironic.drivers.modules import pxe
|
||||
from ironic.drivers.modules import seamicro
|
||||
from ironic.drivers.modules import snmp
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.drivers.modules import virtualbox
|
||||
from ironic.drivers import utils
|
||||
|
||||
|
||||
@ -184,3 +185,16 @@ class FakeIRMCDriver(base.BaseDriver):
|
||||
reason=_("Unable to import python-scciclient library"))
|
||||
self.power = irmc_power.IRMCPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
|
||||
|
||||
class FakeVirtualBoxDriver(base.BaseDriver):
|
||||
"""Fake VirtualBox driver."""
|
||||
|
||||
def __init__(self):
|
||||
if not importutils.try_import('pyremotevbox'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_("Unable to import pyremotevbox library"))
|
||||
self.power = virtualbox.VirtualBoxPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
self.management = virtualbox.VirtualBoxManagement()
|
||||
|
367
ironic/drivers/modules/virtualbox.py
Normal file
367
ironic/drivers/modules/virtualbox.py
Normal file
@ -0,0 +1,367 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
VirtualBox Driver Modules
|
||||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import importutils
|
||||
|
||||
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 import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers import base
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
pyremotevbox = importutils.try_import('pyremotevbox')
|
||||
if pyremotevbox:
|
||||
from pyremotevbox import exception as virtualbox_exc
|
||||
from pyremotevbox import vbox as virtualbox
|
||||
|
||||
IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING = {
|
||||
boot_devices.PXE: 'Network',
|
||||
boot_devices.DISK: 'HardDisk',
|
||||
boot_devices.CDROM: 'DVD',
|
||||
}
|
||||
VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING = {v: k
|
||||
for k, v in IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.items()}
|
||||
|
||||
VIRTUALBOX_TO_IRONIC_POWER_MAPPING = {
|
||||
'PoweredOff': states.POWER_OFF,
|
||||
'Running': states.POWER_ON,
|
||||
'Error': states.ERROR
|
||||
}
|
||||
|
||||
opts = [
|
||||
cfg.IntOpt('port',
|
||||
default=18083,
|
||||
help='Port on which VirtualBox web service is listening.'),
|
||||
]
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(opts, group='virtualbox')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
REQUIRED_PROPERTIES = {
|
||||
'virtualbox_vmname': _("Name of the VM in VirtualBox. Required."),
|
||||
'virtualbox_host': _("IP address or hostname of the VirtualBox host. "
|
||||
"Required.")
|
||||
}
|
||||
|
||||
OPTIONAL_PROPERTIES = {
|
||||
'virtualbox_username': _("Username for the VirtualBox host. "
|
||||
"Default value is ''. Optional."),
|
||||
'virtualbox_password': _("Password for 'virtualbox_username'. "
|
||||
"Default value is ''. Optional."),
|
||||
'virtualbox_port': _("Port on which VirtualBox web service is listening. "
|
||||
"Optional."),
|
||||
}
|
||||
|
||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
||||
|
||||
|
||||
def _strip_virtualbox_from_param_name(param_name):
|
||||
|
||||
if param_name.startswith('virtualbox_'):
|
||||
return param_name[11:]
|
||||
else:
|
||||
return param_name
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
info = node.driver_info
|
||||
d_info = {}
|
||||
|
||||
missing_params = []
|
||||
for param in REQUIRED_PROPERTIES:
|
||||
try:
|
||||
d_info_param_name = _strip_virtualbox_from_param_name(param)
|
||||
d_info[d_info_param_name] = info[param]
|
||||
except KeyError:
|
||||
missing_params.append(param)
|
||||
|
||||
if missing_params:
|
||||
msg = (_("The following parameters are missing in driver_info: %s") %
|
||||
', '.join(missing_params))
|
||||
raise exception.MissingParameterValue(msg)
|
||||
|
||||
for param in OPTIONAL_PROPERTIES:
|
||||
if param in info:
|
||||
d_info_param_name = _strip_virtualbox_from_param_name(param)
|
||||
d_info[d_info_param_name] = info[param]
|
||||
|
||||
try:
|
||||
d_info['port'] = int(d_info.get('port', CONF.virtualbox.port))
|
||||
except ValueError:
|
||||
msg = _("'virtualbox_port' is not an integer.")
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
|
||||
return d_info
|
||||
|
||||
|
||||
def _run_virtualbox_method(node, ironic_method, vm_object_method,
|
||||
*call_args, **call_kwargs):
|
||||
"""Runs a method of pyremotevbox.vbox.VirtualMachine
|
||||
|
||||
This runs a method from pyremotevbox.vbox.VirtualMachine.
|
||||
The VirtualMachine method to be invoked and the argument(s) to be
|
||||
passed to it are to be provided.
|
||||
|
||||
:param node: an Ironic Node object.
|
||||
:param ironic_method: the Ironic method which called
|
||||
'_run_virtualbox_method'. This is used for logging only.
|
||||
:param vm_object_method: The method on the VirtualMachine object
|
||||
to be called.
|
||||
:param call_args: The args to be passed to 'vm_object_method'.
|
||||
:param call_kwargs: The kwargs to be passed to the 'vm_object_method'.
|
||||
:returns: The value returned by 'vm_object_method'
|
||||
:raises: VirtualBoxOperationFailed, if execution of 'vm_object_method'
|
||||
failed.
|
||||
:raises: InvalidParameterValue,
|
||||
- if 'vm_object_method' is not a valid 'VirtualMachine' method.
|
||||
- if some parameter(s) have invalid value(s) in the node's driver_info.
|
||||
:raises: MissingParameterValue, if some required parameter(s) are missing
|
||||
in the node's driver_info.
|
||||
:raises: pyremotevbox.exception.VmInWrongPowerState, if operation cannot
|
||||
be performed when vm is in the current power state.
|
||||
"""
|
||||
driver_info = _parse_driver_info(node)
|
||||
try:
|
||||
host = virtualbox.VirtualBoxHost(**driver_info)
|
||||
vm_object = host.find_vm(driver_info['vmname'])
|
||||
except virtualbox_exc.PyRemoteVBoxException as exc:
|
||||
LOG.error(_LE("Failed while creating a VirtualMachine object for "
|
||||
"node %(node)s. Error: %(error)s."),
|
||||
{'node_id': node.uuid, 'error': exc})
|
||||
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
|
||||
error=exc)
|
||||
|
||||
try:
|
||||
func = getattr(vm_object, vm_object_method)
|
||||
except AttributeError:
|
||||
error_msg = _("Invalid VirtualMachine method '%s' passed "
|
||||
"to '_run_virtualbox_method'.")
|
||||
raise exception.InvalidParameterValue(error_msg % vm_object_method)
|
||||
|
||||
try:
|
||||
return func(*call_args, **call_kwargs)
|
||||
except virtualbox_exc.PyRemoteVBoxException as exc:
|
||||
error_msg = _LE("'%(ironic_method)s' failed for node %(node_id)s with "
|
||||
"error: %(error)s.")
|
||||
LOG.error(error_msg, {'ironic_method': ironic_method,
|
||||
'node_id': node.uuid,
|
||||
'error': exc})
|
||||
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
|
||||
error=exc)
|
||||
|
||||
|
||||
class VirtualBoxPower(base.PowerInterface):
|
||||
|
||||
def get_properties(self):
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
def validate(self, task):
|
||||
"""Check if node.driver_info contains the required credentials.
|
||||
|
||||
: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.
|
||||
"""
|
||||
_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.
|
||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
||||
VirtualBox operation.
|
||||
"""
|
||||
power_status = _run_virtualbox_method(task.node, 'get_power_state',
|
||||
'get_power_status')
|
||||
try:
|
||||
return VIRTUALBOX_TO_IRONIC_POWER_MAPPING[power_status]
|
||||
except KeyError:
|
||||
msg = _LE("VirtualBox returned unknown state '%(state)s' for "
|
||||
"node %(node)s")
|
||||
LOG.error(msg, {'state': power_status, 'node': task.node.uuid})
|
||||
return states.ERROR
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def set_power_state(self, task, target_state):
|
||||
"""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.
|
||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
||||
VirtualBox operation.
|
||||
"""
|
||||
if target_state == states.POWER_OFF:
|
||||
_run_virtualbox_method(task.node, 'set_power_state', 'stop')
|
||||
elif target_state == states.POWER_ON:
|
||||
_run_virtualbox_method(task.node, 'set_power_state', 'start')
|
||||
elif target_state == states.REBOOT:
|
||||
self.reboot(task)
|
||||
else:
|
||||
msg = _("'set_power_state' called with invalid power "
|
||||
"state '%s'") % target_state
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def reboot(self, task):
|
||||
"""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: VirtualBoxOperationFailed, if error encountered from
|
||||
VirtualBox operation.
|
||||
"""
|
||||
_run_virtualbox_method(task.node, 'reboot', 'stop')
|
||||
_run_virtualbox_method(task.node, 'reboot', 'start')
|
||||
|
||||
|
||||
class VirtualBoxManagement(base.ManagementInterface):
|
||||
|
||||
def get_properties(self):
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
def validate(self, task):
|
||||
"""Check that 'driver_info' contains required 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: 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):
|
||||
"""Get a list of the supported boot devices.
|
||||
|
||||
:returns: A list with the supported boot devices defined
|
||||
in :mod:`ironic.common.boot_devices`.
|
||||
"""
|
||||
return list(IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.keys())
|
||||
|
||||
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: VirtualBoxOperationFailed, if error encountered from
|
||||
VirtualBox operation.
|
||||
"""
|
||||
boot_dev = _run_virtualbox_method(task.node, 'get_boot_device',
|
||||
'get_boot_device')
|
||||
persistent = True
|
||||
ironic_boot_dev = VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING.get(boot_dev,
|
||||
None)
|
||||
if not ironic_boot_dev:
|
||||
persistent = None
|
||||
msg = _LE("VirtualBox returned unknown boot device '%(device)s' "
|
||||
"for node %(node)s")
|
||||
LOG.error(msg, {'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 as VirtualBox support only
|
||||
persistent boot devices.
|
||||
: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: VirtualBoxOperationFailed, if error encountered from
|
||||
VirtualBox operation.
|
||||
"""
|
||||
# NOTE(rameshg87): VirtualBox has only persistent boot devices.
|
||||
try:
|
||||
boot_dev = IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING[device]
|
||||
except KeyError:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Invalid boot device %s specified.") % device)
|
||||
|
||||
try:
|
||||
_run_virtualbox_method(task.node, 'set_boot_device',
|
||||
'set_boot_device', boot_dev)
|
||||
except virtualbox_exc.VmInWrongPowerState as exc:
|
||||
# NOTE(rameshg87): We cannot change the boot device when the vm
|
||||
# is powered on. This is a VirtualBox limitation. We just log
|
||||
# the error silently and return because throwing error will cause
|
||||
# deploys to fail (pxe and agent deploy mechanisms change the boot
|
||||
# device after completing the deployment, when node is powered on).
|
||||
# Since this is driver that is meant only for developers, this
|
||||
# should be okay. Developers will need to set the boot device
|
||||
# manually after powering off the vm when deployment is complete.
|
||||
# This will be documented.
|
||||
LOG.error(_LE("'set_boot_device' failed for node %(node_id)s "
|
||||
"with error: %(error)s"),
|
||||
{'node_id': task.node.uuid, 'error': exc})
|
||||
|
||||
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()
|
@ -33,6 +33,7 @@ from ironic.drivers.modules import pxe
|
||||
from ironic.drivers.modules import seamicro
|
||||
from ironic.drivers.modules import snmp
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.drivers.modules import virtualbox
|
||||
from ironic.drivers import utils
|
||||
|
||||
|
||||
@ -209,3 +210,26 @@ class PXEAndIRMCDriver(base.BaseDriver):
|
||||
self.deploy = pxe.PXEDeploy()
|
||||
self.management = ipmitool.IPMIManagement()
|
||||
self.vendor = pxe.VendorPassthru()
|
||||
|
||||
|
||||
class PXEAndVirtualBoxDriver(base.BaseDriver):
|
||||
"""PXE + VirtualBox driver.
|
||||
|
||||
NOTE: This driver is meant only for testing environments.
|
||||
|
||||
This driver implements the `core` functionality, combining
|
||||
:class:`ironic.drivers.virtualbox.VirtualBoxPower` for power on/off and
|
||||
reboot of VirtualBox virtual machines, with :class:`ironic.driver.pxe.PXE`
|
||||
for image deployment. Implementations are in those respective classes;
|
||||
this class is merely the glue between them.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
if not importutils.try_import('pyremotevbox'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_("Unable to import pyremotevbox library"))
|
||||
self.power = virtualbox.VirtualBoxPower()
|
||||
self.deploy = pxe.PXEDeploy()
|
||||
self.management = virtualbox.VirtualBoxManagement()
|
||||
self.vendor = pxe.VendorPassthru()
|
||||
|
374
ironic/tests/drivers/test_virtualbox.py
Normal file
374
ironic/tests/drivers/test_virtualbox.py
Normal file
@ -0,0 +1,374 @@
|
||||
# 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 VirtualBox Driver Modules."""
|
||||
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
from pyremotevbox import exception as pyremotevbox_exc
|
||||
from pyremotevbox import vbox as pyremotevbox_vbox
|
||||
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules import virtualbox
|
||||
from ironic.tests.conductor import utils as mgr_utils
|
||||
from ironic.tests.db import base as db_base
|
||||
from ironic.tests.objects import utils as obj_utils
|
||||
|
||||
INFO_DICT = {
|
||||
'virtualbox_vmname': 'baremetal1',
|
||||
'virtualbox_host': '10.0.2.2',
|
||||
'virtualbox_username': 'username',
|
||||
'virtualbox_password': 'password',
|
||||
'virtualbox_port': 12345,
|
||||
}
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class VirtualBoxMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VirtualBoxMethodsTestCase, self).setUp()
|
||||
driver_info = INFO_DICT.copy()
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_vbox',
|
||||
driver_info=driver_info)
|
||||
|
||||
def test__parse_driver_info(self):
|
||||
info = virtualbox._parse_driver_info(self.node)
|
||||
self.assertEqual('baremetal1', info['vmname'])
|
||||
self.assertEqual('10.0.2.2', info['host'])
|
||||
self.assertEqual('username', info['username'])
|
||||
self.assertEqual('password', info['password'])
|
||||
self.assertEqual(12345, info['port'])
|
||||
|
||||
def test__parse_driver_info_missing_vmname(self):
|
||||
del self.node.driver_info['virtualbox_vmname']
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
virtualbox._parse_driver_info, self.node)
|
||||
|
||||
def test__parse_driver_info_missing_host(self):
|
||||
del self.node.driver_info['virtualbox_host']
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
virtualbox._parse_driver_info, self.node)
|
||||
|
||||
def test__parse_driver_info_invalid_port(self):
|
||||
self.node.driver_info['virtualbox_port'] = 'invalid-port'
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
virtualbox._parse_driver_info, self.node)
|
||||
|
||||
def test__parse_driver_info_missing_port(self):
|
||||
del self.node.driver_info['virtualbox_port']
|
||||
info = virtualbox._parse_driver_info(self.node)
|
||||
self.assertEqual(18083, info['port'])
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method(self, host_mock):
|
||||
host_object_mock = mock.MagicMock()
|
||||
func_mock = mock.MagicMock()
|
||||
vm_object_mock = mock.MagicMock(foo=func_mock)
|
||||
host_mock.return_value = host_object_mock
|
||||
host_object_mock.find_vm.return_value = vm_object_mock
|
||||
func_mock.return_value = 'return-value'
|
||||
|
||||
return_value = virtualbox._run_virtualbox_method(self.node,
|
||||
'some-ironic-method', 'foo', 'args', kwarg='kwarg')
|
||||
|
||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
||||
host='10.0.2.2',
|
||||
username='username',
|
||||
password='password',
|
||||
port=12345)
|
||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
||||
self.assertEqual('return-value', return_value)
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method_get_host_fails(self, host_mock):
|
||||
host_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException
|
||||
|
||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
||||
virtualbox._run_virtualbox_method,
|
||||
self.node, 'some-ironic-method', 'foo',
|
||||
'args', kwarg='kwarg')
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method_find_vm_fails(self, host_mock):
|
||||
host_object_mock = mock.MagicMock()
|
||||
host_mock.return_value = host_object_mock
|
||||
exc = pyremotevbox_exc.PyRemoteVBoxException
|
||||
host_object_mock.find_vm.side_effect = exc
|
||||
|
||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
||||
virtualbox._run_virtualbox_method,
|
||||
self.node, 'some-ironic-method', 'foo', 'args',
|
||||
kwarg='kwarg')
|
||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
||||
host='10.0.2.2',
|
||||
username='username',
|
||||
password='password',
|
||||
port=12345)
|
||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method_func_fails(self, host_mock):
|
||||
host_object_mock = mock.MagicMock()
|
||||
host_mock.return_value = host_object_mock
|
||||
func_mock = mock.MagicMock()
|
||||
vm_object_mock = mock.MagicMock(foo=func_mock)
|
||||
host_object_mock.find_vm.return_value = vm_object_mock
|
||||
func_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException
|
||||
|
||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
||||
virtualbox._run_virtualbox_method,
|
||||
self.node, 'some-ironic-method', 'foo',
|
||||
'args', kwarg='kwarg')
|
||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
||||
host='10.0.2.2',
|
||||
username='username',
|
||||
password='password',
|
||||
port=12345)
|
||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method_invalid_method(self, host_mock):
|
||||
host_object_mock = mock.MagicMock()
|
||||
host_mock.return_value = host_object_mock
|
||||
vm_object_mock = mock.MagicMock()
|
||||
host_object_mock.find_vm.return_value = vm_object_mock
|
||||
del vm_object_mock.foo
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
virtualbox._run_virtualbox_method,
|
||||
self.node, 'some-ironic-method', 'foo',
|
||||
'args', kwarg='kwarg')
|
||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
||||
host='10.0.2.2',
|
||||
username='username',
|
||||
password='password',
|
||||
port=12345)
|
||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
||||
|
||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost')
|
||||
def test__run_virtualbox_method_vm_wrong_power_state(self, host_mock):
|
||||
host_object_mock = mock.MagicMock()
|
||||
host_mock.return_value = host_object_mock
|
||||
func_mock = mock.MagicMock()
|
||||
vm_object_mock = mock.MagicMock(foo=func_mock)
|
||||
host_object_mock.find_vm.return_value = vm_object_mock
|
||||
func_mock.side_effect = pyremotevbox_exc.VmInWrongPowerState
|
||||
|
||||
# _run_virtualbox_method() doesn't catch VmInWrongPowerState and
|
||||
# lets caller handle it.
|
||||
self.assertRaises(pyremotevbox_exc.VmInWrongPowerState,
|
||||
virtualbox._run_virtualbox_method,
|
||||
self.node, 'some-ironic-method', 'foo',
|
||||
'args', kwarg='kwarg')
|
||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
||||
host='10.0.2.2',
|
||||
username='username',
|
||||
password='password',
|
||||
port=12345)
|
||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
||||
|
||||
|
||||
class VirtualBoxPowerTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VirtualBoxPowerTestCase, self).setUp()
|
||||
driver_info = INFO_DICT.copy()
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_vbox',
|
||||
driver_info=driver_info)
|
||||
|
||||
def test_get_properties(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
properties = task.driver.power.get_properties()
|
||||
|
||||
self.assertIn('virtualbox_vmname', properties)
|
||||
self.assertIn('virtualbox_host', properties)
|
||||
|
||||
@mock.patch.object(virtualbox, '_parse_driver_info')
|
||||
def test_validate(self, parse_info_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.power.validate(task)
|
||||
parse_info_mock.assert_called_once_with(task.node)
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_get_power_state(self, run_method_mock):
|
||||
run_method_mock.return_value = 'PoweredOff'
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
power_state = task.driver.power.get_power_state(task)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'get_power_state',
|
||||
'get_power_status')
|
||||
self.assertEqual(states.POWER_OFF, power_state)
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_get_power_state_invalid_state(self, run_method_mock):
|
||||
run_method_mock.return_value = 'invalid-state'
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
power_state = task.driver.power.get_power_state(task)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'get_power_state',
|
||||
'get_power_status')
|
||||
self.assertEqual(states.ERROR, power_state)
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_power_state_off(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.power.set_power_state(task, states.POWER_OFF)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'set_power_state',
|
||||
'stop')
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_power_state_on(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.power.set_power_state(task, states.POWER_ON)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'set_power_state',
|
||||
'start')
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_power_state_reboot(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.power.set_power_state(task, states.REBOOT)
|
||||
run_method_mock.assert_any_call(task.node,
|
||||
'reboot',
|
||||
'stop')
|
||||
run_method_mock.assert_any_call(task.node,
|
||||
'reboot',
|
||||
'start')
|
||||
|
||||
def test_set_power_state_invalid_state(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
task.driver.power.set_power_state,
|
||||
task, 'invalid-state')
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_reboot(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.power.reboot(task)
|
||||
run_method_mock.assert_any_call(task.node,
|
||||
'reboot',
|
||||
'stop')
|
||||
run_method_mock.assert_any_call(task.node,
|
||||
'reboot',
|
||||
'start')
|
||||
|
||||
|
||||
class VirtualBoxManagementTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VirtualBoxManagementTestCase, self).setUp()
|
||||
driver_info = INFO_DICT.copy()
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_vbox',
|
||||
driver_info=driver_info)
|
||||
|
||||
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('virtualbox_vmname', properties)
|
||||
self.assertIn('virtualbox_host', properties)
|
||||
|
||||
@mock.patch.object(virtualbox, '_parse_driver_info')
|
||||
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)
|
||||
|
||||
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()
|
||||
self.assertIn(boot_devices.PXE, devices)
|
||||
self.assertIn(boot_devices.DISK, devices)
|
||||
self.assertIn(boot_devices.CDROM, devices)
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_get_boot_device_ok(self, run_method_mock):
|
||||
run_method_mock.return_value = 'Network'
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
ret_val = task.driver.management.get_boot_device(task)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'get_boot_device',
|
||||
'get_boot_device')
|
||||
self.assertEqual(boot_devices.PXE, ret_val['boot_device'])
|
||||
self.assertTrue(ret_val['persistent'])
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_get_boot_device_invalid(self, run_method_mock):
|
||||
run_method_mock.return_value = 'invalid-boot-device'
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
ret_val = task.driver.management.get_boot_device(task)
|
||||
self.assertIsNone(ret_val['boot_device'])
|
||||
self.assertIsNone(ret_val['persistent'])
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_boot_device_ok(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.management.set_boot_device(task, boot_devices.PXE)
|
||||
run_method_mock.assert_called_once_with(task.node,
|
||||
'set_boot_device',
|
||||
'set_boot_device',
|
||||
'Network')
|
||||
|
||||
@mock.patch.object(virtualbox, 'LOG')
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_boot_device_wrong_power_state(self, run_method_mock,
|
||||
log_mock):
|
||||
run_method_mock.side_effect = pyremotevbox_exc.VmInWrongPowerState
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.management.set_boot_device(task, boot_devices.PXE)
|
||||
log_mock.error.assert_called_once_with(mock.ANY, mock.ANY)
|
||||
|
||||
@mock.patch.object(virtualbox, '_run_virtualbox_method')
|
||||
def test_set_boot_device_invalid(self, run_method_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
task.driver.management.set_boot_device,
|
||||
task, 'invalid-boot-device')
|
||||
|
||||
def test_get_sensors_data(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.assertRaises(NotImplementedError,
|
||||
task.driver.management.get_sensors_data,
|
||||
task)
|
@ -161,3 +161,13 @@ if not scciclient:
|
||||
# external library has been mocked
|
||||
if 'ironic.drivers.modules.irmc' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.modules.irmc'])
|
||||
|
||||
pyremotevbox = importutils.try_import('pyremotevbox')
|
||||
if not pyremotevbox:
|
||||
pyremotevbox = mock.MagicMock()
|
||||
pyremotevbox.exception = mock.MagicMock()
|
||||
pyremotevbox.exception.PyRemoteVBoxException = Exception
|
||||
pyremotevbox.exception.VmInWrongPowerState = Exception
|
||||
sys.modules['pyremotevbox'] = pyremotevbox
|
||||
if 'ironic.drivers.modules.virtualbox' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.modules.virtualbox'])
|
||||
|
@ -38,6 +38,7 @@ ironic.drivers =
|
||||
agent_ipmitool = ironic.drivers.agent:AgentAndIPMIToolDriver
|
||||
agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver
|
||||
agent_ssh = ironic.drivers.agent:AgentAndSSHDriver
|
||||
agent_vbox = ironic.drivers.agent:AgentAndVirtualBoxDriver
|
||||
fake = ironic.drivers.fake:FakeDriver
|
||||
fake_agent = ironic.drivers.fake:FakeAgentDriver
|
||||
fake_ipmitool = ironic.drivers.fake:FakeIPMIToolDriver
|
||||
@ -50,10 +51,12 @@ ironic.drivers =
|
||||
fake_drac = ironic.drivers.fake:FakeDracDriver
|
||||
fake_snmp = ironic.drivers.fake:FakeSNMPDriver
|
||||
fake_irmc = ironic.drivers.fake:FakeIRMCDriver
|
||||
fake_vbox = ironic.drivers.fake:FakeVirtualBoxDriver
|
||||
iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver
|
||||
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
|
||||
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
||||
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
||||
pxe_vbox = ironic.drivers.pxe:PXEAndVirtualBoxDriver
|
||||
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
|
||||
pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver
|
||||
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
||||
|
Loading…
Reference in New Issue
Block a user