From c7e3081bdbae104e0e5278747a88b8facb80dbc1 Mon Sep 17 00:00:00 2001 From: cenne Date: Sun, 2 May 2021 00:23:04 +0200 Subject: [PATCH] Implement driver vendor passthrough * Define 'list' and 'call' vendor_passthru methods in Driver * Add corresponding proxy methods * Implement unit tests - verify result properly returned for list_vendor_passthru - verify session is being called properly for both list and call Story: 2008193 Task: 40960 Change-Id: Id7e5f6d3f651c371efca29002de63c973e8f33d7 --- openstack/baremetal/v1/_proxy.py | 29 ++++++++ openstack/baremetal/v1/driver.py | 60 +++++++++++++++++ .../tests/unit/baremetal/v1/test_driver.py | 66 +++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 4f00e64be..2cbda567b 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -194,6 +194,35 @@ class Proxy(proxy.Proxy): """ return self._get(_driver.Driver, driver) + def list_driver_vendor_passthru(self, driver): + """Get driver's vendor_passthru methods. + + :param driver: The value can be the name of a driver or a + :class:`~openstack.baremetal.v1.driver.Driver` instance. + + :returns: One :dict: of vendor methods with corresponding usages + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + driver matching the name could be found. + """ + driver = self.get_driver(driver) + return driver.list_vendor_passthru(self) + + def call_driver_vendor_passthru(self, driver, + verb: str, method: str, body=None): + """Call driver's vendor_passthru method. + + :param driver: The value can be the name of a driver or a + :class:`~openstack.baremetal.v1.driver.Driver` instance. + :param verb: One of GET, POST, PUT, DELETE, + depending on the driver and method. + :param method: Name of vendor method. + :param body: passed to the vendor function as json body. + + :returns: Server response + """ + driver = self.get_driver(driver) + return driver.call_vendor_passthru(self, verb, method, body) + def nodes(self, details=False, **query): """Retrieve a generator of nodes. diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 00a31954d..91311ae72 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import _common +from openstack import exceptions from openstack import resource +from openstack import utils class Driver(resource.Resource): @@ -117,3 +120,60 @@ class Driver(resource.Resource): #: Enabled vendor interface implementations. #: Introduced in API microversion 1.30. enabled_vendor_interfaces = resource.Body("enabled_vendor_interfaces") + + def list_vendor_passthru(self, session): + """Fetch vendor specific methods exposed by driver + + :param session: The session to use for making this request. + :returns: A dict of the available vendor passthru methods for driver. + Method names keys and corresponding usages in dict form as values + Usage dict properties: + * ``async``: bool # Is passthru function invoked asynchronously + * ``attach``: bool # Is return value attached to response object + * ``description``: str # Description of what the method does + * ``http_methods``: list # List of HTTP methods supported + """ + session = self._get_session(session) + request = self._prepare_request() + request.url = utils.urljoin( + request.url, 'vendor_passthru', 'methods') + response = session.get(request.url, headers=request.headers) + + msg = ("Failed to list list vendor_passthru methods for {driver_name}" + .format(driver_name=self.name)) + exceptions.raise_from_response(response, error_message=msg) + return response.json() + + def call_vendor_passthru(self, session, + verb: str, method: str, body: dict = None): + """Call a vendor specific passthru method + + Contents of body are params passed to the hardware driver + function. Validation happens there. Missing parameters, or + excess parameters will cause the request to be rejected + + :param session: The session to use for making this request. + :param method: Vendor passthru method name. + :param verb: One of GET, POST, PUT, DELETE, + depending on the driver and method. + :param body: passed to the vendor function as json body. + :raises: :exc:`ValueError` if :data:`verb` is not one of + GET, POST, PUT, DELETE + :returns: response of method call. + """ + if verb.upper() not in ['GET', 'PUT', 'POST', 'DELETE']: + raise ValueError('Invalid verb: {}'.format(verb)) + + session = self._get_session(session) + request = self._prepare_request() + request.url = utils.urljoin( + request.url, f'vendor_passthru?method={method}') + call = getattr(session, verb.lower()) + response = call( + request.url, json=body, headers=request.headers, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed call to method {method} on driver {driver_name}" + .format(method=method, driver_name=self.name)) + exceptions.raise_from_response(response, error_message=msg) + return response diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index 364e0c1ae..e4d1f6731 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -10,7 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + +from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import driver +from openstack import exceptions from openstack.tests.unit import base @@ -63,3 +69,63 @@ class TestDriver(base.TestCase): self.assertEqual(FAKE['hosts'], sot.hosts) self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['properties'], sot.properties) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_list_vendor_passthru(self): + self.session = mock.Mock(spec=adapter.Adapter) + sot = driver.Driver(**FAKE) + fake_vendor_passthru_info = { + 'fake_vendor_method': { + 'async': True, + 'attach': False, + 'description': "Fake function that does nothing in background", + 'http_methods': ['GET', 'PUT', 'POST', 'DELETE'] + } + } + self.session.get.return_value.json.return_value = ( + fake_vendor_passthru_info) + result = sot.list_vendor_passthru(self.session) + self.session.get.assert_called_once_with( + 'drivers/{driver_name}/vendor_passthru/methods'.format( + driver_name=FAKE["name"]), + headers=mock.ANY) + self.assertEqual(result, fake_vendor_passthru_info) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_call_vendor_passthru(self): + self.session = mock.Mock(spec=adapter.Adapter) + sot = driver.Driver(**FAKE) + # GET + sot.call_vendor_passthru(self.session, 'GET', 'fake_vendor_method') + self.session.get.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json=None, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # PUT + sot.call_vendor_passthru(self.session, 'PUT', 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}) + self.session.put.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json={"fake_param_key": "fake_param_value"}, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # POST + sot.call_vendor_passthru(self.session, 'POST', 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}) + self.session.post.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json={"fake_param_key": "fake_param_value"}, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # DELETE + sot.call_vendor_passthru(self.session, 'DELETE', 'fake_vendor_method') + self.session.delete.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json=None, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES)