Add drivers.base.BaseDriver.get_properties()

Adds ironic.drivers.base.BaseDriver.get_properties() which returns a
dictionary of <property>:<description> entries.

The driver interfaces (DeployInterface, PowerInterface, ...) have a
new get_properties() method that returns a dictionary of
<property>:<description>.

These changes are needed in order to provide an API to get driver_info
properties.

Change-Id: I5994e990deb26841633ca26de1a5fb63b743271a
Blueprint: get-required-driver-info
Partial-Bug: #1261915
This commit is contained in:
Ruby Loo 2014-07-15 10:02:18 -04:00
parent bf809fdf08
commit c75a070520
18 changed files with 294 additions and 32 deletions

View File

@ -88,11 +88,33 @@ class BaseDriver(object):
def __init__(self):
pass
def get_properties(self):
"""Get the properties of the driver.
:returns: dictionary of <property name>:<property description> entries.
"""
properties = {}
for iface_name in (self.core_interfaces +
self.standard_interfaces +
['vendor']):
iface = getattr(self, iface_name, None)
if iface:
properties.update(iface.get_properties())
return properties
@six.add_metaclass(abc.ABCMeta)
class DeployInterface(object):
"""Interface for deploy-related actions."""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task):
"""Validate the driver-specific Node deployment info.
@ -189,6 +211,13 @@ class DeployInterface(object):
class PowerInterface(object):
"""Interface for power-related actions."""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task):
"""Validate the driver-specific Node power info.
@ -230,6 +259,13 @@ class PowerInterface(object):
class ConsoleInterface(object):
"""Interface for console-related actions."""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task):
"""Validate the driver-specific Node console info.
@ -273,6 +309,13 @@ class ConsoleInterface(object):
class RescueInterface(object):
"""Interface for rescue-related actions."""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task):
"""Validate the rescue info stored in the node' properties.
@ -310,6 +353,13 @@ class VendorInterface(object):
should be short-lived.
"""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task, **kwargs):
"""Validate vendor-specific actions.
@ -357,6 +407,13 @@ class VendorInterface(object):
class ManagementInterface(object):
"""Interface for management related actions."""
@abc.abstractmethod
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
@abc.abstractmethod
def validate(self, task):
"""Validate the driver-specific management information.

View File

@ -42,6 +42,9 @@ def _raise_unsupported_error(method=None):
class FakePower(base.PowerInterface):
"""Example implementation of a simple power interface."""
def get_properties(self):
return {}
def validate(self, task):
pass
@ -63,6 +66,9 @@ class FakeDeploy(base.DeployInterface):
separate power interface.
"""
def get_properties(self):
return {}
def validate(self, task):
pass
@ -85,6 +91,10 @@ class FakeDeploy(base.DeployInterface):
class FakeVendorA(base.VendorInterface):
"""Example implementation of a vendor passthru interface."""
def get_properties(self):
return {'A1': 'A1 description. Required.',
'A2': 'A2 description. Optional.'}
def validate(self, task, **kwargs):
method = kwargs.get('method')
if method == 'first_method':
@ -109,6 +119,10 @@ class FakeVendorA(base.VendorInterface):
class FakeVendorB(base.VendorInterface):
"""Example implementation of a secondary vendor passthru."""
def get_properties(self):
return {'B1': 'B1 description. Required.',
'B2': 'B2 description. Required.'}
def validate(self, task, **kwargs):
method = kwargs.get('method')
if method == 'second_method':
@ -133,6 +147,9 @@ class FakeVendorB(base.VendorInterface):
class FakeConsole(base.ConsoleInterface):
"""Example implementation of a simple console interface."""
def get_properties(self):
return {}
def validate(self, task):
pass
@ -149,6 +166,9 @@ class FakeConsole(base.ConsoleInterface):
class FakeManagement(base.ManagementInterface):
"""Example implementation of a simple management interface."""
def get_properties(self):
return {}
def validate(self, task):
pass

View File

