Merge "Preliminary extension support + Name routing"
This commit is contained in:
commit
a4ddfd04f0
|
@ -0,0 +1,44 @@
|
|||
# Copyright 2017 Massachusetts Open Cloud
|
||||
#
|
||||
# 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.
|
||||
|
||||
from mixmatch import config
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = config.LOG
|
||||
|
||||
EXTENSION_MANAGER = None # type: extension.ExtensionManager
|
||||
|
||||
|
||||
def load_extensions():
|
||||
global EXTENSION_MANAGER
|
||||
|
||||
EXTENSION_MANAGER = extension.ExtensionManager(
|
||||
namespace='mixmatch.extend',
|
||||
invoke_on_load=True
|
||||
)
|
||||
|
||||
|
||||
def get_matched_extensions(request):
|
||||
"""Return list of matched extensions for request
|
||||
|
||||
:type request: Dict[]
|
||||
:rtype: List[mixmatch.extend.base.Extension]
|
||||
"""
|
||||
def _match(e):
|
||||
return e.obj if e.obj.matches(request) else None
|
||||
|
||||
result = EXTENSION_MANAGER.map(_match)
|
||||
return filter(bool, result)
|
|
@ -0,0 +1,65 @@
|
|||
# Copyright 2017 Massachusetts Open Cloud
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class Extension(object):
|
||||
ROUTES = []
|
||||
OPTS = []
|
||||
|
||||
def matches(self, request):
|
||||
for route in self.ROUTES:
|
||||
if route.match(request):
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_request(self, request):
|
||||
pass
|
||||
|
||||
def handle_response(self, response):
|
||||
pass
|
||||
|
||||
|
||||
class Route(object):
|
||||
def __init__(self, service=None, version=None, method=None, action=None):
|
||||
self.service = service
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.action = action
|
||||
|
||||
def _match_service(self, service):
|
||||
if self.service:
|
||||
return self.service == service
|
||||
return True
|
||||
|
||||
def _match_version(self, version):
|
||||
if self.version:
|
||||
return self.version == version
|
||||
return True
|
||||
|
||||
def _match_method(self, method):
|
||||
if self.method:
|
||||
return self.method == method
|
||||
return True
|
||||
|
||||
def _match_action(self, action):
|
||||
if self.action:
|
||||
# FIXME(knikolla): More sophisticated matching after PoC
|
||||
return self.action == action
|
||||
return True
|
||||
|
||||
def match(self, request):
|
||||
return (self._match_service(request['service']) and
|
||||
self._match_version(request['version']) and
|
||||
self._match_method(request['method']) and
|
||||
self._match_action(request['action']))
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright 2017 Massachusetts Open Cloud
|
||||
#
|
||||
# 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.
|
||||
|
||||
from mixmatch.extend import base
|
||||
from mixmatch.session import request as mm_request
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
|
||||
class NameRouting(base.Extension):
|
||||
|
||||
ROUTES = [
|
||||
base.Route(service='volume', version=None,
|
||||
action=['volumes'], method='POST'),
|
||||
base.Route(service='image', version=None,
|
||||
action=['images'], method='POST'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _is_targeted(headers):
|
||||
return 'MM-SERVICE-PROVIDER' in headers
|
||||
|
||||
def handle_request(self, request):
|
||||
if self._is_targeted(request['headers']):
|
||||
return
|
||||
|
||||
body = jsonutils.loads(mm_request.data)
|
||||
if request['service'] == 'image':
|
||||
if request['version'] == 'v1':
|
||||
name = request['headers'].get('X-IMAGE-META-NAME', '')
|
||||
else:
|
||||
name = body.get('name', '')
|
||||
elif request['service'] == 'volume':
|
||||
name = body['volume'].get('name', '')
|
||||
|
||||
name = name.split('@')
|
||||
if len(name) == 2:
|
||||
request['headers']['MM-SERVICE-PROVIDER'] = name[1]
|
|
@ -25,6 +25,7 @@ from mixmatch.session import app
|
|||
from mixmatch.session import chunked_reader
|
||||
from mixmatch.session import request
|
||||
from mixmatch import auth
|
||||
from mixmatch import extend
|
||||
from mixmatch import model
|
||||
from mixmatch import services
|
||||
from mixmatch import utils
|
||||
|
@ -51,9 +52,9 @@ def get_service(a):
|
|||
abort(404)
|
||||
|
||||
|
||||
def get_details(method, path, headers):
|
||||
def get_details(method, orig_path, headers):
|
||||
"""Get details for a request."""
|
||||
path = path[:]
|
||||
path = orig_path.split('/')
|
||||
# NOTE(knikolla): Request usually look like:
|
||||
# /<service>/<version>/<project_id:uuid>/<res_type>/<res_id:uuid>
|
||||
# or
|
||||
|
@ -66,7 +67,8 @@ def get_details(method, path, headers):
|
|||
'resource_type': utils.safe_pop(path), # this
|
||||
'resource_id': utils.pop_if_uuid(path), # and this
|
||||
'token': headers.get('X-AUTH-TOKEN', None),
|
||||
'headers': headers}
|
||||
'headers': dict(headers),
|
||||
'path': orig_path}
|
||||
|
||||
|
||||
def is_token_header_key(string):
|
||||
|
@ -93,16 +95,18 @@ def format_for_log(title=None, method=None, url=None, headers=None,
|
|||
class RequestHandler(object):
|
||||
|
||||
def __init__(self, method, path, headers):
|
||||
self.details = get_details(method, path.split('/'), headers)
|
||||
|
||||
self.details = get_details(method, path, headers)
|
||||
self.extensions = extend.get_matched_extensions(self.details)
|
||||
self._set_strip_details(self.details)
|
||||
|
||||
self.enabled_sps = filter(
|
||||
lambda sp: (self.details['service'] in
|
||||
service_providers.get(CONF, sp).enabled_services),
|
||||
CONF.service_providers
|
||||
)
|
||||
|
||||
for extension in self.extensions:
|
||||
extension.handle_request(self.details)
|
||||
|
||||
if not self.details['version']:
|
||||
if CONF.aggregation:
|
||||
# unversioned calls with no action
|
||||
|
@ -147,8 +151,8 @@ class RequestHandler(object):
|
|||
|
||||
LOG.info(format_for_log(title="Request to proxy",
|
||||
method=self.details['method'],
|
||||
url=path,
|
||||
headers=dict(headers)))
|
||||
url=self.details['path'],
|
||||
headers=dict(self.details['headers'])))
|
||||
|
||||
def _do_request_on(self, sp, project_id=None):
|
||||
headers = self._prepare_headers(self.details['headers'])
|
||||
|
@ -349,6 +353,7 @@ def proxy(path):
|
|||
def main():
|
||||
config.configure()
|
||||
model.create_tables()
|
||||
extend.load_extensions()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -19,6 +19,8 @@ from six.moves.urllib import parse
|
|||
|
||||
from mixmatch import config
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
|
@ -64,7 +66,7 @@ def aggregate(responses, key, service_type, version=None,
|
|||
|
||||
resource_list = []
|
||||
for location, response in responses.items():
|
||||
resources = json.loads(response.text)
|
||||
resources = jsonutils.loads(response.text)
|
||||
if type(resources) == dict:
|
||||
resource_list += resources[key]
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ from requests_mock.contrib import fixture as requests_fixture
|
|||
from oslo_config import fixture as config_fixture
|
||||
|
||||
from mixmatch import config
|
||||
from mixmatch import extend
|
||||
from mixmatch.proxy import app
|
||||
from mixmatch.model import BASE, enginefacade
|
||||
|
||||
|
@ -61,6 +62,7 @@ class BaseTest(testcase.TestCase):
|
|||
image_endpoint='http://images.remote1',
|
||||
volume_endpoint='http://volumes.remote1')
|
||||
config.post_config()
|
||||
extend.load_extensions()
|
||||
|
||||
def load_auth_fixtures(self):
|
||||
self.auth = FakeSession(token=uuid.uuid4().hex,
|
||||
|
|
|
@ -32,13 +32,45 @@ class TestImages(base.BaseTest):
|
|||
|
||||
def _construct_url(self, image_id='', sp=None):
|
||||
if not sp:
|
||||
prefix = '/image'
|
||||
url = '/image'
|
||||
else:
|
||||
prefix = self.service_providers[sp]['image_endpoint']
|
||||
url = self.service_providers[sp]['image_endpoint']
|
||||
url = '%s/v2/images' % url
|
||||
|
||||
return (
|
||||
'%s/v2/images/%s' % (prefix, image_id)
|
||||
if image_id:
|
||||
url = '%s/%s' % (url, image_id)
|
||||
|
||||
return url
|
||||
|
||||
def test_create_image(self):
|
||||
image_id = uuid.uuid4().hex
|
||||
self.requests_fixture.post(
|
||||
self._construct_url(sp='default'),
|
||||
request_headers=self.auth.get_headers(),
|
||||
text=six.u(image_id),
|
||||
headers={'CONTENT-TYPE': 'application/json'}
|
||||
)
|
||||
response = self.app.post(
|
||||
self._construct_url(),
|
||||
headers=self.auth.get_headers(),
|
||||
data=json.dumps({'name': 'local'})
|
||||
)
|
||||
self.assertEqual(six.b(image_id), response.data)
|
||||
|
||||
def test_create_image_routing(self):
|
||||
image_id = uuid.uuid4().hex
|
||||
self.requests_fixture.post(
|
||||
self._construct_url(sp='remote1'),
|
||||
request_headers=self.remote_auth.get_headers(),
|
||||
text=six.u(image_id),
|
||||
headers={'CONTENT-TYPE': 'application/json'}
|
||||
)
|
||||
response = self.app.post(
|
||||
self._construct_url(),
|
||||
headers=self.auth.get_headers(),
|
||||
data=json.dumps({'name': 'local@remote1'})
|
||||
)
|
||||
self.assertEqual(six.b(image_id), response.data)
|
||||
|
||||
def test_get_image_local(self):
|
||||
image_id = uuid.uuid4().hex
|
||||
|
|
|
@ -106,6 +106,36 @@ class TestVolumesV2(base.BaseTest):
|
|||
|
||||
return url
|
||||
|
||||
def test_create_volume(self):
|
||||
volume_id = uuid.uuid4().hex
|
||||
self.requests_fixture.post(
|
||||
self._construct_url(self.auth, sp='default'),
|
||||
request_headers=self.auth.get_headers(),
|
||||
text=six.u(volume_id),
|
||||
headers={'CONTENT-TYPE': 'application/json'}
|
||||
)
|
||||
response = self.app.post(
|
||||
self._construct_url(self.auth),
|
||||
headers=self.auth.get_headers(),
|
||||
data=json.dumps({'volume': {'name': 'local'}})
|
||||
)
|
||||
self.assertEqual(six.b(volume_id), response.data)
|
||||
|
||||
def test_create_volume_routing(self):
|
||||
volume_id = uuid.uuid4().hex
|
||||
self.requests_fixture.post(
|
||||
self._construct_url(self.remote_auth, sp='remote1'),
|
||||
request_headers=self.remote_auth.get_headers(),
|
||||
text=six.u(volume_id),
|
||||
headers={'CONTENT-TYPE': 'application/json'}
|
||||
)
|
||||
response = self.app.post(
|
||||
self._construct_url(self.auth),
|
||||
headers=self.auth.get_headers(),
|
||||
data=json.dumps({'volume': {'name': 'local@remote1'}})
|
||||
)
|
||||
self.assertEqual(six.b(volume_id), response.data)
|
||||
|
||||
def test_get_volume_local_mapping(self):
|
||||
volume_id = uuid.uuid4().hex
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
|
|||
oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0
|
||||
oslo.log>=3.22.0 # Apache-2.0
|
||||
oslo.db>=4.23.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0,!=2.19.1 # Apache-2.0
|
||||
keystoneauth1>=2.21.0 # Apache-2.0
|
||||
python-keystoneclient>=3.8.0 # Apache-2.0
|
||||
requests>=2.14.2 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
stevedore>=1.20.0 # Apache-2.0
|
||||
|
|
|
@ -24,12 +24,16 @@ classifier =
|
|||
[files]
|
||||
packages =
|
||||
mixmatch
|
||||
mixmatch.config
|
||||
mixmatch.extend
|
||||
data_files =
|
||||
etc/ = etc/*
|
||||
|
||||
[entry_points]
|
||||
oslo.config.opts =
|
||||
mixmatch = mixmatch.config:list_opts
|
||||
mixmatch.extend =
|
||||
name_routing = mixmatch.extend.name_routing:NameRouting
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
|
|
Loading…
Reference in New Issue