Restructuring driver API and inheritance.

Based on discussions during and after the Ironic team meeting on June
03, regarding support for substantially different driver work flows,
this is a re-working of the internal driver API.

tl;dr: The strict separation of "control" and "deploy" driver was an
       artefact of the ipmi + pxe implementation used in nova-baremetal,
       and does not map on to all drivers. Furthermore, the prior
       implementation did not accurately represent the separation of
       "core", "standard", and "vendor-specific" driver functionality.

These changes impact the v1 API structure, but since that is largely not
implemented yet, this change does not attempt to affect the public API
itself.

Highlights:
- No more deploy + control driver; nodes have one and only one driver.
  This drops the deploy_driver and deploy_info parameters,
  and renames control_driver -> driver, and control_info -> driver_info.
- Interfaces for core, standard, and vendor functionality now clearly
  defined in the driver API.
- Improve Fake driver to demonstrate use of interfaces.
- Convert IPMI and SSH driver classes into interfaces, and move to
  drivers/modules/ directory.
- Stub for the pxe interfaces.
- Stub implementations of pxe+ipmi and pxe+ssh drivers.
- driver_info field uses more standard names, but requires
  driver-specific data to be in a nested object. Examples in
  tests/db/utils.py as before.

A separate doc change will follow this to update the API v1 spec.

Also includes some cosmetic cleanup of test_ssh.py and test_ipmi.py.

Change-Id: I057ede8e07b1b57010e81ef58415debe0ba8b934
This commit is contained in:
Devananda van der Veen 2013-06-04 12:19:18 -07:00
parent 9d80a7171b
commit 59d5bea14a
24 changed files with 1091 additions and 671 deletions

View File

@ -92,11 +92,10 @@ class Connection(object):
'uuid': uuidutils.generate_uuid(),
'instance_uuid': None,
'task_state': states.NOSTATE,
'control_driver': 'ipmi',
'control_info': { ... },
'deploy_driver': 'pxe',
'deploy_info': { ... },
'driver': 'pxe_ipmi',
'driver_info': { ... },
'properties': { ... },
'extra': { ... },
}
:returns: A node.
"""
@ -131,11 +130,10 @@ class Connection(object):
:param node: The id or uuid of a node.
:param values: Dict of values to update.
May be a partial list, eg. when setting the
properties for a single driver. For example:
properties for a driver. For example:
{
'deploy_driver': 'my-vendor-driver',
'deploy_info':
'driver_info':
{
'my-field-1': val1,
'my-field-2': val2,

View File

@ -0,0 +1,34 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 sqlalchemy import Table, MetaData
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
nodes = Table('nodes', meta, autoload=True)
nodes.c.deploy_driver.drop()
nodes.c.deploy_info.drop()
nodes.c.control_driver.alter(name='driver')
nodes.c.control_info.alter(name='driver_info')
def downgrade(migrate_engine):
raise NotImplementedError('Downgrade from version 005 is unsupported.')

View File

@ -98,10 +98,8 @@ class Node(Base):
task_start = Column(DateTime, nullable=True)
task_state = Column(String(15))
properties = Column(JSONEncodedDict)
control_driver = Column(String(15))
control_info = Column(JSONEncodedDict)
deploy_driver = Column(String(15))
deploy_info = Column(JSONEncodedDict)
driver = Column(String(15))
driver_info = Column(JSONEncodedDict)
reservation = Column(String(255), nullable=True)
extra = Column(JSONEncodedDict)

View File

@ -15,87 +15,226 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
Base classes for drivers.
All methods take, at minimum, a TaskManager resource and a single Node.
Abstract base classes for drivers.
"""
import abc
class DeployDriver(object):
"""Base class for image deployment drivers."""
class BaseDriver(object):
"""Base class for all drivers.
Defines the `core`, `standardized`, and `vendor-specific` interfaces for
drivers. Any loadable driver must implement all `core` interfaces.
Actual implementation may instantiate one or more classes, as long as
the interfaces are appropriate.
"""
__metaclass__ = abc.ABCMeta
power = None
"""`Core` attribute for managing power state.
A reference to an instance of :class:PowerInterface.
"""
deploy = None
"""`Core` attribute for managing deployments.
A reference to an instance of :class:DeployInterface.
"""
console = None
"""`Standard` attribute for managing console access.
A reference to an instance of :class:ConsoleInterface.
May be None, if unsupported by a driver.
"""
rescue = None
"""`Standard` attribute for accessing rescue features.
A reference to an instance of :class:RescueInterface.
May be None, if unsupported by a driver.
"""
vendor = None
"""Attribute for accessing any vendor-specific extensions.
A reference to an instance of :class:VendorInterface.
May be None, if the driver does not implement any vendor extensions.
"""
@abc.abstractmethod
def __init__(self):
pass
class DeployInterface(object):
"""Interface for deploy-related actions."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self):
"""Constructor."""
def validate(self, node):
"""Validate the driver-specific Node deployment info.
@abc.abstractmethod
def validate_driver_info(cls, node):
"""Validate the driver-specific Node info.
This method validates whether the 'deploy_info' property of the
supplied nodes contains the required information for this driver to
manage the nodes.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to
deploy images to the node.
:param node: a single Node to validate.
:returns: True or False, depending on capabilities.
"""
@abc.abstractmethod
def activate_bootloader(self, task, node):
"""Prepare the bootloader for this deployment."""
def deploy(self, task, node):
"""Perform a deployment to a node.
Given a node with complete metadata, deploy the indicated image
to the node.
:param task: a TaskManager instance.
:param node: the Node to act upon.
"""
@abc.abstractmethod
def deactivate_bootloader(self, task, node):
"""Tear down the bootloader for this deployment."""
def tear_down(self, task, node):
"""Tear down a previous deployment.
@abc.abstractmethod
def activate_node(self, task, node):
"""Perform post-power-on operations for this deployment."""
Given a node that has been previously deployed to,
do all cleanup and tear down necessary to "un-deploy" that node.
@abc.abstractmethod
def deactivate_node(self, task, node):
"""Perform pre-power-off operations for this deployment."""
:param task: a TaskManager instance.
:param node: the Node to act upon.
"""
class ControlDriver(object):
"""Base class for node control drivers."""
class PowerInterface(object):
"""Interface for power-related actions."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self):
"""Constructor."""
def validate(self, node):
"""Validate the driver-specific Node power info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to
manage the power state of the node.
:param node: a single Node to validate.
:returns: True or False, depending on capabilities.
"""
@abc.abstractmethod
def validate_driver_info(cls, node):
"""Validate the driver-specific Node info.
def get_power_state(self, task, node):
"""Return the power state of the node.
This method validates whether the 'control_info' property of the
supplied nodes contains the required information for this driver to
manage the nodes.
TODO
"""
@abc.abstractmethod
def set_power_state(self, task, node, power_state):
"""Set the power state of the node.
TODO
"""
@abc.abstractmethod
def reboot(self, task, node):
"""Perform a hard reboot of the node.
TODO
"""
class ConsoleInterface(object):
"""Interface for console-related actions."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def validate(self, node):
"""Validate the driver-specific Node console info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to
provide console access to the Node.
:param node: a single Node to validate.
:returns: True or False, depending on capabilities.
"""
@abc.abstractmethod
def start_console(self, task, node):
"""Start a remote console for the nodes."""
"""Start a remote console for the node.
TODO
"""
@abc.abstractmethod
def stop_console(self, task, node):
"""Stop the remote console session for the nodes."""
"""Stop the remote console session for the node.
TODO
"""
class RescueInterface(object):
"""Interface for rescue-related actions."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def get_power_state(self, task, node):
"""Return the power state of the nodes."""
def validate(self, node):
"""Validate the rescue info stored in the node' properties."""
@abc.abstractmethod
def set_power_state(self, task, node):
"""Set the power state of the nodes."""
def rescue(self, task, node):
"""Boot the node into a rescue environment.
TODO
"""
@abc.abstractmethod
def reboot(self, task, node):
"""Perform a hard reboot of the nodes."""
def unrescue(self, task, node):
"""Tear down the rescue environment, and return to normal.
TODO
"""
class VendorInterface(object):
"""Interface for all vendor passthru functionality.
Additional vendor- or driver-specific capabilities should be implemented as
private methods and invoked from vendor_passthru().
"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def validate(self, node):
"""Validate vendor-specific info stored in the node' properties."""
@abc.abstractmethod
def vendor_passthru(self, task, node, *args, **kwargs):
"""Receive RPC requests for vendor-specific actions.
TODO: Mechanism for vendor_passthru and, in particular, where the
the interpretation of API requests will live, is still being
discussed.
One possible implementation of this method might look like::
method = args.get('method')
if not method:
raise ...
if hasattr(self, method):
callable = getattr(self, method)
self.callable(task, node, *args, **kwargs)
else:
raise ...
"""

View File