@ -44,6 +44,19 @@ CONF.register_opts(opts, group='ilo')
LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {
'ilo_address': _("IP address or hostname of the iLO. Required."),
'ilo_username': _("username for the iLO with administrator privileges. "
"Required."),
'ilo_password': _("password for ilo_username. Required.")
}
OPTIONAL_PROPERTIES = {
'client_port': _("port to be used for iLO operations. Optional."),
'client_timeout': _("timeout (in seconds) for iLO operations. Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
def parse_driver_info(node):
"""Gets the driver specific Node deployment info.
@ -61,13 +74,13 @@ def parse_driver_info(node):
d_info = {}
error_msgs = []
for param in ('ilo_address', 'ilo_username', 'ilo_password'):
for param in REQUIRED_PROPERTIES:
try:
d_info[param] = info[param]
except KeyError:
error_msgs.append(_("'%s' not supplied to IloDriver.") % param)
for param in ('client_port', 'client_timeout'):
for param in OPTIONAL_PROPERTIES:
value = info.get(param, CONF.ilo.get(param))
try:
value = int(value)

View File

@ -152,6 +152,9 @@ def _set_power_state(node, target_state):
class IloPower(base.PowerInterface):
def get_properties(self):
return ilo_common.COMMON_PROPERTIES
def validate(self, task):
"""Check if node.driver_info contains the required iLO credentials.

View File

@ -50,6 +50,11 @@ CONF.register_opts(opts, group='ipmi')
LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {'ipmi_address': _("IP of the node's BMC. Required."),
'ipmi_password': _("IPMI password. Required."),
'ipmi_username': _("IPMI username. Required.")}
COMMON_PROPERTIES = REQUIRED_PROPERTIES
def _parse_driver_info(node):
"""Gets the bmc access info for the given node.
@ -58,19 +63,18 @@ def _parse_driver_info(node):
"""
info = node.driver_info or {}
bmc_info = {}
bmc_info['address'] = info.get('ipmi_address')
bmc_info['username'] = info.get('ipmi_username')
bmc_info['password'] = info.get('ipmi_password')
# address, username and password must be present
missing_info = [key for key in bmc_info if not bmc_info[key]]
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
if missing_info:
raise exception.InvalidParameterValue(_(
"The following IPMI credentials are not supplied"
" to IPMI driver: %s."
) % missing_info)
bmc_info = {}
bmc_info['address'] = info.get('ipmi_address')
bmc_info['username'] = info.get('ipmi_username')
bmc_info['password'] = info.get('ipmi_password')
# get additional info
bmc_info['uuid'] = node.uuid
@ -207,6 +211,9 @@ def _power_status(driver_info):
class NativeIPMIPower(base.PowerInterface):
"""The power driver using native python-ipmi library."""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that node['driver_info'] contains IPMI credentials.
@ -299,6 +306,9 @@ class VendorPassthru(base.VendorInterface):
% {'node_id': driver_info['uuid'], 'error': str(e)})
raise exception.IPMIFailure(cmd=str(e))
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task, **kwargs):
"""Validate vendor-specific actions.
:param task: a TaskManager instance.

View File

@ -65,6 +65,23 @@ CONF.import_opt('min_command_interval',
LOG = logging.getLogger(__name__)
VALID_PRIV_LEVELS = ['ADMINISTRATOR', 'CALLBACK', 'OPERATOR', 'USER']
REQUIRED_PROPERTIES = {
'ipmi_address': _("IP address or hostname of the node. Required.")
}
OPTIONAL_PROPERTIES = {
'ipmi_password': _("password. Optional."),
'ipmi_priv_level': _("privilege level; default is ADMINISTRATOR. One of "
"%s. Optional.") % ', '.join(VALID_PRIV_LEVELS),
'ipmi_username': _("username; default is NULL user. Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
CONSOLE_PROPERTIES = {
'ipmi_terminal_port': _("node's UDP port to connect to. Only required for "
"console access.")
}
LAST_CMD_TIME = {}
TIMING_SUPPORT = None
@ -145,6 +162,13 @@ def _parse_driver_info(node):
"""
info = node.driver_info or {}
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
if missing_info:
raise exception.InvalidParameterValue(_(
"The following IPMI credentials are not supplied"
" to IPMI driver: %s."
) % missing_info)
address = info.get('ipmi_address')
username = info.get('ipmi_username')
password = info.get('ipmi_password')
@ -158,10 +182,6 @@ def _parse_driver_info(node):
raise exception.InvalidParameterValue(_(
"IPMI terminal port is not an integer."))
if not address:
raise exception.InvalidParameterValue(_(
"IPMI address not supplied to IPMI driver."))
if priv_level not in VALID_PRIV_LEVELS:
valid_priv_lvls = ', '.join(VALID_PRIV_LEVELS)
raise exception.InvalidParameterValue(_(
@ -365,6 +385,9 @@ class IPMIPower(base.PowerInterface):
reason="Unable to locate usable ipmitool command in "
"the system path when checking ipmitool version")
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Validate driver_info for ipmitool driver.
@ -438,6 +461,9 @@ class IPMIPower(base.PowerInterface):
class IPMIManagement(base.ManagementInterface):
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that 'driver_info' contains IPMI credentials.
@ -602,6 +628,9 @@ class VendorPassthru(base.VendorInterface):
{'node_id': node_uuid, 'error': e})
raise exception.IPMIFailure(cmd=cmd)
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task, **kwargs):
"""Validate vendor-specific actions.
@ -668,6 +697,11 @@ class IPMIShellinaboxConsole(base.ConsoleInterface):
reason="Unable to locate usable ipmitool command in "
"the system path when checking ipmitool version")
def get_properties(self):
d = COMMON_PROPERTIES.copy()
d.update(CONSOLE_PROPERTIES)
return d
def validate(self, task):
"""Validate the Node console info.

View File

@ -90,6 +90,14 @@ CONF = cfg.CONF
CONF.register_opts(pxe_opts, group='pxe')
CONF.import_opt('use_ipv6', 'ironic.netconf')
REQUIRED_PROPERTIES = {
'pxe_deploy_kernel': _("UUID (from Glance) of the deployment kernel. "
"Required."),
'pxe_deploy_ramdisk': _("UUID (from Glance) of the ramdisk that is "
"mounted at boot time. Required."),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES
def _check_for_missing_params(info_dict, param_prefix=''):
missing_info = []
@ -463,6 +471,9 @@ def _validate_glance_image(ctx, deploy_info):
class PXEDeploy(base.DeployInterface):
"""PXE Deploy Interface: just a stub until the real driver is ported."""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Validate the deployment information for the task's node.
@ -609,6 +620,9 @@ class VendorPassthru(base.VendorInterface):
return params
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task, **kwargs):
method = kwargs['method']
if method == 'pass_deploy_info':

View File

@ -62,6 +62,19 @@ _BOOT_DEVICES_MAP = {
boot_devices.PXE: 'pxe',
}
REQUIRED_PROPERTIES = {
'seamicro_api_endpoint': _("API endpoint. Required."),
'seamicro_password': _("password. Required."),
'seamicro_server_id': _("server ID. Required."),
'seamicro_username': _("username. Required."),
}
OPTIONAL_PROPERTIES = {
'seamicro_api_version': _("version of SeaMicro API client; default is 2. "
"Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
def _get_client(*args, **kwargs):
"""Creates the python-seamicro_client
@ -87,24 +100,18 @@ def _parse_driver_info(node):
"""
info = node.driver_info or {}
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
if missing_info:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires the following to be set: %s.")
% missing_info)
api_endpoint = info.get('seamicro_api_endpoint')
username = info.get('seamicro_username')
password = info.get('seamicro_password')
server_id = info.get('seamicro_server_id')
api_version = info.get('seamicro_api_version', "2")
if not api_endpoint:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires api_endpoint be set"))
if not username or not password:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires both username and password be set"))
if not server_id:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires server_id be set"))
res = {'username': username,
'password': password,
'api_endpoint': api_endpoint,
@ -322,6 +329,9 @@ class Power(base.PowerInterface):
state of servers in a seamicro chassis.
"""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that node 'driver_info' is valid.
@ -389,6 +399,9 @@ class Power(base.PowerInterface):
class VendorPassthru(base.VendorInterface):
"""SeaMicro vendor-specific methods."""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task, **kwargs):
method = kwargs['method']
if method not in VENDOR_PASSTHRU_METHODS:
@ -470,6 +483,9 @@ class VendorPassthru(base.VendorInterface):
class Management(base.ManagementInterface):
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that 'driver_info' contains SeaMicro credentials.

View File

@ -35,6 +35,7 @@ from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers import utils as driver_utils
from ironic.openstack.common.gettextutils import _
from ironic.openstack.common import log as logging
from ironic.openstack.common import processutils
@ -49,6 +50,27 @@ CONF.register_opts(libvirt_opts, group='ssh')
LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {
'ssh_address': _("IP address or hostname of the node to ssh into. "
"Required."),
'ssh_username': _("username to authenticate as. Required."),
'ssh_virt_type': _("virtualization software to use; one of vbox, virsh, "
"vmware. Required.")
}
OTHER_PROPERTIES = {
'ssh_key_contents': _("private key(s). One of this, ssh_key_filename, "
"or ssh_password must be specified."),
'ssh_key_filename': _("(list of) filename(s) of optional private key(s) "
"for authentication. One of this, ssh_key_contents, "
"or ssh_password must be specified."),
'ssh_password': _("password to use for authentication or for unlocking a "
"private key. One of this, ssh_key_contents, or "
"ssh_key_filename must be specified."),
'ssh_port': _("port on the node to connect to; default is 22. Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OTHER_PROPERTIES)
def _get_command_sets(virt_type):
if virt_type == 'vbox':
@ -155,6 +177,12 @@ def _parse_driver_info(node):
"""
info = node.driver_info or {}
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
if missing_info:
raise exception.InvalidParameterValue(_(
"SSHPowerDriver requires the following to be set: %s.")
% missing_info)
address = info.get('ssh_address')
username = info.get('ssh_username')
password = info.get('ssh_password')
@ -176,16 +204,9 @@ def _parse_driver_info(node):
'uuid': node.uuid
}
if not virt_type:
raise exception.InvalidParameterValue(_(
"SSHPowerDriver requires virt_type be set."))
cmd_set = _get_command_sets(virt_type)
res['cmd_set'] = cmd_set
if not address or not username:
raise exception.InvalidParameterValue(_(
"SSHPowerDriver requires both address and username be set."))
# Only one credential may be set (avoids complexity around having
# precedence etc).
if len(filter(None, (password, key_filename, key_contents))) != 1:
@ -354,6 +375,9 @@ class SSHPower(base.PowerInterface):
NOTE: This driver does not currently support multi-node operations.
"""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that the node's 'driver_info' is valid.

View File

@ -46,6 +46,18 @@ class MixinVendorInterface(base.VendorInterface):
method = kwargs.get('method')
return self.mapping.get(method) or _raise_unsupported_error(method)
def get_properties(self):
"""Return the properties from all the VendorInterfaces.
:returns: a dictionary of <property_name>:<property_description>
entries.
"""
properties = {}
interfaces = set(self.mapping.values())
for interface in interfaces:
properties.update(interface.get_properties())
return properties
def validate(self, *args, **kwargs):
"""Call validate on the appropriate interface only.

View File

@ -149,6 +149,12 @@ class IloPowerTestCase(base.TestCase):
driver='ilo',
driver_info=driver_info)
def test_get_properties(self):
expected = ilo_common.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(ilo_common, 'parse_driver_info')
def test_validate(self, mock_drvinfo):
with task_manager.acquire(self.context, self.node.uuid,

View File

@ -53,7 +53,13 @@ class FakeDriverTestCase(base.TestCase):
driver_base.ConsoleInterface)
self.assertIsNone(self.driver.rescue)
def test_get_properties(self):
expected = ['A1', 'A2', 'B1', 'B2']
properties = self.driver.get_properties()
self.assertEqual(sorted(expected), sorted(properties.keys()))
def test_power_interface(self):
self.assertEqual({}, self.driver.power.get_properties())
self.driver.power.validate(self.task)
self.driver.power.get_power_state(self.task)
self.assertRaises(exception.InvalidParameterValue,
@ -63,6 +69,7 @@ class FakeDriverTestCase(base.TestCase):
self.driver.power.reboot(self.task)
def test_deploy_interface(self):
self.assertEqual({}, self.driver.deploy.get_properties())
self.driver.deploy.validate(None)
self.driver.deploy.prepare(None)
@ -74,11 +81,15 @@ class FakeDriverTestCase(base.TestCase):
self.driver.deploy.tear_down(None)
def test_console_interface(self):
self.assertEqual({}, self.driver.console.get_properties())
self.driver.console.validate(self.task)
self.driver.console.start_console(self.task)
self.driver.console.stop_console(self.task)
self.driver.console.get_console(self.task)
def test_management_interface_get_properties(self):
self.assertEqual({}, self.driver.management.get_properties())
def test_management_interface_validate(self):
self.driver.management.validate(self.task)

View File

@ -143,6 +143,10 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
self.dbapi = db_api.get_instance()
self.info = ipminative._parse_driver_info(self.node)
def test_get_properties(self):
expected = ipminative.COMMON_PROPERTIES
self.assertEqual(expected, self.driver.get_properties())
@mock.patch('pyghmi.ipmi.command.Command')
def test_get_power_state(self, ipmi_mock):
# Getting the mocked command.

View File

@ -484,6 +484,17 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
driver_info=INFO_DICT)
self.info = ipmi._parse_driver_info(self.node)
def test_get_properties(self):
expected = ipmi.COMMON_PROPERTIES
self.assertEqual(expected, self.driver.power.get_properties())
expected = ipmi.COMMON_PROPERTIES.keys()
expected += ipmi.CONSOLE_PROPERTIES.keys()
self.assertEqual(sorted(expected),
sorted(self.driver.console.get_properties().keys()))
self.assertEqual(sorted(expected),
sorted(self.driver.get_properties().keys()))
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
def test_get_power_state(self, mock_exec):
returns = iter([["Chassis Power is off\n", None],

View File

@ -519,6 +519,12 @@ class PXEDriverTestCase(db_base.DbTestCase):
open(token_path, 'w').close()
return token_path
def test_get_properties(self):
expected = pxe.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test_validate_good(self, mock_glance):
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',

View File

@ -274,6 +274,12 @@ class SeaMicroPowerDriverTestCase(db_base.DbTestCase):
self.Server = Fake_Server
self.Volume = Fake_Volume
def test_get_properties(self):
expected = seamicro.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node['uuid'],
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(seamicro, '_parse_driver_info')
def test_power_interface_validate_good(self, parse_drv_info_mock):
with task_manager.acquire(self.context, self.node['uuid'],

View File

@ -580,6 +580,13 @@ class SSHDriverTestCase(db_base.DbTestCase):
driver_info = ssh._parse_driver_info(task.node)
ssh_connect_mock.assert_called_once_with(driver_info)
def test_get_properties(self):
expected = ssh.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.power.get_properties())
self.assertEqual(expected, task.driver.get_properties())
def test_validate_fail_no_port(self):
new_node = obj_utils.create_test_node(
self.context,

View File

@ -38,6 +38,14 @@ class UtilsTestCase(base.TestCase):
self.driver = driver_factory.get_driver("fake")
self.node = obj_utils.create_test_node(self.context)
def test_vendor_interface_get_properties(self):
expected = {'A1': 'A1 description. Required.',
'A2': 'A2 description. Optional.',
'B1': 'B1 description. Required.',
'B2': 'B2 description. Required.'}
props = self.driver.vendor.get_properties()
self.assertEqual(expected, props)
@mock.patch.object(fake.FakeVendorA, 'validate')
def test_vendor_interface_validate_valid_methods(self,
mock_fakea_validate):