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
This commit is contained in:
cFouts 2015-08-26 18:49:56 -04:00 committed by Chuck Fouts
parent 80eac1e73c
commit 36384fa190
14 changed files with 159 additions and 24 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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='<compute-api-ver>',
metavar='<share-api-ver>',
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")

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'])

View File

@ -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)

View File

@ -31,6 +31,10 @@ class Service(common_base.Resource):
def __repr__(self):
return "<Service: %s>" % 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")

View File

@ -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