@ -19,49 +19,42 @@ Fake drivers used in testing.
"""
from ironic.drivers import base
from ironic.drivers.modules import fake
from ironic.drivers.modules import ipmi
from ironic.drivers.modules import pxe
from ironic.drivers.modules import ssh
class FakeDeployDriver(base.DeployDriver):
class FakeDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def __init__(self):
pass
def validate_driver_info(self, node):
return True
def activate_bootloader(self, task, node):
pass
def deactivate_bootloader(self, task, node):
pass
def activate_node(self, task, node):
pass
def deactivate_node(self, task, node):
pass
self.power = fake.FakePower()
self.deploy = fake.FakeDeploy()
self.vendor = fake.FakeVendor()
class FakeControlDriver(base.ControlDriver):
class FakeIPMIDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def __init__(self):
pass
self.power = ipmi.IPMIPower()
self.deploy = fake.FakeDeploy()
self.vendor = self.power
def validate_driver_info(self, node):
return True
def start_console(self, task, node):
pass
class FakePXEDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def stop_console(self, task, node):
pass
def __init__(self):
self.power = fake.FakePower()
self.deploy = pxe.PXEDeploy()
self.rescue = self.deploy
def attach_console(self, task, node):
pass
def get_power_state(self, task, node):
pass
class FakeSSHDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def set_power_state(self, task, node):
pass
def reboot(self, task, node):
pass
def __init__(self):
self.power = ssh.SSHPower()
self.deploy = fake.FakeDeploy()

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,78 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
"""
Fake driver interfaces used in testing.
"""
from ironic.common import exception
from ironic.common import states
from ironic.drivers import base
class FakePower(base.PowerInterface):
"""Example implementation of a simple power interface."""
def validate(self, node):
return True
def get_power_state(self, task, node):
return states.NOSTATE
def set_power_state(self, task, node, power_state):
pass
def reboot(self, task, node):
pass
class FakeDeploy(base.DeployInterface):
"""Example imlementation of a deploy interface that uses a
separate power interface.
"""
def validate(self, node):
return True
def deploy(self, task, node):
pass
def tear_down(self, task, node):
pass
class FakeVendor(base.VendorInterface):
"""Example implementation of a vendor passthru interface."""
def validate(self, node):
return True
def _foo(self, task, node, bar):
return True if bar else False
def vendor_passthru(self, task, node, *args, **kwargs):
method = kwargs.get('method')
if not method:
raise exception.IronicException(_(
"Invalid vendor passthru, no 'method' specified."))
if method == 'foo':
bar = kwargs.get('bar')
return self._foo(task, node, bar)
else:
raise exception.IronicException(_(
"Unsupported method (%s) passed through to vendor extension.")
% method)

View File

@ -60,7 +60,8 @@ LOG = logging.getLogger(__name__)
VALID_BOOT_DEVICES = ['pxe', 'disk', 'safe', 'cdrom', 'bios']
# TODO(deva): use a contextmanager for this, and port it to nova.
# TODO(deva): replace this with remove_path_on_error once that is available
# https://review.openstack.org/#/c/31030
def _make_password_file(password):
fd, path = tempfile.mkstemp()
os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR)
@ -69,37 +70,38 @@ def _make_password_file(password):
return path
def _parse_control_info(node):
info = json.loads(node.get('control_info', ''))
address = info.get('ipmi_address', None)
user = info.get('ipmi_username', None)
password = info.get('ipmi_password', None)
port = info.get('ipmi_terminal_port', None)
def _parse_driver_info(node):
driver_info = json.loads(node.get('driver_info', ''))
ipmi_info = driver_info.get('ipmi')
address = ipmi_info.get('address', None)
username = ipmi_info.get('username', None)
password = ipmi_info.get('password', None)
port = ipmi_info.get('terminal_port', None)
if not address or not user or not password:
if not address or not username or not password:
raise exception.InvalidParameterValue(_(
"IPMI credentials not supplied to IPMI driver."))
return {
'address': address,
'user': user,
'username': username,
'password': password,
'port': port,
'uuid': node.get('uuid')
}
def _exec_ipmitool(c_info, command):
def _exec_ipmitool(driver_info, command):
args = ['ipmitool',
'-I',
'lanplus',
'-H',
c_info['address'],
driver_info['address'],
'-U',
c_info['user'],
driver_info['username'],
'-f']
try:
pwfile = _make_password_file(c_info['password'])
pwfile = _make_password_file(driver_info['password'])
args.append(pwfile)
args.extend(command.split(" "))
out, err = utils.execute(*args, attempts=3)
@ -110,7 +112,7 @@ def _exec_ipmitool(c_info, command):
utils.delete_if_exists(pwfile)
def _power_on(c_info):
def _power_on(driver_info):
"""Turn the power to this node ON."""
# use mutable objects so the looped method can change them
@ -120,7 +122,7 @@ def _power_on(c_info):
def _wait_for_power_on(state, retries):
"""Called at an interval until the node's power is on."""
state[0] = _power_status(c_info)
state[0] = _power_status(driver_info)
if state[0] == states.POWER_ON:
raise loopingcall.LoopingCallDone()
@ -129,11 +131,11 @@ def _power_on(c_info):
raise loopingcall.LoopingCallDone()
try:
retries[0] += 1
_exec_ipmitool(c_info, "power on")
_exec_ipmitool(driver_info, "power on")
except Exception:
# Log failures but keep trying
LOG.warning(_("IPMI power on failed for node %s.")
% c_info['uuid'])
% driver_info['uuid'])
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_on,
state, retries)
@ -141,7 +143,7 @@ def _power_on(c_info):
return state[0]
def _power_off(c_info):
def _power_off(driver_info):
"""Turn the power to this node OFF."""
# use mutable objects so the looped method can change them
@ -151,7 +153,7 @@ def _power_off(c_info):
def _wait_for_power_off(state, retries):
"""Called at an interval until the node's power is off."""
state[0] = _power_status(c_info)
state[0] = _power_status(driver_info)
if state[0] == states.POWER_OFF:
raise loopingcall.LoopingCallDone()
@ -160,11 +162,11 @@ def _power_off(c_info):
raise loopingcall.LoopingCallDone()
try:
retries[0] += 1
_exec_ipmitool(c_info, "power off")
_exec_ipmitool(driver_info, "power off")
except Exception:
# Log failures but keep trying
LOG.warning(_("IPMI power off failed for node %s.")
% c_info['uuid'])
% driver_info['uuid'])
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_off,
state=state, retries=retries)
@ -172,8 +174,8 @@ def _power_off(c_info):
return state[0]
def _power_status(c_info):
out_err = _exec_ipmitool(c_info, "power status")
def _power_status(driver_info):
out_err = _exec_ipmitool(driver_info, "power status")
if out_err[0] == "Chassis Power is on\n":
return states.POWER_ON
elif out_err[0] == "Chassis Power is off\n":
@ -182,41 +184,30 @@ def _power_status(c_info):
return states.ERROR
class IPMIPowerDriver(base.ControlDriver):
"""Generic IPMI Power Driver
class IPMIPower(base.PowerInterface):
This ControlDriver class provides mechanism for controlling the power state
of physical hardware via IPMI calls. It also provides console access for
some supported hardware.
NOTE: This driver does not currently support multi-node operations.
"""
def __init__(self):
pass
def validate_driver_info(self, node):
"""Check that node['control_info'] contains the requisite fields."""
def validate(self, node):
"""Check that node['driver_info'] contains IPMI credentials."""
try:
_parse_control_info(node)
_parse_driver_info(node)
except exception.InvalidParameterValue:
return False
return True
def get_power_state(self, task, node):
"""Get the current power state."""
c_info = _parse_control_info(node)
return _power_status(c_info)
driver_info = _parse_driver_info(node)
return _power_status(driver_info)
@task_manager.require_exclusive_lock
def set_power_state(self, task, node, pstate):
"""Turn the power on or off."""
c_info = _parse_control_info(node)
driver_info = _parse_driver_info(node)
if pstate == states.POWER_ON:
state = _power_on(c_info)
state = _power_on(driver_info)
elif pstate == states.POWER_OFF:
state = _power_off(c_info)
state = _power_off(driver_info)
else:
raise exception.IronicException(_(
"set_power_state called with invalid power state."))
@ -227,18 +218,18 @@ class IPMIPowerDriver(base.ControlDriver):
@task_manager.require_exclusive_lock
def reboot(self, task, node):
"""Cycles the power to a node."""
c_info = _parse_control_info(node)
_power_off(c_info)
state = _power_on(c_info)
driver_info = _parse_driver_info(node)
_power_off(driver_info)
state = _power_on(driver_info)
if state != states.POWER_ON:
raise exception.PowerStateFailure(pstate=states.POWER_ON)
@task_manager.require_exclusive_lock
def set_boot_device(self, task, node, device, persistent=False):
def _set_boot_device(self, task, node, device, persistent=False):
"""Set the boot device for a node.
:param task: TaskManager context.
:param task: a TaskManager instance.
:param node: The Node.
:param device: Boot device. One of [pxe, disk, cdrom, safe, bios].
:param persistent: Whether to set next-boot, or make the change
@ -253,19 +244,9 @@ class IPMIPowerDriver(base.ControlDriver):
cmd = "chassis bootdev %s" % device
if persistent:
cmd = cmd + " options=persistent"
c_info = _parse_control_info(node)
driver_info = _parse_driver_info(node)
try:
out, err = _exec_ipmitool(c_info, cmd)
out, err = _exec_ipmitool(driver_info, cmd)
# TODO(deva): validate (out, err) and add unit test for failure
except Exception:
raise exception.IPMIFailure(cmd=cmd)
# TODO(deva): port start_console
def start_console(self, task, node):
raise exception.IronicException(_(
"start_console is not supported by IPMIPowerDriver."))
# TODO(deva): port stop_console
def stop_console(self, task, node):
raise exception.IronicException(_(
"stop_console is not supported by IPMIPowerDriver."))

View File

@ -0,0 +1,64 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 base
class PXEDeploy(base.DeployInterface):
"""PXE Deploy Interface: just a stub until the real driver is ported."""
def validate(self, nodes):
pass
def deploy(self, task, nodes):
pass
def tear_down(self, task, nodes):
pass
class PXERescue(base.RescueInterface):
"""PXE Rescue Interface: just a stub until the real driver is ported."""
def validate(self, nodes):
pass
def rescue(self, task, nodes):
pass
def unrescue(self, task, nodes):
pass
class IPMIVendorPassthru(base.VendorInterface):
"""Interface to mix IPMI and PXE vendor-specific interfaces."""
def validate(self, node):
pass
def vendor_passthru(self, task, node, *args, **kwargs):
method = kwargs.get('method')
if method == 'set_boot_device':
return node.driver.vendor._set_boot_device(
task, node,
args.get('device'),
args.get('persistent'))
else:
return

View File

@ -25,7 +25,6 @@ For use in dev and test environments.
Currently supported environments are:
Virtual Box (vbox)
Virsh (virsh)
"""
import os
@ -109,17 +108,19 @@ def _exec_ssh_command(ssh_obj, command):
return (stdout, stderr)
def _parse_control_info(node):
info = json.loads(node.get('control_info', ''))
host = info.get('ssh_host', None)
username = info.get('ssh_user', None)
password = info.get('ssh_pass', None)
port = info.get('ssh_port', 22)
key_filename = info.get('ssh_key', None)
virt_type = info.get('virt_type', None)
def _parse_driver_info(node):
driver_info = json.loads(node.get('driver_info', ''))
ssh_info = driver_info.get('ssh')
address = ssh_info.get('address', None)
username = ssh_info.get('username', None)
password = ssh_info.get('password', None)
port = ssh_info.get('port', 22)
key_filename = ssh_info.get('key_filename', None)
virt_type = ssh_info.get('virt_type', None)
# NOTE(deva): we map 'address' from API to 'host' for common utils
res = {
'host': host,
'host': address,
'username': username,
'port': port,
'virt_type': virt_type,
@ -136,15 +137,16 @@ def _parse_control_info(node):
"SSHPowerDriver unknown virt_type (%s).") % cmd_set)
res['cmd_set'] = cmd_set
if not host or not username:
if not address or not username:
raise exception.InvalidParameterValue(_(
"SSHPowerDriver requires both ssh_host and ssh_user be set."))
"SSHPowerDriver requires both address and username be set."))
if password:
res['password'] = password
else:
if not key_filename:
raise exception.InvalidParameterValue(_(
"SSHPowerDriver requires either ssh_pass or ssh_key be set."))
"SSHPowerDriver requires either password or "
"key_filename be set."))
if not os.path.isfile(key_filename):
raise exception.FileNotFound(file_path=key_filename)
res['key_filename'] = key_filename
@ -152,15 +154,15 @@ def _parse_control_info(node):
return res
def _get_power_status(ssh_obj, c_info):
def _get_power_status(ssh_obj, driver_info):
"""Returns a node's current power state."""
power_state = None
cmd_to_exec = c_info['cmd_set']['list_running']
cmd_to_exec = driver_info['cmd_set']['list_running']
running_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
# Command should return a list of running vms. If the current node is
# not listed then we can assume it is not powered on.
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
node_name = _get_hosts_name_for_node(ssh_obj, driver_info)
if node_name:
for node in running_list:
if node_name in node:
@ -175,23 +177,23 @@ def _get_power_status(ssh_obj, c_info):
def _get_connection(node):
return utils.ssh_connect(_parse_control_info(node))
return utils.ssh_connect(_parse_driver_info(node))
def _get_hosts_name_for_node(ssh_obj, c_info):
def _get_hosts_name_for_node(ssh_obj, driver_info):
"""Get the name the host uses to reference the node."""
matched_name = None
cmd_to_exec = c_info['cmd_set']['list_all']
cmd_to_exec = driver_info['cmd_set']['list_all']
full_node_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
# for each node check Mac Addresses
for node in full_node_list:
cmd_to_exec = c_info['cmd_set']['get_node_macs']
cmd_to_exec = driver_info['cmd_set']['get_node_macs']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node)
hosts_node_mac_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
for host_mac in hosts_node_mac_list:
for node_mac in c_info['macs']:
for node_mac in driver_info['macs']:
if _normalize_mac(host_mac) in _normalize_mac(node_mac):
matched_name = node
break
@ -204,40 +206,40 @@ def _get_hosts_name_for_node(ssh_obj, c_info):
return matched_name
def _power_on(ssh_obj, c_info):
def _power_on(ssh_obj, driver_info):
"""Power ON this node."""
current_pstate = _get_power_status(ssh_obj, c_info)
current_pstate = _get_power_status(ssh_obj, driver_info)
if current_pstate == states.POWER_ON:
_power_off(ssh_obj, c_info)
_power_off(ssh_obj, driver_info)
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
cmd_to_power_on = c_info['cmd_set']['start_cmd']
node_name = _get_hosts_name_for_node(ssh_obj, driver_info)
cmd_to_power_on = driver_info['cmd_set']['start_cmd']
cmd_to_power_on = cmd_to_power_on.replace('{_NodeName_}', node_name)
_exec_ssh_command(ssh_obj, cmd_to_power_on)
current_pstate = _get_power_status(ssh_obj, c_info)
current_pstate = _get_power_status(ssh_obj, driver_info)
if current_pstate == states.POWER_ON:
return current_pstate
else:
return states.ERROR
def _power_off(ssh_obj, c_info):
def _power_off(ssh_obj, driver_info):
"""Power OFF this node."""
current_pstate = _get_power_status(ssh_obj, c_info)
current_pstate = _get_power_status(ssh_obj, driver_info)
if current_pstate == states.POWER_OFF:
return current_pstate
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
cmd_to_power_off = c_info['cmd_set']['stop_cmd']
node_name = _get_hosts_name_for_node(ssh_obj, driver_info)
cmd_to_power_off = driver_info['cmd_set']['stop_cmd']
cmd_to_power_off = cmd_to_power_off.replace('{_NodeName_}', node_name)
_exec_ssh_command(ssh_obj, cmd_to_power_off)
current_pstate = _get_power_status(ssh_obj, c_info)
current_pstate = _get_power_status(ssh_obj, driver_info)
if current_pstate == states.POWER_OFF:
return current_pstate
else:
@ -251,30 +253,25 @@ def _get_nodes_mac_addresses(task, node):
return macs
class SSHPowerDriver(base.ControlDriver):
"""SSH Power Driver.
class SSHPower(base.PowerInterface):
"""SSH Power Interface.
This ControlDriver class provides a mechanism for controlling the power
This PowerInterface class provides a mechanism for controlling the power
state of virtual machines via SSH.
NOTE: This driver supports VirtualBox and Virsh commands.
NOTE: This driver does not currently support multi-node operations.
"""
def __init__(self):
pass
def validate_driver_info(self, node):
"""Check that node['control_info'] contains the requisite fields.
def validate(self, node):
"""Check that node['driver_info'] contains the requisite fields.
:param node: Single node object.
:returns: True / False.
"""
try:
_parse_control_info(node)
_parse_driver_info(node)
except exception.InvalidParameterValue:
return False
return True
@ -288,12 +285,11 @@ class SSHPowerDriver(base.ControlDriver):
:param node: A single node.
:returns: power state. One of :class:`ironic.common.states`.
"""
c_info = _parse_control_info(node)
c_info['macs'] = _get_nodes_mac_addresses(task, node)
driver_info = _parse_driver_info(node)
driver_info['macs'] = _get_nodes_mac_addresses(task, node)
ssh_obj = _get_connection(node)
return _get_power_status(ssh_obj, c_info)
return _get_power_status(ssh_obj, driver_info)
@task_manager.require_exclusive_lock
def set_power_state(self, task, node, pstate):
@ -307,18 +303,16 @@ class SSHPowerDriver(base.ControlDriver):
`ironic.common.states`.
:returns NOTHING:
Can raise exception.IronicException or exception.PowerStateFailure.
:raises: exception.IronicException or exception.PowerStateFailure.
"""
c_info = _parse_control_info(node)
c_info['macs'] = _get_nodes_mac_addresses(task, node)
driver_info = _parse_driver_info(node)
driver_info['macs'] = _get_nodes_mac_addresses(task, node)
ssh_obj = _get_connection(node)
if pstate == states.POWER_ON:
state = _power_on(ssh_obj, c_info)
state = _power_on(ssh_obj, driver_info)
elif pstate == states.POWER_OFF:
state = _power_off(ssh_obj, c_info)
state = _power_off(ssh_obj, driver_info)
else:
raise exception.IronicException(_(
"set_power_state called with invalid power state."))
@ -336,47 +330,16 @@ class SSHPowerDriver(base.ControlDriver):
:param node: A single node.
:returns NOTHING:
Can raise exception.PowerStateFailure.
:raises: exception.PowerStateFailure.
"""
c_info = _parse_control_info(node)
c_info['macs'] = _get_nodes_mac_addresses(task, node)
driver_info = _parse_driver_info(node)
driver_info['macs'] = _get_nodes_mac_addresses(task, node)
ssh_obj = _get_connection(node)
current_pstate = _get_power_status(ssh_obj, c_info)
current_pstate = _get_power_status(ssh_obj, driver_info)
if current_pstate == states.POWER_ON:
_power_off(ssh_obj, c_info)
_power_off(ssh_obj, driver_info)
state = _power_on(ssh_obj, c_info)
state = _power_on(ssh_obj, driver_info)
if state != states.POWER_ON:
raise exception.PowerStateFailure(pstate=states.POWER_ON)
def start_console(self, task, node):
"""Starts a console connection to a node.
CURRENTLY NOT SUPPORTED.
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
:param node: A single node.
Will raise raise exception.IronicException.
"""
raise exception.IronicException(_(
"start_console is not supported by SSHPowerDriver."))
def stop_console(self, task, node):
"""Stops a console connection to a node.
CURRENTLY NOT SUPPORTED.
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
:param node: A single node.
Will raise raise exception.IronicException.
"""
raise exception.IronicException(_(
"stop_console is not supported by SSHPowerDriver."))

59
ironic/drivers/pxe.py Normal file
View File

@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 base
from ironic.drivers.modules import ipmi
from ironic.drivers.modules import pxe
from ironic.drivers.modules import ssh
class PXEAndIPMIDriver(base.BaseDriver):
"""PXE + IPMI driver.
This driver implements the `core` functionality, combinding
:class:ironic.drivers.ipmi.IPMI for power on/off and reboot 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):
self.power = ipmi.IPMIPower()
self.deploy = pxe.PXEDeploy()
self.rescue = self.deploy
self.vendor = pxe.IPMIVendorPassthru()
class PXEAndSSHDriver(base.BaseDriver):
"""PXE + SSH driver.
NOTE: This driver is meant only for testing environments.
This driver implements the `core` functionality, combinding
:class:ironic.drivers.ssh.SSH for power on/off and reboot of virtual
machines tunneled over SSH, 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):
self.power = ssh.SSHPower()
self.deploy = pxe.PXEDeploy()
self.rescue = self.deploy
self.vendor = None

View File

@ -75,8 +75,8 @@ class ManagerService(service.PeriodicService):
with task_manager.acquire([node_id], shared=True) as task:
node = task.resources[0].node
driver = task.resources[0].controller
state = driver.get_power_state(task, node)
driver = task.resources[0].driver
state = driver.power.get_power_state(task, node)
return state
# TODO(deva)

View File

@ -46,12 +46,8 @@ class NodeManager(object):
_nodes = {}
_control_factory = dispatch.NameDispatchExtensionManager(
namespace='ironic.controllers',
check_func=lambda x: True,
invoke_on_load=True)
_deploy_factory = dispatch.NameDispatchExtensionManager(
namespace='ironic.deployers',
_driver_factory = dispatch.NameDispatchExtensionManager(
namespace='ironic.drivers',
check_func=lambda x: True,
invoke_on_load=True)
@ -69,22 +65,13 @@ class NodeManager(object):
# NOTE(deva): Driver loading here may get refactored, depend on:
# https://github.com/dreamhost/stevedore/issues/15
try:
ref = NodeManager._control_factory.map(
[self.node.get('control_driver')], _get_instance)
self.controller = ref[0]
ref = NodeManager._driver_factory.map(
[self.node.get('driver')], _get_instance)
self.driver = ref[0]
except KeyError:
raise exception.IronicException(_(
"Failed to load Control driver %s.") %
self.node.get('control_driver'))
try:
ref = NodeManager._deploy_factory.map(
[self.node.get('deploy_driver')], _get_instance)
self.deployer = ref[0]
except KeyError:
raise exception.IronicException(_(
"Failed to load Deploy driver %s.") %
self.node.get('deploy_driver'))
"Failed to load driver %s.") %
self.node.get('driver'))
@classmethod
@lockutils.synchronized(RESOURCE_MANAGER_SEMAPHORE, 'ironic-')

View File

@ -23,7 +23,7 @@ A context manager to peform a series of tasks on a set of resources.
locking and simplify operations across a set of
:class:`ironic.manager.resource_manager.NodeManager` instances. Each
NodeManager holds the data model for a node, as well as references to the
controller and deployer driver singletons appropriate for that node.
driver singleton appropriate for that node.
The :class:`TaskManager` will acquire either a shared or exclusive lock, as
indicated. Multiple shared locks for the same resource may coexist with an
@ -56,11 +56,10 @@ driver function, you can access the drivers directly in this way::
with task_manager.acquire(node_ids) as task:
states = []
for node, control, deploy in [r.node, r.controller, r.deployer
for node, driver in [r.node, r.driver
for r in task.resources]:
# control and deploy are driver singletons,
# loaded based on that node's configuration.
states.append(control.get_power_state(task, node)
# the driver is loaded based on that node's configuration.
states.append(driver.power.get_power_state(task, node)
"""
import contextlib

