From 4bc5142df26c8279e1ab15703cbfba6849654c4b Mon Sep 17 00:00:00 2001 From: Iury Gregory Melo Ferreira Date: Fri, 16 Jul 2021 16:02:09 +0200 Subject: [PATCH] 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 --- driver-requirements.txt | 2 +- ironic/drivers/modules/redfish/utils.py | 17 ++ ironic/drivers/modules/redfish/vendor.py | 192 ++++++++++++++++ .../drivers/modules/redfish/test_utils.py | 203 +++++++++++------ .../drivers/modules/redfish/test_vendor.py | 210 ++++++++++++++++++ ...assthru-subscription-5d28a2420e2af111.yaml | 5 + 6 files changed, 559 insertions(+), 70 deletions(-) create mode 100644 releasenotes/notes/vendor-passthru-subscription-5d28a2420e2af111.yaml diff --git a/driver-requirements.txt b/driver-requirements.txt index e379bb99a3..8b2af33a29 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -11,7 +11,7 @@ python-dracclient>=5.1.0,<7.0.0 python-xclarityclient>=0.1.6 # The Redfish hardware type uses the Sushy library -sushy>=3.8.0 +sushy>=3.10.0 # Ansible-deploy interface ansible>=2.7 diff --git a/ironic/drivers/modules/redfish/utils.py b/ironic/drivers/modules/redfish/utils.py index 2bd8abe36a..9915dba116 100644 --- a/ironic/drivers/modules/redfish/utils.py +++ b/ironic/drivers/modules/redfish/utils.py @@ -263,6 +263,23 @@ def get_update_service(node): 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): """Get a Redfish System that represents a node. diff --git a/ironic/drivers/modules/redfish/vendor.py b/ironic/drivers/modules/redfish/vendor.py index 49d29f0f0d..e4849f59e3 100644 --- a/ironic/drivers/modules/redfish/vendor.py +++ b/ironic/drivers/modules/redfish/vendor.py @@ -16,6 +16,9 @@ Vendor Interface for Redfish drivers and its supporting methods. """ 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.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 utils as redfish_utils +sushy = importutils.try_import('sushy') + +LOG = log.getLogger(__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): @@ -49,6 +59,12 @@ class RedfishVendorPassthru(base.VendorInterface): if method == 'eject_vmedia': self._validate_eject_vmedia(task, kwargs) 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) 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 boot_device = kwargs.get('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) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py index 837e8a804c..9bf59532fb 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_utils.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py @@ -168,64 +168,6 @@ class RedfishUtilsTestCase(db_base.DbTestCase): response = redfish_utils.parse_driver_info(self.node) 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): redfish_utils._get_connection = mock.Mock() fake_monitor = mock.Mock() @@ -245,6 +187,64 @@ class RedfishUtilsTestCase(db_base.DbTestCase): self.assertRaises(exception.RedfishError, 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('ironic.drivers.modules.redfish.utils.' 'SessionCache._sessions', {}) @@ -359,22 +359,87 @@ class RedfishUtilsTestCase(db_base.DbTestCase): 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): - redfish_utils._get_connection = mock.Mock() - redfish_utils._get_connection.side_effect =\ - sushy.exceptions.MissingAttributeError + @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_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(sushy, 'Sushy', autospec=True) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_vendor.py b/ironic/tests/unit/drivers/modules/redfish/test_vendor.py index 0ca487c1e8..089464c36a 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_vendor.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_vendor.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. + from unittest import mock from oslo_utils import importutils @@ -19,6 +20,7 @@ from oslo_utils import importutils from ironic.common import exception from ironic.conductor import task_manager 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.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils @@ -81,3 +83,211 @@ class RedfishVendorPassthruTestCase(db_base.DbTestCase): self.assertRaises( exception.InvalidParameterValue, 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": "", + "@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": "", + "@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) diff --git a/releasenotes/notes/vendor-passthru-subscription-5d28a2420e2af111.yaml b/releasenotes/notes/vendor-passthru-subscription-5d28a2420e2af111.yaml new file mode 100644 index 0000000000..9a4a6ed56a --- /dev/null +++ b/releasenotes/notes/vendor-passthru-subscription-5d28a2420e2af111.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Provides new vendor passthru methods for Redfish to create, delete + and get subscriptions.