From 36384fa1901637ac7b86e00332aa8d6b0a1fb3bc Mon Sep 17 00:00:00 2001 From: cFouts Date: Wed, 26 Aug 2015 18:49:56 -0400 Subject: [PATCH] Manila REST API Microversion Support for Client Manila client needs to provide 'X-Openstack-Manila-Api-Version' in the HTTP request header along with the appropriate version. Manila client also needs to provide 'X-OpenStack-Manila-API-Experimental' in the HTTP request header for APIs that are marked as experimental. This is a temporary fix until Manila client can support Nova style microversions. Closes-bug: #1489450 Change-Id: I6344dfa6d272f0e813a4873c015be614ebeb4e7e --- manilaclient/api_versions.py | 27 +++++++++++++++++++ manilaclient/base.py | 8 ++++++ manilaclient/client.py | 8 ++++-- manilaclient/common/constants.py | 3 +++ manilaclient/httpclient.py | 30 +++++++++++++++++++--- manilaclient/shell.py | 9 ++++--- manilaclient/tests/unit/test_httpclient.py | 14 +++++++--- manilaclient/tests/unit/v1/fake_clients.py | 7 +++++ manilaclient/tests/unit/v1/fakes.py | 25 ++++++++++++++++++ manilaclient/tests/unit/v1/test_client.py | 17 +++++++----- manilaclient/tests/unit/v1/test_shell.py | 9 +++++++ manilaclient/v1/client.py | 9 ++++--- manilaclient/v1/services.py | 8 ++++++ manilaclient/v1/shell.py | 9 +++++++ 14 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 manilaclient/api_versions.py diff --git a/manilaclient/api_versions.py b/manilaclient/api_versions.py new file mode 100644 index 000000000..b52a871cb --- /dev/null +++ b/manilaclient/api_versions.py @@ -0,0 +1,27 @@ +# Copyright 2015 Chuck Fouts +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import manilaclient +from manilaclient.common import constants + + +def experimental_api(f): + """Adds to HTTP Header to indicate this is an experimental API call.""" + def _decorator(*args, **kwargs): + client = args[0] + if isinstance(client, manilaclient.v1.client.Client): + dh = client.client.default_headers + dh[constants.EXPERIMENTAL_HTTP_HEADER] = 'true' + f(*args, **kwargs) + return _decorator diff --git a/manilaclient/base.py b/manilaclient/base.py index 5cc2479e2..3763beda3 100644 --- a/manilaclient/base.py +++ b/manilaclient/base.py @@ -138,6 +138,14 @@ class Manager(utils.HookableMixin): else: return self.resource_class(self, body, loaded=True) + def _get_with_base_url(self, url, response_key=None): + resp, body = self.api.client.get_with_base_url(url) + if response_key: + return [self.resource_class(self, res, loaded=True) + for res in body[response_key] if res] + else: + return self.resource_class(self, body, loaded=True) + def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) diff --git a/manilaclient/client.py b/manilaclient/client.py index 2bc7ad6ce..670ee9d18 100644 --- a/manilaclient/client.py +++ b/manilaclient/client.py @@ -39,6 +39,10 @@ def get_client_class(version): return importutils.import_class(client_path) +def get_major_version(version): + return version.split('.')[0] + + def Client(version, *args, **kwargs): - client_class = get_client_class(version) - return client_class(*args, **kwargs) + client_class = get_client_class(get_major_version(version)) + return client_class(version, *args, **kwargs) diff --git a/manilaclient/common/constants.py b/manilaclient/common/constants.py index 71bb4846f..861449e01 100644 --- a/manilaclient/common/constants.py +++ b/manilaclient/common/constants.py @@ -38,3 +38,6 @@ SNAPSHOT_SORT_KEY_VALUES = ( 'progress', 'name', 'display_name', ) + +EXPERIMENTAL_HTTP_HEADER = 'X-OpenStack-Manila-API-Experimental' +MAX_API_VERSION = '1.4' diff --git a/manilaclient/httpclient.py b/manilaclient/httpclient.py index edf80b132..faf7ddf25 100644 --- a/manilaclient/httpclient.py +++ b/manilaclient/httpclient.py @@ -33,10 +33,12 @@ except ImportError: class HTTPClient(object): - def __init__(self, base_url, token, user_agent, + + def __init__(self, endpoint_url, token, user_agent, api_version, insecure=False, cacert=None, timeout=None, retries=None, http_log_debug=False): - self.base_url = base_url + self.endpoint_url = endpoint_url + self.base_url = self._get_base_url(self.endpoint_url) self.retries = retries self.http_log_debug = http_log_debug @@ -45,6 +47,7 @@ class HTTPClient(object): self.default_headers = { 'X-Auth-Token': token, + 'X-Openstack-Manila-Api-Version': api_version, 'User-Agent': user_agent, 'Accept': 'application/json', } @@ -57,6 +60,11 @@ class HTTPClient(object): if hasattr(requests, 'logging'): requests.logging.getLogger(requests.__name__).addHandler(ch) + def _get_base_url(self, url): + """Truncates url and returns transport, address, and port number.""" + base_url = '/'.join(url.split('/')[:3]) + '/' + return base_url + def _set_request_options(self, insecure, cacert, timeout=None): options = {'verify': True} @@ -98,13 +106,24 @@ class HTTPClient(object): return resp, body def _cs_request(self, url, method, **kwargs): + return self._cs_request_with_retries( + self.endpoint_url + url, + method, + **kwargs) + + def _cs_request_base_url(self, url, method, **kwargs): + return self._cs_request_with_retries( + self.base_url + url, + method, + **kwargs) + + def _cs_request_with_retries(self, url, method, **kwargs): attempts = 0 timeout = 1 while True: attempts += 1 try: - resp, body = self.request(self.base_url + url, method, - **kwargs) + resp, body = self.request(url, method, **kwargs) return resp, body except (exceptions.BadRequest, requests.exceptions.RequestException, @@ -124,6 +143,9 @@ class HTTPClient(object): sleep(timeout) timeout *= 2 + def get_with_base_url(self, url, **kwargs): + return self._cs_request_base_url(url, 'GET', **kwargs) + def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) diff --git a/manilaclient/shell.py b/manilaclient/shell.py index 02ac62f25..96b93cde8 100644 --- a/manilaclient/shell.py +++ b/manilaclient/shell.py @@ -33,13 +33,14 @@ from oslo_utils import encodeutils import six from manilaclient import client +from manilaclient.common import constants from manilaclient import exceptions as exc import manilaclient.extension from manilaclient.openstack.common import cliutils from manilaclient.v1 import shell as shell_v1 # from manilaclient.v2 import shell as shell_v2 -DEFAULT_OS_SHARE_API_VERSION = "1" +DEFAULT_OS_SHARE_API_VERSION = constants.MAX_API_VERSION DEFAULT_MANILA_ENDPOINT_TYPE = 'publicURL' DEFAULT_MANILA_SERVICE_TYPE = 'share' @@ -218,11 +219,11 @@ class OpenStackManilaShell(object): help=argparse.SUPPRESS) parser.add_argument('--os-share-api-version', - metavar='', + metavar='', default=cliutils.env( 'OS_SHARE_API_VERSION', default=DEFAULT_OS_SHARE_API_VERSION), - help='Accepts 1 or 2, defaults ' + help='Accepts 1.x to override default ' 'to env[OS_SHARE_API_VERSION].') parser.add_argument('--os_share_api_version', help=argparse.SUPPRESS) @@ -294,7 +295,7 @@ class OpenStackManilaShell(object): def _discover_via_contrib_path(self, version): module_path = os.path.dirname(os.path.abspath(__file__)) - version_str = "v%s" % version.replace('.', '_') + version_str = "v%s" % version.split('.')[0] ext_path = os.path.join(module_path, version_str, 'contrib') ext_glob = os.path.join(ext_path, "*.py") diff --git a/manilaclient/tests/unit/test_httpclient.py b/manilaclient/tests/unit/test_httpclient.py index 30cadabc8..6573c57c6 100644 --- a/manilaclient/tests/unit/test_httpclient.py +++ b/manilaclient/tests/unit/test_httpclient.py @@ -13,6 +13,7 @@ import mock import requests +from manilaclient.common import constants from manilaclient import exceptions from manilaclient import httpclient from manilaclient.tests.unit import utils @@ -72,7 +73,8 @@ retry_after_non_supporting_mock_request = mock.Mock( def get_authed_client(retries=0): cl = httpclient.HTTPClient("http://example.com", "token", fake_user_agent, - retries=retries, http_log_debug=True) + retries=retries, http_log_debug=True, + api_version=constants.MAX_API_VERSION) return cl @@ -85,9 +87,12 @@ class ClientTest(utils.TestCase): @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") - headers = {"X-Auth-Token": "token", - "User-Agent": fake_user_agent, - 'Accept': 'application/json', } + headers = { + "X-Auth-Token": "token", + "User-Agent": fake_user_agent, + "X-Openstack-Manila-Api-Version": constants.MAX_API_VERSION, + 'Accept': 'application/json', + } mock_request.assert_called_with( "GET", "http://example.com/hi", @@ -176,6 +181,7 @@ class ClientTest(utils.TestCase): "X-Auth-Token": "token", "Content-Type": "application/json", 'Accept': 'application/json', + "X-Openstack-Manila-Api-Version": constants.MAX_API_VERSION, "User-Agent": fake_user_agent } mock_request.assert_called_with( diff --git a/manilaclient/tests/unit/v1/fake_clients.py b/manilaclient/tests/unit/v1/fake_clients.py index ef693031e..b61d37811 100644 --- a/manilaclient/tests/unit/v1/fake_clients.py +++ b/manilaclient/tests/unit/v1/fake_clients.py @@ -36,8 +36,15 @@ class FakeHTTPClient(httpclient.HTTPClient): self.password = 'password' self.auth_url = 'auth_url' self.callstack = [] + self.base_url = 'localhost' def _cs_request(self, url, method, **kwargs): + return self._cs_request_with_retries(url, method, **kwargs) + + def _cs_request_base_url(self, url, method, **kwargs): + return self._cs_request_with_retries(url, method, **kwargs) + + def _cs_request_with_retries(self, url, method, **kwargs): # Check that certain things are called correctly if method in ['GET', 'DELETE']: assert 'body' not in kwargs diff --git a/manilaclient/tests/unit/v1/fakes.py b/manilaclient/tests/unit/v1/fakes.py index 17ebfaa99..73177feef 100644 --- a/manilaclient/tests/unit/v1/fakes.py +++ b/manilaclient/tests/unit/v1/fakes.py @@ -32,6 +32,31 @@ class FakeClient(fakes.FakeClient): class FakeHTTPClient(fakes.FakeHTTPClient): + def get_(self, **kw): + body = { + "versions": [ + { + "status": "CURRENT", + "updated": "2015-07-30T11:33:21Z", + "links": [ + { + "href": "http://docs.openstack.org/", + "type": "text/html", + "rel": "describedby", + }, + { + "href": "http://localhost:8786/v1/", + "rel": "self", + } + ], + "min_version": "1.0", + "version": "1.1", + "id": "v1.0", + } + ] + } + return (200, {}, body) + def get_shares_1234(self, **kw): share = {'share': {'id': 1234, 'name': 'sharename'}} return (200, {}, share) diff --git a/manilaclient/tests/unit/v1/test_client.py b/manilaclient/tests/unit/v1/test_client.py index 651a73d69..0dacfb912 100644 --- a/manilaclient/tests/unit/v1/test_client.py +++ b/manilaclient/tests/unit/v1/test_client.py @@ -14,6 +14,7 @@ import uuid from keystoneclient import session +from manilaclient.common import constants from manilaclient import exceptions from manilaclient.tests.unit import utils from manilaclient.v1 import client @@ -27,21 +28,24 @@ class ClientTest(utils.TestCase): base_url = uuid.uuid4().hex s = session.Session() - c = client.Client(session=s, service_catalog_url=base_url, - retries=retries, input_auth_token='token') + c = client.Client(session=s, api_version=constants.MAX_API_VERSION, + service_catalog_url=base_url, retries=retries, + input_auth_token='token') - self.assertEqual(base_url, c.client.base_url) + self.assertEqual(base_url, c.client.endpoint_url) self.assertEqual(retries, c.client.retries) def test_auth_via_token_invalid(self): self.assertRaises(exceptions.ClientException, client.Client, - input_auth_token='token') + api_version=constants.MAX_API_VERSION, + input_auth_token="token") def test_auth_via_token_and_session(self): s = session.Session() base_url = uuid.uuid4().hex c = client.Client(input_auth_token='token', - service_catalog_url=base_url, session=s) + service_catalog_url=base_url, session=s, + api_version=constants.MAX_API_VERSION) self.assertIsNotNone(c.client) self.assertIsNone(c.keystone_client) @@ -50,7 +54,8 @@ class ClientTest(utils.TestCase): base_url = uuid.uuid4().hex c = client.Client(input_auth_token='token', - service_catalog_url=base_url) + service_catalog_url=base_url, + api_version=constants.MAX_API_VERSION) self.assertIsNotNone(c.client) self.assertIsNone(c.keystone_client) diff --git a/manilaclient/tests/unit/v1/test_shell.py b/manilaclient/tests/unit/v1/test_shell.py index 493462086..4752e48e8 100644 --- a/manilaclient/tests/unit/v1/test_shell.py +++ b/manilaclient/tests/unit/v1/test_shell.py @@ -1152,3 +1152,12 @@ class ShellTest(test_utils.TestCase): cliutils.print_list.assert_called_with( mock.ANY, fields=["Name", "Host", "Backend", "Pool"]) + + @mock.patch.object(cliutils, 'print_list', mock.Mock()) + def test_api_version(self): + self.run_command('api-version') + self.assert_called('GET', '') + cliutils.print_list.assert_called_with( + mock.ANY, + ['ID', 'Status', 'Version', 'Min_version'], + field_labels=['ID', 'Status', 'Version', 'Minimum Version']) diff --git a/manilaclient/v1/client.py b/manilaclient/v1/client.py index e43657d36..4bad2a56d 100644 --- a/manilaclient/v1/client.py +++ b/manilaclient/v1/client.py @@ -58,9 +58,9 @@ class Client(object): >>> client.shares.list() ... """ - def __init__(self, username=None, api_key=None, project_id=None, - auth_url=None, insecure=False, timeout=None, tenant_id=None, - project_name=None, region_name=None, + def __init__(self, api_version, username=None, api_key=None, + project_id=None, auth_url=None, insecure=False, timeout=None, + tenant_id=None, project_name=None, region_name=None, endpoint_type='publicURL', extensions=None, service_type='share', service_name=None, retries=None, http_log_debug=False, input_auth_token=None, session=None, @@ -157,7 +157,8 @@ class Client(object): cacert=cacert, timeout=timeout, retries=retries, - http_log_debug=http_log_debug) + http_log_debug=http_log_debug, + api_version=api_version) self.limits = limits.LimitsManager(self) self.services = services.ServiceManager(self) diff --git a/manilaclient/v1/services.py b/manilaclient/v1/services.py index 27c1d3f62..30e3f8454 100644 --- a/manilaclient/v1/services.py +++ b/manilaclient/v1/services.py @@ -31,6 +31,10 @@ class Service(common_base.Resource): def __repr__(self): return "" % self.id + def api_version(self): + """Get api version.""" + return self.manager.api_version(self) + class ServiceManager(base.Manager): """Manage :class:`Service` resources.""" @@ -48,3 +52,7 @@ class ServiceManager(base.Manager): if query_string: query_string = "?%s" % query_string return self._list(RESOURCES_PATH + query_string, RESOURCES_NAME) + + def api_version(self): + """Get api version.""" + return self._get_with_base_url("", "versions") diff --git a/manilaclient/v1/shell.py b/manilaclient/v1/shell.py index 376d3c5be..0a95c1307 100644 --- a/manilaclient/v1/shell.py +++ b/manilaclient/v1/shell.py @@ -15,6 +15,7 @@ from __future__ import print_function + import os import sys import time @@ -149,6 +150,14 @@ def _extract_key_value_options(args, option_name): return result_dict +def do_api_version(cs, args): + """Display the API version information.""" + columns = ['ID', 'Status', 'Version', 'Min_version'] + column_labels = ['ID', 'Status', 'Version', 'Minimum Version'] + versions = cs.services.api_version() + cliutils.print_list(versions, columns, field_labels=column_labels) + + def do_endpoints(cs, args): """Discover endpoints that get returned from the authenticate services.""" catalog = cs.keystone_client.service_catalog.catalog