Add vendor_passthru method for subscriptions
This patch adds two new vendor_passthru methods for Redfish: - create_subscription (create a sbuscription) - delete_subscription (delete a subscription) - get_all_subscriptions (get all subscriptions on the node) - get_subscription (get a single subscription) Unit Tests in test_utils split into multiple classes to avoid random failures due to cache. Tested in bifrost env using two different HW: - HPE EL8000 e910 - Dell R640 Story: #2009061 Task: #42854 Change-Id: I5b7fa99b0ee64ccdc0f62d9686df655082db3665
This commit is contained in:
parent
7b42258ab9
commit
4bc5142df2
@ -11,7 +11,7 @@ python-dracclient>=5.1.0,<7.0.0
|
|||||||
python-xclarityclient>=0.1.6
|
python-xclarityclient>=0.1.6
|
||||||
|
|
||||||
# The Redfish hardware type uses the Sushy library
|
# The Redfish hardware type uses the Sushy library
|
||||||
sushy>=3.8.0
|
sushy>=3.10.0
|
||||||
|
|
||||||
# Ansible-deploy interface
|
# Ansible-deploy interface
|
||||||
ansible>=2.7
|
ansible>=2.7
|
||||||
|
@ -263,6 +263,23 @@ def get_update_service(node):
|
|||||||
raise exception.RedfishError(error=e)
|
raise exception.RedfishError(error=e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_service(node):
|
||||||
|
"""Get a node's event service.
|
||||||
|
|
||||||
|
:param node: an Ironic node object.
|
||||||
|
:raises: RedfishConnectionError when it fails to connect to Redfish
|
||||||
|
:raises: RedfishError when the EventService is not registered in Redfish
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _get_connection(node, lambda conn: conn.get_event_service())
|
||||||
|
except sushy.exceptions.MissingAttributeError as e:
|
||||||
|
LOG.error('The Redfish EventService was not found for '
|
||||||
|
'node %(node)s. Error %(error)s',
|
||||||
|
{'node': node.uuid, 'error': e})
|
||||||
|
raise exception.RedfishError(error=e)
|
||||||
|
|
||||||
|
|
||||||
def get_system(node):
|
def get_system(node):
|
||||||
"""Get a Redfish System that represents a node.
|
"""Get a Redfish System that represents a node.
|
||||||
|
|
||||||
|
@ -16,6 +16,9 @@ Vendor Interface for Redfish drivers and its supporting methods.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ironic_lib import metrics_utils
|
from ironic_lib import metrics_utils
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import importutils
|
||||||
|
import rfc3986
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
@ -23,7 +26,14 @@ from ironic.drivers import base
|
|||||||
from ironic.drivers.modules.redfish import boot as redfish_boot
|
from ironic.drivers.modules.redfish import boot as redfish_boot
|
||||||
from ironic.drivers.modules.redfish import utils as redfish_utils
|
from ironic.drivers.modules.redfish import utils as redfish_utils
|
||||||
|
|
||||||
|
sushy = importutils.try_import('sushy')
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||||
|
SUBSCRIPTION_FIELDS_REMOVE = {
|
||||||
|
'@odata.context', '@odate.etag', '@odata.id', '@odata.type',
|
||||||
|
'HttpHeaders', 'Oem', 'Name', 'Description'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RedfishVendorPassthru(base.VendorInterface):
|
class RedfishVendorPassthru(base.VendorInterface):
|
||||||
@ -49,6 +59,12 @@ class RedfishVendorPassthru(base.VendorInterface):
|
|||||||
if method == 'eject_vmedia':
|
if method == 'eject_vmedia':
|
||||||
self._validate_eject_vmedia(task, kwargs)
|
self._validate_eject_vmedia(task, kwargs)
|
||||||
return
|
return
|
||||||
|
if method == 'create_subscription':
|
||||||
|
self._validate_create_subscription(task, kwargs)
|
||||||
|
return
|
||||||
|
if method == 'delete_subscription':
|
||||||
|
self._validate_delete_subscription(task, kwargs)
|
||||||
|
return
|
||||||
super(RedfishVendorPassthru, self).validate(task, method, **kwargs)
|
super(RedfishVendorPassthru, self).validate(task, method, **kwargs)
|
||||||
|
|
||||||
def _validate_eject_vmedia(self, task, kwargs):
|
def _validate_eject_vmedia(self, task, kwargs):
|
||||||
@ -90,3 +106,179 @@ class RedfishVendorPassthru(base.VendorInterface):
|
|||||||
# If boot_device not provided all vmedia devices will be ejected
|
# If boot_device not provided all vmedia devices will be ejected
|
||||||
boot_device = kwargs.get('boot_device')
|
boot_device = kwargs.get('boot_device')
|
||||||
redfish_boot.eject_vmedia(task, boot_device)
|
redfish_boot.eject_vmedia(task, boot_device)
|
||||||
|
|
||||||
|
def _validate_create_subscription(self, task, kwargs):
|
||||||
|
"""Verify that the args input are valid."""
|
||||||
|
destination = kwargs.get('Destination')
|
||||||
|
event_types = kwargs.get('EventTypes')
|
||||||
|
context = kwargs.get('Context')
|
||||||
|
protocol = kwargs.get('Protocol')
|
||||||
|
|
||||||
|
if event_types is not None:
|
||||||
|
event_service = redfish_utils.get_event_service(task.node)
|
||||||
|
allowed_values = set(
|
||||||
|
event_service.get_event_types_for_subscription())
|
||||||
|
if not (isinstance(event_types, list)
|
||||||
|
and set(event_types).issubset(allowed_values)):
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("EventTypes %s is not a valid value, allowed values %s")
|
||||||
|
% (str(event_types), str(allowed_values)))
|
||||||
|
|
||||||
|
# NOTE(iurygregory): check only if they are strings.
|
||||||
|
# BMCs will fail to create a subscription if the context, protocol or
|
||||||
|
# destination are invalid.
|
||||||
|
if not isinstance(context, str):
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("Context %s is not a valid string") % context)
|
||||||
|
if not isinstance(protocol, str):
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("Protocol %s is not a string") % protocol)
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = rfc3986.uri_reference(destination)
|
||||||
|
if not parsed.is_valid(require_scheme=True,
|
||||||
|
require_authority=True):
|
||||||
|
# NOTE(iurygregory): raise error because the parsed
|
||||||
|
# destination does not contain scheme or authority.
|
||||||
|
raise TypeError
|
||||||
|
except TypeError:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("Destination %s is not a valid URI") % destination)
|
||||||
|
|
||||||
|
def _filter_subscription_fields(self, subscription_json):
|
||||||
|
filter_subscription = {k: v for k, v in subscription_json.items()
|
||||||
|
if k not in SUBSCRIPTION_FIELDS_REMOVE}
|
||||||
|
return filter_subscription
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishVendorPassthru.create_subscription')
|
||||||
|
@base.passthru(['POST'], async_call=False,
|
||||||
|
description=_("Creates a subscription on a node. "
|
||||||
|
"Required argument: a dictionary of "
|
||||||
|
"{'destination': 'destination_url'}"))
|
||||||
|
def create_subscription(self, task, **kwargs):
|
||||||
|
"""Creates a subscription.
|
||||||
|
|
||||||
|
:param task: A TaskManager object.
|
||||||
|
:param kwargs: The arguments sent with vendor passthru.
|
||||||
|
:raises: RedfishError, if any problem occurs when trying to create
|
||||||
|
a subscription.
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
'Destination': kwargs.get('Destination'),
|
||||||
|
'Protocol': kwargs.get('Protocol', "Redfish"),
|
||||||
|
'Context': kwargs.get('Context', ""),
|
||||||
|
'EventTypes': kwargs.get('EventTypes', ["Alert"])
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
event_service = redfish_utils.get_event_service(task.node)
|
||||||
|
subscription = event_service.subscriptions.create(payload)
|
||||||
|
return self._filter_subscription_fields(subscription.json)
|
||||||
|
except sushy.exceptions.SushyError as e:
|
||||||
|
error_msg = (_('Failed to create subscription on node %(node)s. '
|
||||||
|
'Subscription payload: %(payload)s. '
|
||||||
|
'Error: %(error)s') % {'node': task.node.uuid,
|
||||||
|
'payload': str(payload),
|
||||||
|
'error': e})
|
||||||
|
LOG.error(error_msg)
|
||||||
|
raise exception.RedfishError(error=error_msg)
|
||||||
|
|
||||||
|
def _validate_delete_subscription(self, task, kwargs):
|
||||||
|
"""Verify that the args input are valid."""
|
||||||
|
# We can only check if the kwargs contain the id field.
|
||||||
|
|
||||||
|
if not kwargs.get('id'):
|
||||||
|
raise exception.InvalidParameterValue(_("id can't be None"))
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishVendorPassthru.delete_subscription')
|
||||||
|
@base.passthru(['DELETE'], async_call=False,
|
||||||
|
description=_("Delete a subscription on a node. "
|
||||||
|
"Required argument: a dictionary of "
|
||||||
|
"{'id': 'subscription_bmc_id'}"))
|
||||||
|
def delete_subscription(self, task, **kwargs):
|
||||||
|
"""Creates a subscription.
|
||||||
|
|
||||||
|
:param task: A TaskManager object.
|
||||||
|
:param kwargs: The arguments sent with vendor passthru.
|
||||||
|
:raises: RedfishError, if any problem occurs when trying to delete
|
||||||
|
the subscription.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
event_service = redfish_utils.get_event_service(task.node)
|
||||||
|
redfish_subscriptions = event_service.subscriptions
|
||||||
|
bmc_id = kwargs.get('id')
|
||||||
|
# NOTE(iurygregory): some BMCs doesn't report the last /
|
||||||
|
# in the path for the resource, since we will add the ID
|
||||||
|
# we need to make sure the separator is present.
|
||||||
|
separator = "" if redfish_subscriptions.path[-1] == "/" else "/"
|
||||||
|
|
||||||
|
resource = redfish_subscriptions.path + separator + bmc_id
|
||||||
|
subscription = redfish_subscriptions.get_member(resource)
|
||||||
|
msg = (_('Sucessfuly deleted subscription %(id)s on node '
|
||||||
|
'%(node)s') % {'id': bmc_id, 'node': task.node.uuid})
|
||||||
|
subscription.delete()
|
||||||
|
LOG.debug(msg)
|
||||||
|
except sushy.exceptions.SushyError as e:
|
||||||
|
error_msg = (_('Redfish delete_subscription failed for '
|
||||||
|
'subscription %(id)s on node %(node)s. '
|
||||||
|
'Error: %(error)s') % {'id': bmc_id,
|
||||||
|
'node': task.node.uuid,
|
||||||
|
'error': e})
|
||||||
|
LOG.error(error_msg)
|
||||||
|
raise exception.RedfishError(error=error_msg)
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishVendorPassthru.get_subscriptions')
|
||||||
|
@base.passthru(['GET'], async_call=False,
|
||||||
|
description=_("Returns all subscriptions on the node."))
|
||||||
|
def get_all_subscriptions(self, task, **kwargs):
|
||||||
|
"""Get all Subscriptions on the node
|
||||||
|
|
||||||
|
:param task: A TaskManager object.
|
||||||
|
:param kwargs: Not used.
|
||||||
|
:raises: RedfishError, if any problem occurs when retrieving all
|
||||||
|
subscriptions.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
event_service = redfish_utils.get_event_service(task.node)
|
||||||
|
subscriptions = event_service.subscriptions.json
|
||||||
|
return subscriptions
|
||||||
|
except sushy.exceptions.SushyError as e:
|
||||||
|
error_msg = (_('Redfish get_subscriptions failed for '
|
||||||
|
'node %(node)s. '
|
||||||
|
'Error: %(error)s') % {'node': task.node.uuid,
|
||||||
|
'error': e})
|
||||||
|
LOG.error(error_msg)
|
||||||
|
raise exception.RedfishError(error=error_msg)
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishVendorPassthru.get_subscription')
|
||||||
|
@base.passthru(['GET'], async_call=False,
|
||||||
|
description=_("Get a subscription on the node. "
|
||||||
|
"Required argument: a dictionary of "
|
||||||
|
"{'id': 'subscription_bmc_id'}"))
|
||||||
|
def get_subscription(self, task, **kwargs):
|
||||||
|
"""Get a specific subscription on the node
|
||||||
|
|
||||||
|
:param task: A TaskManager object.
|
||||||
|
:param kwargs: The arguments sent with vendor passthru.
|
||||||
|
:raises: RedfishError, if any problem occurs when retrieving the
|
||||||
|
subscription.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
event_service = redfish_utils.get_event_service(task.node)
|
||||||
|
redfish_subscriptions = event_service.subscriptions
|
||||||
|
bmc_id = kwargs.get('id')
|
||||||
|
# NOTE(iurygregory): some BMCs doesn't report the last /
|
||||||
|
# in the path for the resource, since we will add the ID
|
||||||
|
# we need to make sure the separator is present.
|
||||||
|
separator = "" if redfish_subscriptions.path[-1] == "/" else "/"
|
||||||
|
resource = redfish_subscriptions.path + separator + bmc_id
|
||||||
|
subscription = event_service.subscriptions.get_member(resource)
|
||||||
|
return self._filter_subscription_fields(subscription.json)
|
||||||
|
except sushy.exceptions.SushyError as e:
|
||||||
|
error_msg = (_('Redfish get_subscription failed for '
|
||||||
|
'subscription %(id)s on node %(node)s. '
|
||||||
|
'Error: %(error)s') % {'id': bmc_id,
|
||||||
|
'node': task.node.uuid,
|
||||||
|
'error': e})
|
||||||
|
LOG.error(error_msg)
|
||||||
|
raise exception.RedfishError(error=error_msg)
|
||||||
|
@ -168,64 +168,6 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
|
|||||||
response = redfish_utils.parse_driver_info(self.node)
|
response = redfish_utils.parse_driver_info(self.node)
|
||||||
self.assertEqual(self.parsed_driver_info, response)
|
self.assertEqual(self.parsed_driver_info, response)
|
||||||
|
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
|
||||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
|
||||||
'SessionCache._sessions', {})
|
|
||||||
def test_get_system(self, mock_sushy):
|
|
||||||
fake_conn = mock_sushy.return_value
|
|
||||||
fake_system = fake_conn.get_system.return_value
|
|
||||||
response = redfish_utils.get_system(self.node)
|
|
||||||
self.assertEqual(fake_system, response)
|
|
||||||
fake_conn.get_system.assert_called_once_with(
|
|
||||||
'/redfish/v1/Systems/FAKESYSTEM')
|
|
||||||
|
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
|
||||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
|
||||||
'SessionCache._sessions', {})
|
|
||||||
def test_get_system_resource_not_found(self, mock_sushy):
|
|
||||||
fake_conn = mock_sushy.return_value
|
|
||||||
fake_conn.get_system.side_effect = (
|
|
||||||
sushy.exceptions.ResourceNotFoundError('GET',
|
|
||||||
'/',
|
|
||||||
requests.Response()))
|
|
||||||
|
|
||||||
self.assertRaises(exception.RedfishError,
|
|
||||||
redfish_utils.get_system, self.node)
|
|
||||||
fake_conn.get_system.assert_called_once_with(
|
|
||||||
'/redfish/v1/Systems/FAKESYSTEM')
|
|
||||||
|
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
|
||||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
|
||||||
'SessionCache._sessions', {})
|
|
||||||
def test_get_system_multiple_systems(self, mock_sushy):
|
|
||||||
self.node.driver_info.pop('redfish_system_id')
|
|
||||||
fake_conn = mock_sushy.return_value
|
|
||||||
redfish_utils.get_system(self.node)
|
|
||||||
fake_conn.get_system.assert_called_once_with(None)
|
|
||||||
|
|
||||||
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
|
||||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
|
||||||
'SessionCache._sessions', {})
|
|
||||||
def test_get_system_resource_connection_error_retry(self, mock_sushy):
|
|
||||||
# Redfish specific configurations
|
|
||||||
self.config(connection_attempts=3, group='redfish')
|
|
||||||
|
|
||||||
fake_conn = mock_sushy.return_value
|
|
||||||
fake_conn.get_system.side_effect = sushy.exceptions.ConnectionError()
|
|
||||||
|
|
||||||
self.assertRaises(exception.RedfishConnectionError,
|
|
||||||
redfish_utils.get_system, self.node)
|
|
||||||
|
|
||||||
expected_get_system_calls = [
|
|
||||||
mock.call(self.parsed_driver_info['system_id']),
|
|
||||||
mock.call(self.parsed_driver_info['system_id']),
|
|
||||||
mock.call(self.parsed_driver_info['system_id']),
|
|
||||||
]
|
|
||||||
fake_conn.get_system.assert_has_calls(expected_get_system_calls)
|
|
||||||
self.assertEqual(fake_conn.get_system.call_count,
|
|
||||||
redfish_utils.CONF.redfish.connection_attempts)
|
|
||||||
|
|
||||||
def test_get_task_monitor(self):
|
def test_get_task_monitor(self):
|
||||||
redfish_utils._get_connection = mock.Mock()
|
redfish_utils._get_connection = mock.Mock()
|
||||||
fake_monitor = mock.Mock()
|
fake_monitor = mock.Mock()
|
||||||
@ -245,6 +187,64 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
|
|||||||
self.assertRaises(exception.RedfishError,
|
self.assertRaises(exception.RedfishError,
|
||||||
redfish_utils.get_task_monitor, self.node, uri)
|
redfish_utils.get_task_monitor, self.node, uri)
|
||||||
|
|
||||||
|
def test_get_update_service(self):
|
||||||
|
redfish_utils._get_connection = mock.Mock()
|
||||||
|
mock_update_service = mock.Mock()
|
||||||
|
redfish_utils._get_connection.return_value = mock_update_service
|
||||||
|
|
||||||
|
result = redfish_utils.get_update_service(self.node)
|
||||||
|
|
||||||
|
self.assertEqual(mock_update_service, result)
|
||||||
|
|
||||||
|
def test_get_update_service_error(self):
|
||||||
|
redfish_utils._get_connection = mock.Mock()
|
||||||
|
redfish_utils._get_connection.side_effect =\
|
||||||
|
sushy.exceptions.MissingAttributeError
|
||||||
|
|
||||||
|
self.assertRaises(exception.RedfishError,
|
||||||
|
redfish_utils.get_update_service, self.node)
|
||||||
|
|
||||||
|
def test_get_event_service(self):
|
||||||
|
redfish_utils._get_connection = mock.Mock()
|
||||||
|
mock_event_service = mock.Mock()
|
||||||
|
redfish_utils._get_connection.return_value = mock_event_service
|
||||||
|
|
||||||
|
result = redfish_utils.get_event_service(self.node)
|
||||||
|
|
||||||
|
self.assertEqual(mock_event_service, result)
|
||||||
|
|
||||||
|
def test_get_event_service_error(self):
|
||||||
|
redfish_utils._get_connection = mock.Mock()
|
||||||
|
redfish_utils._get_connection.side_effect =\
|
||||||
|
sushy.exceptions.MissingAttributeError
|
||||||
|
|
||||||
|
self.assertRaises(exception.RedfishError,
|
||||||
|
redfish_utils.get_event_service, self.node)
|
||||||
|
|
||||||
|
|
||||||
|
class RedfishUtilsAuthTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(RedfishUtilsAuthTestCase, self).setUp()
|
||||||
|
# Default configurations
|
||||||
|
self.config(enabled_hardware_types=['redfish'],
|
||||||
|
enabled_power_interfaces=['redfish'],
|
||||||
|
enabled_boot_interfaces=['redfish-virtual-media'],
|
||||||
|
enabled_management_interfaces=['redfish'])
|
||||||
|
# Redfish specific configurations
|
||||||
|
self.config(connection_attempts=1, group='redfish')
|
||||||
|
self.node = obj_utils.create_test_node(
|
||||||
|
self.context, driver='redfish', driver_info=INFO_DICT)
|
||||||
|
self.parsed_driver_info = {
|
||||||
|
'address': 'https://example.com',
|
||||||
|
'system_id': '/redfish/v1/Systems/FAKESYSTEM',
|
||||||
|
'username': 'username',
|
||||||
|
'password': 'password',
|
||||||
|
'verify_ca': True,
|
||||||
|
'auth_type': 'auto',
|
||||||
|
'node_uuid': self.node.uuid
|
||||||
|
}
|
||||||
|
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||||
'SessionCache._sessions', {})
|
'SessionCache._sessions', {})
|
||||||
@ -359,22 +359,87 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
|
|||||||
auth=mock_basic_auth.return_value
|
auth=mock_basic_auth.return_value
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_update_service(self):
|
|
||||||
redfish_utils._get_connection = mock.Mock()
|
|
||||||
mock_update_service = mock.Mock()
|
|
||||||
redfish_utils._get_connection.return_value = mock_update_service
|
|
||||||
|
|
||||||
result = redfish_utils.get_update_service(self.node)
|
class RedfishUtilsSystemTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
self.assertEqual(mock_update_service, result)
|
def setUp(self):
|
||||||
|
super(RedfishUtilsSystemTestCase, self).setUp()
|
||||||
|
# Default configurations
|
||||||
|
self.config(enabled_hardware_types=['redfish'],
|
||||||
|
enabled_power_interfaces=['redfish'],
|
||||||
|
enabled_boot_interfaces=['redfish-virtual-media'],
|
||||||
|
enabled_management_interfaces=['redfish'])
|
||||||
|
# Redfish specific configurations
|
||||||
|
self.config(connection_attempts=1, group='redfish')
|
||||||
|
self.node = obj_utils.create_test_node(
|
||||||
|
self.context, driver='redfish', driver_info=INFO_DICT)
|
||||||
|
self.parsed_driver_info = {
|
||||||
|
'address': 'https://example.com',
|
||||||
|
'system_id': '/redfish/v1/Systems/FAKESYSTEM',
|
||||||
|
'username': 'username',
|
||||||
|
'password': 'password',
|
||||||
|
'verify_ca': True,
|
||||||
|
'auth_type': 'auto',
|
||||||
|
'node_uuid': self.node.uuid
|
||||||
|
}
|
||||||
|
|
||||||
def test_get_update_service_error(self):
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
redfish_utils._get_connection = mock.Mock()
|
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||||
redfish_utils._get_connection.side_effect =\
|
'SessionCache._sessions', {})
|
||||||
sushy.exceptions.MissingAttributeError
|
def test_get_system(self, mock_sushy):
|
||||||
|
fake_conn = mock_sushy.return_value
|
||||||
|
fake_system = fake_conn.get_system.return_value
|
||||||
|
response = redfish_utils.get_system(self.node)
|
||||||
|
self.assertEqual(fake_system, response)
|
||||||
|
fake_conn.get_system.assert_called_once_with(
|
||||||
|
'/redfish/v1/Systems/FAKESYSTEM')
|
||||||
|
|
||||||
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
|
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||||
|
'SessionCache._sessions', {})
|
||||||
|
def test_get_system_resource_not_found(self, mock_sushy):
|
||||||
|
fake_conn = mock_sushy.return_value
|
||||||
|
fake_conn.get_system.side_effect = (
|
||||||
|
sushy.exceptions.ResourceNotFoundError('GET',
|
||||||
|
'/',
|
||||||
|
requests.Response()))
|
||||||
|
|
||||||
self.assertRaises(exception.RedfishError,
|
self.assertRaises(exception.RedfishError,
|
||||||
redfish_utils.get_update_service, self.node)
|
redfish_utils.get_system, self.node)
|
||||||
|
fake_conn.get_system.assert_called_once_with(
|
||||||
|
'/redfish/v1/Systems/FAKESYSTEM')
|
||||||
|
|
||||||
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
|
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||||
|
'SessionCache._sessions', {})
|
||||||
|
def test_get_system_multiple_systems(self, mock_sushy):
|
||||||
|
self.node.driver_info.pop('redfish_system_id')
|
||||||
|
fake_conn = mock_sushy.return_value
|
||||||
|
redfish_utils.get_system(self.node)
|
||||||
|
fake_conn.get_system.assert_called_once_with(None)
|
||||||
|
|
||||||
|
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
||||||
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
|
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||||
|
'SessionCache._sessions', {})
|
||||||
|
def test_get_system_resource_connection_error_retry(self, mock_sushy):
|
||||||
|
# Redfish specific configurations
|
||||||
|
self.config(connection_attempts=3, group='redfish')
|
||||||
|
|
||||||
|
fake_conn = mock_sushy.return_value
|
||||||
|
fake_conn.get_system.side_effect = sushy.exceptions.ConnectionError()
|
||||||
|
|
||||||
|
self.assertRaises(exception.RedfishConnectionError,
|
||||||
|
redfish_utils.get_system, self.node)
|
||||||
|
|
||||||
|
expected_get_system_calls = [
|
||||||
|
mock.call(self.parsed_driver_info['system_id']),
|
||||||
|
mock.call(self.parsed_driver_info['system_id']),
|
||||||
|
mock.call(self.parsed_driver_info['system_id']),
|
||||||
|
]
|
||||||
|
fake_conn.get_system.assert_has_calls(expected_get_system_calls)
|
||||||
|
self.assertEqual(fake_conn.get_system.call_count,
|
||||||
|
redfish_utils.CONF.redfish.connection_attempts)
|
||||||
|
|
||||||
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
||||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
@ -19,6 +20,7 @@ from oslo_utils import importutils
|
|||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
from ironic.drivers.modules.redfish import boot as redfish_boot
|
from ironic.drivers.modules.redfish import boot as redfish_boot
|
||||||
|
from ironic.drivers.modules.redfish import utils as redfish_utils
|
||||||
from ironic.drivers.modules.redfish import vendor as redfish_vendor
|
from ironic.drivers.modules.redfish import vendor as redfish_vendor
|
||||||
from ironic.tests.unit.db import base as db_base
|
from ironic.tests.unit.db import base as db_base
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
@ -81,3 +83,211 @@ class RedfishVendorPassthruTestCase(db_base.DbTestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exception.InvalidParameterValue,
|
exception.InvalidParameterValue,
|
||||||
task.driver.vendor.validate, task, 'eject_vmedia', **kwargs)
|
task.driver.vendor.validate, task, 'eject_vmedia', **kwargs)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_validate_invalid_create_subscription(self,
|
||||||
|
mock_get_event_service):
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
kwargs = {'Destination': 10000}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidParameterValue,
|
||||||
|
task.driver.vendor.validate, task, 'create_subscription',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
kwargs = {'Context': 10}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidParameterValue,
|
||||||
|
task.driver.vendor.validate, task, 'create_subscription',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
kwargs = {'Protocol': 10}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidParameterValue,
|
||||||
|
task.driver.vendor.validate, task, 'create_subscription',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.get_event_types_for_subscription.return_value = \
|
||||||
|
['Alert']
|
||||||
|
kwargs = {'EventTypes': ['Other']}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidParameterValue,
|
||||||
|
task.driver.vendor.validate, task, 'create_subscription',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def test_validate_invalid_delete_subscription(self):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
kwargs = {} # Empty missing id key
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidParameterValue,
|
||||||
|
task.driver.vendor.validate, task, 'delete_subscription',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_delete_subscription(self, mock_get_event_service):
|
||||||
|
kwargs = {'id': '30'}
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.path.return_value = \
|
||||||
|
"/redfish/v1/EventService/Subscriptions/"
|
||||||
|
subscription = mock_subscriptions.get_member.return_value
|
||||||
|
subscription.delete.return_value = None
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
task.driver.vendor.delete_subscription(task, **kwargs)
|
||||||
|
|
||||||
|
self.assertTrue(subscription.delete.called)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_invalid_delete_subscription(self, mock_get_event_service):
|
||||||
|
kwargs = {'id': '30'}
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.path.return_value = \
|
||||||
|
"/redfish/v1/EventService/Subscriptions/"
|
||||||
|
uri = "/redfish/v1/EventService/Subscriptions/" + kwargs.get('id')
|
||||||
|
mock_subscriptions.get_member.side_effect = [
|
||||||
|
sushy.exceptions.ResourceNotFoundError('GET', uri, mock.Mock())
|
||||||
|
]
|
||||||
|
subscription = mock_subscriptions.get_member.return_value
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
self.assertRaises(exception.RedfishError,
|
||||||
|
task.driver.vendor.delete_subscription,
|
||||||
|
task, **kwargs)
|
||||||
|
self.assertFalse(subscription.delete.called)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_get_all_subscriptions_empty(self, mock_get_event_service):
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.json.return_value = {
|
||||||
|
"@odata.context": "<some context>",
|
||||||
|
"@odata.id": "/redfish/v1/EventService/Subscriptions",
|
||||||
|
"@odata.type": "#EventDestinationCollection",
|
||||||
|
"Description": "List of Event subscriptions",
|
||||||
|
"Members": [],
|
||||||
|
"Members@odata.count": 0,
|
||||||
|
"Name": "Event Subscriptions Collection"
|
||||||
|
}
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
output = task.driver.vendor.get_all_subscriptions(task)
|
||||||
|
self.assertEqual(len(output.return_value['Members']), 0)
|
||||||
|
mock_get_event_service.assert_called_once_with(task.node)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_get_all_subscriptions(self, mock_get_event_service):
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.json.return_value = {
|
||||||
|
"@odata.context": "<some context>",
|
||||||
|
"@odata.id": "/redfish/v1/EventService/Subscriptions",
|
||||||
|
"@odata.type": "#EventDestinationCollection.",
|
||||||
|
"Description": "List of Event subscriptions",
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"@odata.id": "/redfish/v1/EventService/Subscriptions/33/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Members@odata.count": 1,
|
||||||
|
"Name": "Event Subscriptions Collection"
|
||||||
|
}
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
output = task.driver.vendor.get_all_subscriptions(task)
|
||||||
|
self.assertEqual(len(output.return_value['Members']), 1)
|
||||||
|
mock_get_event_service.assert_called_once_with(task.node)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_get_subscription_does_not_exist(self, mock_get_event_service):
|
||||||
|
kwargs = {'id': '30'}
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.path.return_value = \
|
||||||
|
"/redfish/v1/EventService/Subscriptions/"
|
||||||
|
uri = "/redfish/v1/EventService/Subscriptions/" + kwargs.get('id')
|
||||||
|
mock_subscriptions.get_member.side_effect = [
|
||||||
|
sushy.exceptions.ResourceNotFoundError('GET', uri, mock.Mock())
|
||||||
|
]
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
self.assertRaises(exception.RedfishError,
|
||||||
|
task.driver.vendor.get_subscription,
|
||||||
|
task, **kwargs)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_create_subscription(self, mock_get_event_service):
|
||||||
|
subscription_json = {
|
||||||
|
"@odata.context": "",
|
||||||
|
"@odata.etag": "",
|
||||||
|
"@odata.id": "/redfish/v1/EventService/Subscriptions/100",
|
||||||
|
"@odata.type": "#EventDestination.v1_0_0.EventDestination",
|
||||||
|
"Id": "100",
|
||||||
|
"Context": "Ironic",
|
||||||
|
"Description": "iLO Event Subscription",
|
||||||
|
"Destination": "https://someurl",
|
||||||
|
"EventTypes": [
|
||||||
|
"Alert"
|
||||||
|
],
|
||||||
|
"HttpHeaders": [],
|
||||||
|
"Name": "Event Subscription",
|
||||||
|
"Oem": {
|
||||||
|
},
|
||||||
|
"Protocol": "Redfish"
|
||||||
|
}
|
||||||
|
mock_event_service = mock_get_event_service.return_value
|
||||||
|
|
||||||
|
subscription = mock.MagicMock()
|
||||||
|
subscription.json.return_value = subscription_json
|
||||||
|
mock_event_service.subscriptions.create = subscription
|
||||||
|
kwargs = {'destination': 'https://someurl'}
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
task.driver.vendor.create_subscription(task, **kwargs)
|
||||||
|
|
||||||
|
@mock.patch.object(redfish_utils, 'get_event_service', autospec=True)
|
||||||
|
def test_get_subscription_exists(self, mock_get_event_service):
|
||||||
|
kwargs = {'id': '36'}
|
||||||
|
mock_subscriptions = mock.MagicMock()
|
||||||
|
mock_evt_serv = mock_get_event_service.return_value
|
||||||
|
mock_evt_serv.subscriptions = mock_subscriptions
|
||||||
|
mock_subscriptions.path.return_value = \
|
||||||
|
"/redfish/v1/EventService/Subscriptions/"
|
||||||
|
subscription = mock_subscriptions.get_member.return_value
|
||||||
|
subscription.json.return_value = {
|
||||||
|
"@odata.context": "",
|
||||||
|
"@odata.etag": "",
|
||||||
|
"@odata.id": "/redfish/v1/EventService/Subscriptions/36",
|
||||||
|
"@odata.type": "#EventDestination.v1_0_0.EventDestination",
|
||||||
|
"Id": "36",
|
||||||
|
"Context": "Ironic",
|
||||||
|
"Description": "iLO Event Subscription",
|
||||||
|
"Destination": "https://someurl",
|
||||||
|
"EventTypes": [
|
||||||
|
"Alert"
|
||||||
|
],
|
||||||
|
"HttpHeaders": [],
|
||||||
|
"Name": "Event Subscription",
|
||||||
|
"Oem": {
|
||||||
|
},
|
||||||
|
"Protocol": "Redfish"
|
||||||
|
}
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
task.driver.vendor.get_subscription(task, **kwargs)
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Provides new vendor passthru methods for Redfish to create, delete
|
||||||
|
and get subscriptions.
|
Loading…
Reference in New Issue
Block a user