Merge "Preliminary extension support + Name routing"

This commit is contained in:
Jenkins 2017-07-21 21:40:28 +00:00 committed by Gerrit Code Review
commit a4ddfd04f0
10 changed files with 248 additions and 13 deletions

View File

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

65
mixmatch/extend/base.py Normal file
View File

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

View File

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

View File

@ -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__":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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