View File

@ -20,30 +20,45 @@ from ironic.db.sqlalchemy import models
from ironic.openstack.common import jsonutils as json
_ipmi_control_info = json.dumps(
fake_info = json.dumps({"foo": "bar"})
ipmi_info = json.dumps(
{
"ipmi_address": "1.2.3.4",
"ipmi_username": "admin",
"ipmi_password": "fake",
'ipmi': {
"address": "1.2.3.4",
"username": "admin",
"password": "fake",
}
})
_ssh_control_info = json.dumps(
ssh_info = json.dumps(
{
"ssh_host": "1.2.3.4",
"ssh_user": "admin",
"ssh_pass": "fake",
"ssh_port": 22,
"virt_type": "vbox",
'ssh': {
"address": "1.2.3.4",
"username": "admin",
"password": "fake",
"port": 22,
"virt_type": "vbox",
"key_filename": "/not/real/file",
}
})
_deploy_info = json.dumps(
pxe_info = json.dumps(
{
"image_path": "/path/to/image.qcow2",
"image_source": "glance://image-uuid",
"deploy_image_source": "glance://deploy-image-uuid",
'pxe': {
"image_path": "/path/to/image.qcow2",
"image_source": "glance://image-uuid",
"deploy_image_source": "glance://deploy-image-uuid",
}
})
_properties = json.dumps(
pxe_ssh_info = json.dumps(
dict(json.loads(pxe_info), **json.loads(ssh_info)))
pxe_ipmi_info = json.dumps(
dict(json.loads(pxe_info), **json.loads(ipmi_info)))
properties = json.dumps(
{
"cpu_arch": "x86_64",
"cpu_num": 8,
@ -61,18 +76,11 @@ def get_test_node(**kw):
node.instance_uuid = kw.get('instance_uuid',
'8227348d-5f1d-4488-aad1-7c92b2d42504')
node.control_driver = kw.get('control_driver', 'ipmi')
node.control_info = kw.get('control_info', None)
node.driver = kw.get('driver', 'fake')
node.driver_info = kw.get('driver_info', fake_info)
if node.control_driver == 'ipmi' and not node.control_info:
node.control_info = _ipmi_control_info
elif node.control_driver == 'ssh' and not node.control_info:
node.control_info = _ssh_control_info
node.deploy_driver = kw.get('deploy_driver', 'pxe')
node.deploy_info = kw.get('deploy_info', _deploy_info)
node.properties = kw.get('properties', _properties)
node.properties = kw.get('properties', properties)
node.extra = kw.get('extra', '{}')
return node

View File

@ -0,0 +1,61 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Test class for Fake driver."""
from ironic.common import exception
from ironic.common import states
from ironic.drivers import base as driver_base
from ironic.drivers import fake
from ironic.tests import base
from ironic.tests.db import utils as db_utils
class FakeDriverTestCase(base.TestCase):
def setUp(self):
super(FakeDriverTestCase, self).setUp()
self.driver = fake.FakeDriver()
self.node = db_utils.get_test_node()
def test_driver_interfaces(self):
# fake driver implements only 3 out of 5 interfaces
self.assertIsInstance(self.driver.power, driver_base.PowerInterface)
self.assertIsInstance(self.driver.deploy, driver_base.DeployInterface)
self.assertIsInstance(self.driver.vendor, driver_base.VendorInterface)
self.assertIsNone(self.driver.rescue)
self.assertIsNone(self.driver.console)
def test_power_interface(self):
self.driver.power.validate(self.node)
self.driver.power.get_power_state(None, self.node)
self.driver.power.set_power_state(None, None, states.NOSTATE)
self.driver.power.reboot(None, None)
def test_deploy_interface(self):
self.driver.deploy.validate(self.node)
self.driver.deploy.deploy(None, None)
self.driver.deploy.tear_down(None, None)
def test_vendor_interface(self):
self.driver.vendor.validate(self.node)
self.driver.vendor.vendor_passthru(None, self.node,
method='foo', bar='baz')
self.assertRaises(exception.IronicException,
self.driver.vendor.vendor_passthru,
None, self.node, method='fake')

View File

@ -17,7 +17,7 @@
# License for the specific language governing permissions and limitations
# under the License.
"""Test class for baremetal IPMI power manager."""
"""Test class for IPMI driver module."""
import os
import stat
@ -30,26 +30,24 @@ from ironic.common import exception
from ironic.common import states
from ironic.common import utils
from ironic.db import api as db_api
from ironic.drivers import ipmi
from ironic.drivers.modules import ipmi
from ironic.manager import task_manager
from ironic.tests import base
from ironic.tests.db import base as db_base
from ironic.tests.db import utils as db_utils
from ironic.tests.manager import utils as mgr_utils
CONF = cfg.CONF
class BareMetalIPMITestCase(base.TestCase):
class IPMIPrivateMethodTestCase(base.TestCase):
def setUp(self):
super(BareMetalIPMITestCase, self).setUp()
self.dbapi = db_api.get_instance()
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager(
control_driver='ipmi')
self.node = db_utils.get_test_node(control_driver='ipmi',
deploy_driver='fake')
self.dbapi.create_node(self.node)
super(IPMIPrivateMethodTestCase, self).setUp()
self.node = db_utils.get_test_node(
driver='fake_ipmi',
driver_info=db_utils.ipmi_info)
self.info = ipmi._parse_driver_info(self.node)
def test__make_password_file(self):
fakepass = 'this is a fake password'
@ -63,39 +61,38 @@ class BareMetalIPMITestCase(base.TestCase):
finally:
os.unlink(pw_file)
def test__parse_control_info(self):
def test__parse_driver_info(self):
# make sure we get back the expected things
node = db_utils.get_test_node()
info = ipmi._parse_control_info(node)
self.assertIsNotNone(info.get('address'))
self.assertIsNotNone(info.get('user'))
self.assertIsNotNone(info.get('password'))
self.assertIsNotNone(info.get('uuid'))
self.assertIsNotNone(self.info.get('address'))
self.assertIsNotNone(self.info.get('username'))
self.assertIsNotNone(self.info.get('password'))
self.assertIsNotNone(self.info.get('uuid'))
# make sure error is raised when info, eg. username, is missing
_control_info = json.dumps(
_driver_info = json.dumps(
{
"ipmi_address": "1.2.3.4",
"ipmi_password": "fake",
'ipmi': {
"address": "1.2.3.4",
"password": "fake",
}
})
node = db_utils.get_test_node(control_info=_control_info)
node = db_utils.get_test_node(driver_info=_driver_info)
self.assertRaises(exception.InvalidParameterValue,
ipmi._parse_control_info,
ipmi._parse_driver_info,
node)
def test__exec_ipmitool(self):
pw_file = '/tmp/password_file'
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_make_password_file')
self.mox.StubOutWithMock(utils, 'execute')
self.mox.StubOutWithMock(utils, 'delete_if_exists')
ipmi._make_password_file(info['password']).AndReturn(pw_file)
ipmi._make_password_file(self.info['password']).AndReturn(pw_file)
args = [
'ipmitool',
'-I', 'lanplus',
'-H', info['address'],
'-U', info['user'],
'-H', self.info['address'],
'-U', self.info['username'],
'-f', pw_file,
'A', 'B', 'C',
]
@ -103,125 +100,133 @@ class BareMetalIPMITestCase(base.TestCase):
utils.delete_if_exists(pw_file).AndReturn(None)
self.mox.ReplayAll()
ipmi._exec_ipmitool(info, 'A B C')
ipmi._exec_ipmitool(self.info, 'A B C')
self.mox.VerifyAll()
def test__power_status_on(self):
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
info = ipmi._parse_control_info(self.node)
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is on\n", None])
self.mox.ReplayAll()
state = ipmi._power_status(info)
state = ipmi._power_status(self.info)
self.mox.VerifyAll()
self.assertEqual(state, states.POWER_ON)
def test__power_status_off(self):
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
info = ipmi._parse_control_info(self.node)
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
self.mox.ReplayAll()
state = ipmi._power_status(info)
state = ipmi._power_status(self.info)
self.mox.VerifyAll()
self.assertEqual(state, states.POWER_OFF)
def test__power_status_error(self):
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
info = ipmi._parse_control_info(self.node)
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is badstate\n", None])
self.mox.ReplayAll()
state = ipmi._power_status(info)
state = ipmi._power_status(self.info)
self.mox.VerifyAll()
self.assertEqual(state, states.ERROR)
def test__power_on_max_retries(self):
self.config(ipmi_power_retry=2)
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
info = ipmi._parse_control_info(self.node)
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
ipmi._exec_ipmitool(info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
ipmi._exec_ipmitool(info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
ipmi._exec_ipmitool(info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power on").AndReturn([None, None])
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
self.mox.ReplayAll()
state = ipmi._power_on(info)
state = ipmi._power_on(self.info)
self.mox.VerifyAll()
self.assertEqual(state, states.ERROR)
class IPMIDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(IPMIDriverTestCase, self).setUp()
self.dbapi = db_api.get_instance()
self.driver = mgr_utils.get_mocked_node_manager(driver='fake_ipmi')
self.node = db_utils.get_test_node(
driver='fake_ipmi',
driver_info=db_utils.ipmi_info)
self.info = ipmi._parse_driver_info(self.node)
self.dbapi.create_node(self.node)
def test_get_power_state(self):
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is off\n", None])
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["Chassis Power is on\n", None])
ipmi._exec_ipmitool(info, "power status").AndReturn(
ipmi._exec_ipmitool(self.info, "power status").AndReturn(
["\n", None])
self.mox.ReplayAll()
pstate = self.controller.get_power_state(None, self.node)
pstate = self.driver.power.get_power_state(None, self.node)
self.assertEqual(pstate, states.POWER_OFF)
pstate = self.controller.get_power_state(None, self.node)
pstate = self.driver.power.get_power_state(None, self.node)
self.assertEqual(pstate, states.POWER_ON)
pstate = self.controller.get_power_state(None, self.node)
pstate = self.driver.power.get_power_state(None, self.node)
self.assertEqual(pstate, states.ERROR)
self.mox.VerifyAll()
def test_set_power_on_ok(self):
self.config(ipmi_power_retry=0)
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_power_on')
self.mox.StubOutWithMock(ipmi, '_power_off')
ipmi._power_on(info).AndReturn(states.POWER_ON)
ipmi._power_on(self.info).AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.controller.set_power_state(task, self.node, states.POWER_ON)
self.driver.power.set_power_state(
task, self.node, states.POWER_ON)
self.mox.VerifyAll()
def test_set_power_off_ok(self):
self.config(ipmi_power_retry=0)
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_power_on')
self.mox.StubOutWithMock(ipmi, '_power_off')
ipmi._power_off(info).AndReturn(states.POWER_OFF)
ipmi._power_off(self.info).AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.controller.set_power_state(task, self.node, states.POWER_OFF)
self.driver.power.set_power_state(
task, self.node, states.POWER_OFF)
self.mox.VerifyAll()
def test_set_power_on_fail(self):
self.config(ipmi_power_retry=0)
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_power_on')
self.mox.StubOutWithMock(ipmi, '_power_off')
ipmi._power_on(info).AndReturn(states.ERROR)
ipmi._power_on(self.info).AndReturn(states.ERROR)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.assertRaises(exception.PowerStateFailure,
self.controller.set_power_state,
self.driver.power.set_power_state,
task,
self.node,
states.POWER_ON)
@ -230,57 +235,54 @@ class BareMetalIPMITestCase(base.TestCase):
def test_set_power_invalid_state(self):
with task_manager.acquire([self.node.uuid]) as task:
self.assertRaises(exception.IronicException,
self.controller.set_power_state,
self.driver.power.set_power_state,
task,
self.node,
"fake state")
def test_set_boot_device_ok(self):
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_exec_ipmitool')
ipmi._exec_ipmitool(info, "chassis bootdev pxe").\
ipmi._exec_ipmitool(self.info, "chassis bootdev pxe").\
AndReturn([None, None])
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.controller.set_boot_device(task, self.node, 'pxe')
self.driver.power._set_boot_device(task, self.node, 'pxe')
self.mox.VerifyAll()
def test_set_boot_device_bad_device(self):
with task_manager.acquire([self.node.uuid]) as task:
self.assertRaises(exception.InvalidParameterValue,
self.controller.set_boot_device,
self.driver.power._set_boot_device,
task,
self.node,
'fake-device')
def test_reboot_ok(self):
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_power_off')
self.mox.StubOutWithMock(ipmi, '_power_on')
ipmi._power_off(info)
ipmi._power_on(info).AndReturn(states.POWER_ON)
ipmi._power_off(self.info)
ipmi._power_on(self.info).AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.controller.reboot(task, self.node)
self.driver.power.reboot(task, self.node)
self.mox.VerifyAll()
def test_reboot_fail(self):
info = ipmi._parse_control_info(self.node)
self.mox.StubOutWithMock(ipmi, '_power_off')
self.mox.StubOutWithMock(ipmi, '_power_on')
ipmi._power_off(info)
ipmi._power_on(info).AndReturn(states.ERROR)
ipmi._power_off(self.info)
ipmi._power_on(self.info).AndReturn(states.ERROR)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
self.assertRaises(exception.PowerStateFailure,
self.controller.reboot,
self.driver.power.reboot,
task,
self.node)

View File

@ -0,0 +1,22 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Test class for PXE driver.
TODO
"""

View File

@ -18,49 +18,29 @@
import mox
import paramiko
from oslo.config import cfg
from ironic.openstack.common import jsonutils as json
from ironic.common import exception
from ironic.common import states
from ironic.db import api as dbapi
from ironic.drivers import ssh
from ironic.drivers.modules import ssh
from ironic.manager import task_manager
from ironic.tests import base
from ironic.tests.db import base as db_base
from ironic.tests.db import utils as db_utils
from ironic.tests.manager import utils as mgr_utils
CONF = cfg.CONF
INFO_DICT = json.loads(db_utils.ssh_info).get('ssh')
class IronicSSHTestCase(base.TestCase):
class SSHValidateParametersTestCase(base.TestCase):
def setUp(self):
super(IronicSSHTestCase, self).setUp()
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager(
control_driver="ssh")
self.node = db_utils.get_test_node(**{'control_driver': 'ssh',
'deploy_driver': 'fake'})
self.dbapi = dbapi.get_instance()
self.dbapi.create_node(self.node)
self.ifports = [db_utils.get_test_port(id=6, address='aa:bb:cc'),
db_utils.get_test_port(id=7, address='dd:ee:ff')]
self.dbapi.create_port(self.ifports[0])
self.dbapi.create_port(self.ifports[1])
self.ssh = ssh.SSHPowerDriver()
self.test_control_info_dict = {
"ssh_host": "1.2.3.4",
"ssh_user": "admin",
"ssh_pass": "fake",
"ssh_port": 22,
"virt_type": "vbox",
"ssh_key": "/not/real/file"}
def test__parse_control_info_good(self):
def test__parse_driver_info_good(self):
# make sure we get back the expected things
info = ssh._parse_control_info(self.node)
node = db_utils.get_test_node(
driver='fake_ssh',
driver_info=db_utils.ssh_info)
info = ssh._parse_driver_info(node)
self.assertIsNotNone(info.get('host'))
self.assertIsNotNone(info.get('username'))
self.assertIsNotNone(info.get('password'))
@ -70,62 +50,62 @@ class IronicSSHTestCase(base.TestCase):
self.assertIsNotNone(info.get('uuid'))
self.mox.VerifyAll()
def test__parse_control_info_missing_host(self):
def test__parse_driver_info_missing_host(self):
# make sure error is raised when info is missing
tmp_dict = self.test_control_info_dict
del tmp_dict['ssh_host']
del tmp_dict['ssh_key']
_ssh_control_info = json.dumps(tmp_dict)
node = db_utils.get_test_node(control_info=_ssh_control_info)
tmp_dict = dict(INFO_DICT)
del tmp_dict['address']
del tmp_dict['key_filename']
info = json.dumps({'ssh': tmp_dict})
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
ssh._parse_control_info,
ssh._parse_driver_info,
node)
self.mox.VerifyAll()
def test__parse_control_info_missing_user(self):
def test__parse_driver_info_missing_user(self):
# make sure error is raised when info is missing
tmp_dict = self.test_control_info_dict
del tmp_dict['ssh_user']
del tmp_dict['ssh_key']
_ssh_control_info = json.dumps(tmp_dict)
node = db_utils.get_test_node(control_info=_ssh_control_info)
tmp_dict = dict(INFO_DICT)
del tmp_dict['username']
del tmp_dict['key_filename']
info = json.dumps({'ssh': tmp_dict})
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
ssh._parse_control_info,
ssh._parse_driver_info,
node)
self.mox.VerifyAll()
def test__parse_control_info_missing_pass(self):
def test__parse_driver_info_missing_pass(self):
# make sure error is raised when info is missing
tmp_dict = self.test_control_info_dict
del tmp_dict['ssh_pass']
del tmp_dict['ssh_key']
_ssh_control_info = json.dumps(tmp_dict)
node = db_utils.get_test_node(control_info=_ssh_control_info)
tmp_dict = dict(INFO_DICT)
del tmp_dict['password']
del tmp_dict['key_filename']
info = json.dumps({'ssh': tmp_dict})
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
ssh._parse_control_info,
ssh._parse_driver_info,
node)
self.mox.VerifyAll()
def test__parse_control_info_missing_virt_type(self):
def test__parse_driver_info_missing_virt_type(self):
# make sure error is raised when info is missing
tmp_dict = self.test_control_info_dict
tmp_dict = dict(INFO_DICT)
del tmp_dict['virt_type']
del tmp_dict['ssh_key']
_ssh_control_info = json.dumps(tmp_dict)
node = db_utils.get_test_node(control_info=_ssh_control_info)
del tmp_dict['key_filename']
info = json.dumps({'ssh': tmp_dict})
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
ssh._parse_control_info,
ssh._parse_driver_info,
node)
self.mox.VerifyAll()
def test__parse_control_info_missing_key(self):
def test__parse_driver_info_missing_key(self):
# make sure error is raised when info is missing
tmp_dict = self.test_control_info_dict
del tmp_dict['ssh_pass']
_ssh_control_info = json.dumps(tmp_dict)
node = db_utils.get_test_node(control_info=_ssh_control_info)
tmp_dict = dict(INFO_DICT)
del tmp_dict['password']
info = json.dumps({'ssh': tmp_dict})
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.FileNotFound,
ssh._parse_control_info,
ssh._parse_driver_info,
node)
self.mox.VerifyAll()
@ -135,219 +115,191 @@ class IronicSSHTestCase(base.TestCase):
self.assertEqual(mac_clean, "0a1b2c3d4f")
self.mox.VerifyAll()
class SSHPrivateMethodsTestCase(base.TestCase):
def setUp(self):
super(SSHPrivateMethodsTestCase, self).setUp()
self.node = db_utils.get_test_node(
driver='fake_ssh',
driver_info=db_utils.ssh_info)
self.sshclient = paramiko.SSHClient()
def test__get_power_status_on(self):
info = ssh._parse_control_info(self.node)
ssh_obj = paramiko.SSHClient()
info = ssh._parse_driver_info(self.node)
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
ssh._exec_ssh_command(
ssh_obj, info['cmd_set']['list_running']).AndReturn(
self.sshclient, info['cmd_set']['list_running']).AndReturn(
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NodeName")
self.mox.ReplayAll()
pstate = ssh._get_power_status(ssh_obj, info)
pstate = ssh._get_power_status(self.sshclient, info)
self.assertEqual(pstate, states.POWER_ON)
self.mox.VerifyAll()
def test__get_power_status_off(self):
info = ssh._parse_control_info(self.node)
ssh_obj = paramiko.SSHClient()
info = ssh._parse_driver_info(self.node)
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
ssh._exec_ssh_command(
ssh_obj, info['cmd_set']['list_running']).AndReturn(
self.sshclient, info['cmd_set']['list_running']).AndReturn(
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NotNodeName")
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NotNodeName")
self.mox.ReplayAll()
pstate = ssh._get_power_status(ssh_obj, info)
pstate = ssh._get_power_status(self.sshclient, info)
self.assertEqual(pstate, states.POWER_OFF)
self.mox.VerifyAll()
def test__get_power_status_error(self):
info = ssh._parse_control_info(self.node)
ssh_obj = paramiko.SSHClient()
info = ssh._parse_driver_info(self.node)
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
ssh._exec_ssh_command(
ssh_obj, info['cmd_set']['list_running']).AndReturn(
self.sshclient, info['cmd_set']['list_running']).AndReturn(
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn(None)
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn(None)
self.mox.ReplayAll()
pstate = ssh._get_power_status(ssh_obj, info)
pstate = ssh._get_power_status(self.sshclient, info)
self.assertEqual(pstate, states.ERROR)
self.mox.VerifyAll()
def test__get_hosts_name_for_node_match(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._exec_ssh_command(ssh_obj, info['cmd_set']['list_all']).AndReturn(
['NodeName'])
cmd_to_exec = info['cmd_set']['get_node_macs']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(
['52:54:00:cf:2d:31'])
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._exec_ssh_command(self.sshclient, info['cmd_set']['list_all']).\
AndReturn(['NodeName'])
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(['52:54:00:cf:2d:31'])
self.mox.ReplayAll()
found_name = ssh._get_hosts_name_for_node(ssh_obj, info)
found_name = ssh._get_hosts_name_for_node(self.sshclient, info)
self.assertEqual(found_name, 'NodeName')
self.mox.VerifyAll()
def test__get_hosts_name_for_node_no_match(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "22:22:22:22:22:22"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._exec_ssh_command(ssh_obj, info['cmd_set']['list_all']).AndReturn(
['NodeName'])
ssh._exec_ssh_command(self.sshclient, info['cmd_set']['list_all']).\
AndReturn(['NodeName'])
cmd_to_exec = info['cmd_set']['get_node_macs']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(
['52:54:00:cf:2d:31'])
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(['52:54:00:cf:2d:31'])
self.mox.ReplayAll()
found_name = ssh._get_hosts_name_for_node(ssh_obj, info)
found_name = ssh._get_hosts_name_for_node(self.sshclient, info)
self.assertEqual(found_name, None)
self.mox.VerifyAll()
def test__power_on_good(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_OFF)
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NodeName")
cmd_to_exec = info['cmd_set']['start_cmd']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(None)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()
current_state = ssh._power_on(ssh_obj, info)
current_state = ssh._power_on(self.sshclient, info)
self.assertEqual(current_state, states.POWER_ON)
self.mox.VerifyAll()
def test__power_on_fail(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_OFF)
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NodeName")
cmd_to_exec = info['cmd_set']['start_cmd']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(None)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
current_state = ssh._power_on(ssh_obj, info)
current_state = ssh._power_on(self.sshclient, info)
self.assertEqual(current_state, states.ERROR)
self.mox.VerifyAll()
def test__power_off_good(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NodeName")
cmd_to_exec = info['cmd_set']['stop_cmd']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(None)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
current_state = ssh._power_off(ssh_obj, info)
current_state = ssh._power_off(self.sshclient, info)
self.assertEqual(current_state, states.POWER_OFF)
self.mox.VerifyAll()
def test__power_off_fail(self):
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
ssh._get_hosts_name_for_node(self.sshclient, info).\
AndReturn("NodeName")
cmd_to_exec = info['cmd_set']['stop_cmd']
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._exec_ssh_command(self.sshclient, cmd_to_exec).\
AndReturn(None)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()
current_state = ssh._power_off(ssh_obj, info)
current_state = ssh._power_off(self.sshclient, info)
self.assertEqual(current_state, states.ERROR)
self.mox.VerifyAll()
def test_reboot_good(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_power_off')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._power_off(ssh_obj, info).AndReturn(None)
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
task.resources[0].controller.reboot(task, self.node)
self.mox.VerifyAll()
def test_reboot_fail(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_power_off')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._power_off(ssh_obj, info).AndReturn(None)
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0].controller.reboot,
task,
self.node)
self.mox.VerifyAll()
def test_exec_ssh_command_good(self):
info = ssh._parse_control_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh_obj, 'exec_command')
self.mox.StubOutWithMock(self.sshclient, 'exec_command')
class Channel(object):
def recv_exit_status(self):
@ -364,18 +316,16 @@ class IronicSSHTestCase(base.TestCase):
def close(self):
pass
ssh_obj.exec_command("command").AndReturn(
self.sshclient.exec_command("command").AndReturn(
(Stream(), Stream('hello'), Stream()))
self.mox.ReplayAll()
stdout, stderr = ssh._exec_ssh_command(ssh_obj, "command")
stdout, stderr = ssh._exec_ssh_command(self.sshclient, "command")
self.assertEqual(stdout, 'hello')
self.mox.VerifyAll()
def test_exec_ssh_command_fail(self):
info = ssh._parse_control_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
ssh_obj = paramiko.SSHClient()
self.mox.StubOutWithMock(ssh_obj, 'exec_command')
self.mox.StubOutWithMock(self.sshclient, 'exec_command')
class Channel(object):
def recv_exit_status(self):
@ -392,69 +342,148 @@ class IronicSSHTestCase(base.TestCase):
def close(self):
pass
ssh_obj.exec_command("command").AndReturn(
self.sshclient.exec_command("command").AndReturn(
(Stream(), Stream('hello'), Stream()))
self.mox.ReplayAll()
self.assertRaises(exception.ProcessExecutionError,
ssh._exec_ssh_command,
ssh_obj,
self.sshclient,
"command")
self.mox.VerifyAll()
def test_start_console(self):
self.assertRaises(exception.IronicException,
self.ssh.start_console,
None,
self.node)
class SSHDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(SSHDriverTestCase, self).setUp()
self.driver = mgr_utils.get_mocked_node_manager(driver='fake_ssh')
self.node = db_utils.get_test_node(
driver='fake_ssh',
driver_info=db_utils.ssh_info)
self.dbapi = dbapi.get_instance()
self.dbapi.create_node(self.node)
self.sshclient = paramiko.SSHClient()
def test__get_nodes_mac_addresses(self):
class task(object):
dbapi = self.dbapi
ports = [db_utils.get_test_port(id=6, address='aa:bb:cc'),
db_utils.get_test_port(id=7, address='dd:ee:ff')]
self.dbapi.create_port(ports[0])
self.dbapi.create_port(ports[1])
self.mox.StubOutWithMock(self.dbapi, 'get_ports_by_node')
self.dbapi.get_ports_by_node(self.node.get('id')).\
AndReturn(ports)
self.mox.ReplayAll()
node_macs = ssh._get_nodes_mac_addresses(task(), self.node)
self.assertEqual(node_macs, ['aa:bb:cc', 'dd:ee:ff'])
self.mox.VerifyAll()
def test_stop_console(self):
self.assertRaises(exception.IronicException,
self.ssh.start_console,
None,
self.node)
def test_reboot_good(self):
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_power_off')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
ssh._power_off(self.sshclient, info).\
AndReturn(None)
ssh._power_on(self.sshclient, info).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
task.resources[0].driver.power.reboot(task, self.node)
self.mox.VerifyAll()
def test_reboot_fail(self):
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_get_power_status')
self.mox.StubOutWithMock(ssh, '_power_off')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).AndReturn(self.sshclient)
ssh._get_power_status(self.sshclient, info).\
AndReturn(states.POWER_ON)
ssh._power_off(self.sshclient, info).\
AndReturn(None)
ssh._power_on(self.sshclient, info).\
AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0].driver.power.reboot,
task,
self.node)
self.mox.VerifyAll()
def test_set_power_state_bad_state(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
self.assertRaises(exception.IronicException,
task.resources[0].controller.set_power_state,
task.resources[0].driver.power.set_power_state,
task,
self.node,
"BAD_PSTATE")
self.mox.VerifyAll()
def test_set_power_state_on_good(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
ssh._power_on(self.sshclient, info).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
task.resources[0].controller.set_power_state(
task.resources[0].driver.power.set_power_state(
task,
self.node,
states.POWER_ON)
@ -462,47 +491,51 @@ class IronicSSHTestCase(base.TestCase):
self.mox.VerifyAll()
def test_set_power_state_on_fail(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_power_on')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
ssh._power_on(self.sshclient, info).\
AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0].controller.set_power_state,
task.resources[0].driver.power.set_power_state,
task,
self.node,
states.POWER_ON)
self.mox.VerifyAll()
def test_set_power_state_off_good(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_power_off')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._power_off(ssh_obj, info).AndReturn(states.POWER_OFF)
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
ssh._power_off(self.sshclient, info).\
AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
task.resources[0].controller.set_power_state(
task.resources[0].driver.power.set_power_state(
task,
self.node,
states.POWER_OFF)
@ -510,40 +543,27 @@ class IronicSSHTestCase(base.TestCase):
self.mox.VerifyAll()
def test_set_power_state_off_fail(self):
ssh_obj = paramiko.SSHClient()
info = ssh._parse_control_info(self.node)
info = ssh._parse_driver_info(self.node)
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
self.mox.StubOutWithMock(ssh, '_parse_control_info')
self.mox.StubOutWithMock(ssh, '_parse_driver_info')
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
self.mox.StubOutWithMock(ssh, '_get_connection')
self.mox.StubOutWithMock(ssh, '_power_off')
ssh._parse_control_info(self.node).AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
info['macs'])
ssh._get_connection(self.node).AndReturn(ssh_obj)
ssh._power_off(ssh_obj, info).AndReturn(states.POWER_ON)
ssh._parse_driver_info(self.node).\
AndReturn(info)
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).\
AndReturn(info['macs'])
ssh._get_connection(self.node).\
AndReturn(self.sshclient)
ssh._power_off(self.sshclient, info).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']], shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0].controller.set_power_state,
task.resources[0].driver.power.set_power_state,
task,
self.node,
states.POWER_OFF)
self.mox.VerifyAll()
def test__get_nodes_mac_addresses(self):
info = ssh._parse_control_info(self.node)
self.mox.StubOutWithMock(self.dbapi, 'get_ports_by_node')
self.dbapi.get_ports_by_node(self.node.get('uuid')).\
AndReturn(self.ifports)
self.dbapi.get_ports_by_node(self.node.get('id')).\
AndReturn(self.ifports)
self.mox.ReplayAll()
with task_manager.acquire([info['uuid']]) as task:
node_macs = ssh._get_nodes_mac_addresses(task, self.node)
self.assertEqual(node_macs, ['aa:bb:cc', 'dd:ee:ff'])
self.mox.VerifyAll()

View File

@ -36,11 +36,10 @@ class ManagerTestCase(base.DbTestCase):
self.service = manager.ManagerService('test-host', 'test-topic')
self.context = context.get_admin_context()
self.dbapi = dbapi.get_instance()
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager()
self.driver = mgr_utils.get_mocked_node_manager()
def test_get_power_state(self):
n = utils.get_test_node(control_driver='fake',
deploy_driver='fake')
n = utils.get_test_node(driver='fake')
self.dbapi.create_node(n)
# FakeControlDriver.get_power_state will "pass"
@ -49,15 +48,14 @@ class ManagerTestCase(base.DbTestCase):
self.assertEqual(state, states.NOSTATE)
def test_get_power_state_with_mock(self):
n = utils.get_test_node(control_driver='fake',
deploy_driver='fake')
n = utils.get_test_node(driver='fake')
self.dbapi.create_node(n)
self.mox.StubOutWithMock(self.controller, 'get_power_state')
self.mox.StubOutWithMock(self.driver.power, 'get_power_state')
self.controller.get_power_state(mox.IgnoreArg(), mox.IgnoreArg()).\
self.driver.power.get_power_state(mox.IgnoreArg(), mox.IgnoreArg()).\
AndReturn(states.POWER_OFF)
self.controller.get_power_state(mox.IgnoreArg(), mox.IgnoreArg()).\
self.driver.power.get_power_state(mox.IgnoreArg(), mox.IgnoreArg()).\
AndReturn(states.POWER_ON)
self.mox.ReplayAll()

View File

@ -51,7 +51,7 @@ class TaskManagerTestCase(base.DbTestCase):
def setUp(self):
super(TaskManagerTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager()
self.driver = mgr_utils.get_mocked_node_manager()
self.uuids = [create_fake_node(i) for i in xrange(1, 6)]
self.uuids.sort()
@ -119,7 +119,7 @@ class ExclusiveLockDecoratorTestCase(base.DbTestCase):
def setUp(self):
super(ExclusiveLockDecoratorTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager()
self.driver = mgr_utils.get_mocked_node_manager()
self.uuids = [create_fake_node(123)]
def test_require_exclusive_lock(self):

View File

@ -27,19 +27,29 @@ class UtilsTestCase(base.TestCase):
def setUp(self):
super(UtilsTestCase, self).setUp()
def test_fails_to_load_extension(self):
self.assertRaises(AttributeError,
utils.get_mockable_extension_manager,
'fake',
'bad.namespace')
self.assertRaises(AttributeError,
utils.get_mockable_extension_manager,
'no-such-driver',
'ironic.drivers')
def test_get_mockable_ext_mgr(self):
(mgr, ext) = utils.get_mockable_extension_manager("fake",
'ironic.controllers')
(mgr, ext) = utils.get_mockable_extension_manager('fake',
'ironic.drivers')
# confirm that stevedore did not scan the actual entrypoints
self.assertNotEqual(mgr.namespace, 'ironic.controllers')
self.assertNotEqual(mgr.namespace, 'ironic.drivers')
# confirm mgr has only one extension
self.assertEqual(len(mgr.extensions), 1)
# confirm that we got a reference to the extension in this manager
self.assertEqual(mgr.extensions[0], ext)
# confirm that it is the "fake" driver we asked for
self.assertEqual("%s" % ext.entry_point,
"fake = ironic.drivers.fake:FakeControlDriver")
"fake = ironic.drivers.fake:FakeDriver")
def test_get_mocked_node_mgr(self):
self.mox.StubOutWithMock(utils, 'get_mockable_extension_manager')
@ -48,19 +58,14 @@ class UtilsTestCase(base.TestCase):
def __init__(self, name):
self.obj = name
utils.get_mockable_extension_manager('foo', 'ironic.controllers').\
utils.get_mockable_extension_manager('foo', 'ironic.drivers').\
AndReturn(('foo-manager', ext('foo-extension')))
utils.get_mockable_extension_manager('bar', 'ironic.deployers').\
AndReturn(('bar-manager', ext('bar-extension')))
self.mox.ReplayAll()
(control_ext, deploy_ext) = utils.get_mocked_node_manager('foo', 'bar')
driver = utils.get_mocked_node_manager('foo')
self.assertEqual(resource_manager.NodeManager._control_factory,
self.assertEqual(resource_manager.NodeManager._driver_factory,
'foo-manager')
self.assertEqual(control_ext, 'foo-extension')
self.assertEqual(resource_manager.NodeManager._deploy_factory,
'bar-manager')
self.assertEqual(deploy_ext, 'bar-extension')
self.assertEqual(driver, 'foo-extension')
self.mox.VerifyAll()

View File

@ -35,10 +35,13 @@ def get_mockable_extension_manager(driver, namespace):
namespace.
"""
for entry_point in list(pkg_resources.iter_entry_points(namespace)):
s = "%s" % entry_point
if s.startswith(driver):
entry_point = None
for ep in list(pkg_resources.iter_entry_points(namespace)):
s = "%s" % ep
if driver == s[:s.index(' =')]:
entry_point = ep
break
mock_ext_mgr = dispatch.NameDispatchExtensionManager(
'ironic.no-such-namespace',
lambda x: True)
@ -48,25 +51,18 @@ def get_mockable_extension_manager(driver, namespace):
return (mock_ext_mgr, mock_ext)
def get_mocked_node_manager(control_driver="fake", deploy_driver="fake"):
"""Get a mockable :class:NodeManager instance.
def get_mocked_node_manager(driver="fake"):
"""Mock :class:NodeManager and get a ref to the driver inside..
To enable testing of NodeManagers, we need to control what plugins
stevedore loads under the hood. To do that, we fake the plugin loading,
substitute NodeManager's _control_factory and _deploy_factory with the
fake managers, and then return handles to the actual objects.
substituting NodeManager's _driver_factory with an instance of the
specified driver only, and return a reference directly to that driver
instance.
:returns: A tuple of (control, deploy) drivers.
:returns: A driver instance.
"""
(mgr, ext) = get_mockable_extension_manager(control_driver,
'ironic.controllers')
resource_manager.NodeManager._control_factory = mgr
c = ext.obj
(mgr, ext) = get_mockable_extension_manager(deploy_driver,
'ironic.deployers')
resource_manager.NodeManager._deploy_factory = mgr
d = ext.obj
return (c, d)
(mgr, ext) = get_mockable_extension_manager(driver, 'ironic.drivers')
resource_manager.NodeManager._driver_factory = mgr
return ext.obj

View File

@ -33,14 +33,13 @@ console_scripts =
ironic-manager = ironic.cmd.manager:main
ironic-rootwrap = ironic.openstack.common.rootwrap.cmd:main
ironic.controllers =
fake = ironic.drivers.fake:FakeControlDriver
ipmi = ironic.drivers.ipmi:IPMIPowerDriver
ssh = ironic.drivers.ssh:SSHPowerDriver
ironic.deployers =
fake = ironic.drivers.fake:FakeDeployDriver
# pxe = ironic.drivers.pxe.PXEDeployDriver
ironic.drivers =
fake = ironic.drivers.fake:FakeDriver
fake_ipmi = ironic.drivers.fake:FakeIPMIDriver
fake_ssh = ironic.drivers.fake:FakeSSHDriver
fake_pxe = ironic.drivers.fake:FakePXEDriver
pxe_ipmi = ironic.drivers.pxe:PXEAndIPMIDriver
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
[pbr]
autodoc_index_modules = True