From 4bc5142df26c8279e1ab15703cbfba6849654c4b Mon Sep 17 00:00:00 2001
From: Iury Gregory Melo Ferreira <iurygregory@gmail.com>
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": "<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)
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.