Volume CRD through Cinder API gateway

This patch is to implement basic volume operation: post/get_one
/get_all/detail/delete through Cinder API gateway

Some TODO work was left in the code for later implementation

BP: https://blueprints.launchpad.net/tricircle/+spec/implement-stateless

Change-Id: Id338894592a2522dc3e138d754d278c604b21758
Signed-off-by: Chaoyi Huang <joehuang@huawei.com>
This commit is contained in:
Chaoyi Huang 2016-01-08 16:46:39 +08:00
parent b1991481c8
commit 5e886aebda
13 changed files with 1635 additions and 25 deletions

View File

@ -104,3 +104,11 @@ will be used.
```
nova boot --flavor 1 --image $image_id --nic net-id=$net_id --availability-zone az1 vm1
```
- 9 Create, list, show and delete volume.
```
cinder --debug create --availability-zone=az1 1
cinder --debug list
cinder --debug show $volume_id
cinder --debug delete $volume_id
cinder --debug list
```

View File

@ -44,19 +44,7 @@ function create_nova_apigw_accounts {
local tricircle_nova_apigw=$(get_or_create_service "nova" \
"compute" "Nova Compute Service")
local endpoint_id
interface_list="public admin internal"
for interface in $interface_list; do
endpoint_id=$(openstack endpoint list \
--service "$tricircle_nova_apigw" \
--interface "$interface" \
--region "$REGION_NAME" \
-c ID -f value)
if [[ -n "$endpoint_id" ]]; then
# Delete endpoint
openstack endpoint delete "$endpoint_id"
fi
done
remove_old_endpoint_conf $tricircle_nova_apigw
get_or_create_endpoint $tricircle_nova_apigw \
"$REGION_NAME" \
@ -80,16 +68,40 @@ function create_cinder_apigw_accounts {
if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then
local tricircle_cinder_apigw=$(get_or_create_service "cinder" \
"volume" "Cinder Volume Service")
"volumev2" "Cinder Volume Service")
remove_old_endpoint_conf $tricircle_cinder_apigw
get_or_create_endpoint $tricircle_cinder_apigw \
"$REGION_NAME" \
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/" \
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/" \
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s' \
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s' \
"$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s'
fi
fi
}
# common config-file configuration for tricircle services
function remove_old_endpoint_conf {
local service=$1
local endpoint_id
interface_list="public admin internal"
for interface in $interface_list; do
endpoint_id=$(openstack endpoint list \
--service "$service" \
--interface "$interface" \
--region "$REGION_NAME" \
-c ID -f value)
if [[ -n "$endpoint_id" ]]; then
# Delete endpoint
openstack endpoint delete "$endpoint_id"
fi
done
}
# create_tricircle_cache_dir() - Set up cache dir for tricircle
function create_tricircle_cache_dir {
@ -308,6 +320,12 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then
create_cinder_apigw_accounts
run_process t-cgw "python $TRICIRCLE_CINDER_APIGW --config-file $TRICIRCLE_CINDER_APIGW_CONF"
get_or_create_endpoint "volumev2" \
"$POD_REGION_NAME" \
"$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s' \
"$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s' \
"$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s'
fi
if is_service_enabled t-job; then

View File

@ -17,6 +17,8 @@ import pecan
import oslo_log.log as logging
from tricircle.cinder_apigw.controllers import volume
LOG = logging.getLogger(__name__)
@ -61,12 +63,20 @@ class V2Controller(object):
def __init__(self):
self.sub_controllers = {
self.resource_controller = {
'volumes': volume.VolumeController,
}
for name, ctrl in self.sub_controllers.items():
setattr(self, name, ctrl)
@pecan.expose()
def _lookup(self, tenant_id, *remainder):
if not remainder:
pecan.abort(404)
return
resource = remainder[0]
if resource not in self.resource_controller:
pecan.abort(404)
return
return self.resource_controller[resource](tenant_id), remainder[1:]
@pecan.expose(generic=True, template='json')
def index(self):

View File

@ -0,0 +1,332 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# 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 pecan
from pecan import expose
from pecan import request
from pecan import response
from pecan import Response
from pecan import rest
from oslo_log import log as logging
from oslo_serialization import jsonutils
from tricircle.common import az_ag
from tricircle.common import constants as cons
import tricircle.common.context as t_context
from tricircle.common import httpclient as hclient
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
LOG = logging.getLogger(__name__)
class VolumeController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if 'volume' not in kw:
pecan.abort(400, _('Volume not found in request body'))
return
if 'availability_zone' not in kw['volume']:
pecan.abort(400, _('Availability zone not set in request'))
return
pod = az_ag.get_pod_by_az_tenant(
context,
az_name=kw['volume']['availability_zone'],
tenant_id=self.tenant_id)
if not pod:
pecan.abort(500, _('Pod not configured or scheduling failure'))
LOG.error(_LE("Pod not configured or scheduling failure"))
return
t_pod = db_api.get_top_pod(context)
if not t_pod:
pecan.abort(500, _('Top Pod not configured'))
LOG.error(_LE("Top Po not configured"))
return
# TODO(joehuang): get release from pod configuration,
# to convert the content
# b_release = pod['release']
# t_release = t_pod['release']
t_release = 'Mitaka'
b_release = 'Mitaka'
s_ctx = hclient.get_pod_service_ctx(
context,
request.url,
pod['pod_name'],
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
pecan.abort(500, _('bottom pod endpoint incorrect'))
LOG.error(_LE("bottom pod endpoint incorrect %s") %
pod['pod_name'])
return
b_headers = self._convert_header(t_release,
b_release,
request.headers)
t_vol = kw['volume']
# add or remove key-value in the request for diff. version
b_vol_req = self._convert_object(t_release, b_release, t_vol,
res_type=cons.RT_VOLUME)
# convert az to the configured one
# remove the AZ parameter to bottom request for default one
b_vol_req['availability_zone'] = pod['pod_az_name']
if b_vol_req['availability_zone'] == '':
b_vol_req.pop("availability_zone", None)
b_body = jsonutils.dumps({'volume': b_vol_req})
resp = hclient.forward_req(
context,
'POST',
b_headers,
s_ctx['b_url'],
b_body)
b_status = resp.status_code
b_ret_body = jsonutils.loads(resp.content)
# build routing and convert response from the bottom pod
# for different version.
response.status = b_status
if b_status == 202:
if b_ret_body.get('volume') is not None:
b_vol_ret = b_ret_body['volume']
try:
with context.session.begin():
core.create_resource(
context, models.ResourceRouting,
{'top_id': b_vol_ret['id'],
'bottom_id': b_vol_ret['id'],
'pod_id': pod['pod_id'],
'project_id': self.tenant_id,
'resource_type': cons.RT_VOLUME})
except Exception as e:
LOG.error(_LE('Fail to create volume: %(exception)s'),
{'exception': e})
return Response(_('Failed to create volume'), 500)
ret_vol = self._convert_object(b_release, t_release,
b_vol_ret,
res_type=cons.RT_VOLUME)
ret_vol['availability_zone'] = pod['az_name']
return {'volume': ret_vol}
return {'error': b_ret_body}
@expose(generic=True, template='json')
def get_one(self, _id):
context = t_context.extract_context_from_environ()
if _id == 'detail':
return {'volumes': self._get_all(context)}
# TODO(joehuang): get the release of top and bottom
t_release = 'MITATA'
b_release = 'MITATA'
b_headers = self._convert_header(t_release,
b_release,
request.headers)
s_ctx = self._get_res_routing_ref(context, _id, request.url)
if not s_ctx:
return Response(_('Failed to find resource'), 404)
if s_ctx['b_url'] == '':
return Response(_('bottom pod endpoint incorrect'), 404)
resp = hclient.forward_req(context, 'GET',
b_headers,
s_ctx['b_url'],
request.body)
b_ret_body = jsonutils.loads(resp.content)
b_status = resp.status_code
response.status = b_status
if b_status == 200:
if b_ret_body.get('volume') is not None:
b_vol_ret = b_ret_body['volume']
ret_vol = self._convert_object(b_release, t_release,
b_vol_ret,
res_type=cons.RT_VOLUME)
pod = self._get_pod_by_top_id(context, _id)
if pod:
ret_vol['availability_zone'] = pod['az_name']
return {'volume': ret_vol}
# resource not find but routing exist, remove the routing
if b_status == 404:
filters = [{'key': 'top_id', 'comparator': 'eq', 'value': _id},
{'key': 'resource_type',
'comparator': 'eq',
'value': cons.RT_VOLUME}]
with context.session.begin():
core.delete_resources(context,
models.ResourceRouting,
filters)
return b_ret_body
@expose(generic=True, template='json')
def get_all(self):
# TODO(joehuang): here should return link instead,
# now combined with 'detail'
context = t_context.extract_context_from_environ()
return {'volumes': self._get_all(context)}
def _get_all(self, context):
# TODO(joehuang): query optimization for pagination, sort, etc
ret = []
pods = az_ag.list_pods_by_tenant(context, self.tenant_id)
for pod in pods:
if pod['pod_name'] == '':
continue
s_ctx = hclient.get_pod_service_ctx(
context,
request.url,
pod['pod_name'],
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
LOG.error(_LE("bottom pod endpoint incorrect %s")
% pod['pod_name'])
continue
# TODO(joehuang): convert header and body content
resp = hclient.forward_req(context, 'GET',
request.headers,
s_ctx['b_url'],
request.body)
if resp.status_code == 200:
routings = db_api.get_bottom_mappings_by_tenant_pod(
context, self.tenant_id,
pod['pod_id'], cons.RT_VOLUME
)
b_ret_body = jsonutils.loads(resp.content)
if b_ret_body.get('volumes'):
for vol in b_ret_body['volumes']:
if not routings.get(vol['id']):
b_ret_body['volumes'].remove(vol)
continue
vol['availability_zone'] = pod['az_name']
ret.extend(b_ret_body['volumes'])
return ret
@expose(generic=True, template='json')
def delete(self, _id):
context = t_context.extract_context_from_environ()
# TODO(joehuang): get the release of top and bottom
t_release = 'MITATA'
b_release = 'MITATA'
s_ctx = self._get_res_routing_ref(context, _id, request.url)
if not s_ctx:
return Response(_('Failed to find resource'), 404)
if s_ctx['b_url'] == '':
return Response(_('bottom pod endpoint incorrect'), 404)
b_headers = self._convert_header(t_release,
b_release,
request.headers)
resp = hclient.forward_req(context, 'DELETE',
b_headers,
s_ctx['b_url'],
request.body)
response.status = resp.status_code
# don't remove the resource routing for delete is async. operation
# remove the routing when query is executed but not find
# No content in the resp actually
return {}
# move to common function if other modules need
def _get_res_routing_ref(self, context, _id, t_url):
pod = self._get_pod_by_top_id(context, _id)
if not pod:
return None
pod_name = pod['pod_name']
s_ctx = hclient.get_pod_service_ctx(
context,
t_url,
pod_name,
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
LOG.error(_LE("bottom pod endpoint incorrect %s") %
pod_name)
return s_ctx
# move to common function if other modules need
def _get_pod_by_top_id(self, context, _id):
mappings = db_api.get_bottom_mappings_by_top_id(
context, _id,
cons.RT_VOLUME)
if not mappings or len(mappings) != 1:
return None
return mappings[0][0]
def _convert_header(self, from_release, to_release, header):
return header
def _convert_object(self, from_release, to_release, res_object,
res_type=cons.RT_VOLUME):
return res_object

View File

@ -13,10 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_log import log as logging
from oslo_utils import uuidutils
from tricircle.common.i18n import _LE
from tricircle.db import api as db_api
from tricircle.db import core
from tricircle.db import models
LOG = logging.getLogger(__name__)
def create_ag_az(context, ag_name, az_name):
aggregate = core.create_resource(context, models.Aggregate,
@ -100,3 +107,59 @@ def get_all_ag(context, filters=None, sorts=None):
aggregate.update(extra_fields)
return aggregates
def get_pod_by_az_tenant(context, az_name, tenant_id):
pod_bindings = core.query_resource(context,
models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': tenant_id}],
[])
if pod_bindings:
for pod_b in pod_bindings:
pod = core.get_resource(context,
models.Pod,
pod_b['pod_id'])
if pod['az_name'] == az_name:
return pod
# TODO(joehuang): schedule one dynamicly in the future
filters = [{'key': 'az_name', 'comparator': 'eq', 'value': az_name}]
pods = db_api.list_pods(context, filters=filters)
for pod in pods:
if pod['pod_name'] != '' and az_name != '':
try:
with context.session.begin():
core.create_resource(
context, models.PodBinding,
{'id': uuidutils.generate_uuid(),
'tenant_id': tenant_id,
'pod_id': pod['pod_id']})
return pod
except Exception as e:
LOG.error(_LE('Fail to create pod binding: %(exception)s'),
{'exception': e})
return None
return None
def list_pods_by_tenant(context, tenant_id):
pod_bindings = core.query_resource(context,
models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': tenant_id}],
[])
pods = []
if pod_bindings:
for pod_b in pod_bindings:
pod = core.get_resource(context,
models.Pod,
pod_b['pod_id'])
pods.append(pod)
return pods

View File

@ -0,0 +1,39 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
#
# 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.
# service type
ST_NOVA = 'nova'
# only support cinder v2
ST_CINDER = 'cinderv2'
ST_NEUTRON = 'neutron'
ST_GLANCE = 'glance'
# resource_type
RT_SERVER = 'server'
RT_VOLUME = 'volume'
RT_BACKUP = 'backup'
RT_SNAPSHOT = 'snapshot'
RT_NETWORK = 'network'
RT_SUBNET = 'subnet'
RT_PORT = 'port'
# version list
NOVA_VERSION_V21 = 'v2.1'
CINDER_VERSION_V2 = 'v2'
NEUTRON_VERSION_V2 = 'v2'
# supported release
R_LIBERTY = 'liberty'
R_MITAKA = 'mitaka'

View File

@ -0,0 +1,138 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# 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 urlparse
from requests import Request
from requests import Session
from tricircle.common import client
from tricircle.common import constants as cons
from tricircle.db import api as db_api
# the url could be endpoint registered in the keystone
# or url sent to tricircle service, which is stored in
# pecan.request.url
def get_version_from_url(url):
components = urlparse.urlsplit(url)
path = components.path
pos = path.find('/')
ver = ''
if pos == 0:
path = path[1:]
i = path.find('/')
if i >= 0:
ver = path[:i]
else:
ver = path
elif pos > 0:
ver = path[:pos]
else:
ver = path
return ver
def get_bottom_url(t_ver, t_url, b_ver, b_endpoint):
"""get_bottom_url
convert url received by Tricircle service to bottom OpenStack
request url through the configured endpoint in the KeyStone
:param t_ver: version of top service
:param t_url: request url to the top service
:param b_ver: version of bottom service
:param b_endpoint: endpoint registered in keystone for bottom service
:return: request url to bottom service
"""
t_parse = urlparse.urlsplit(t_url)
after_ver = t_parse.path
remove_ver = '/' + t_ver + '/'
pos = after_ver.find(remove_ver)
if pos == 0:
after_ver = after_ver[len(remove_ver):]
else:
remove_ver = t_ver + '/'
pos = after_ver.find(remove_ver)
if pos == 0:
after_ver = after_ver[len(remove_ver):]
if after_ver == t_parse.path:
# wrong t_url
return ''
b_parse = urlparse.urlsplit(b_endpoint)
scheme = b_parse.scheme
netloc = b_parse.netloc
path = '/' + b_ver + '/' + after_ver
if b_ver == '':
path = '/' + after_ver
query = t_parse.query
fragment = t_parse.fragment
b_url = urlparse.urlunsplit((scheme,
netloc,
path,
query,
fragment))
return b_url
def get_pod_service_endpoint(context, pod_name, st):
pod = db_api.get_pod_by_name(context, pod_name)
if pod:
c = client.Client()
return c.get_endpoint(context, pod['pod_id'], st)
return ''
def get_pod_service_ctx(context, t_url, pod_name, s_type=cons.ST_NOVA):
t_ver = get_version_from_url(t_url)
b_endpoint = get_pod_service_endpoint(context,
pod_name,
s_type)
b_ver = get_version_from_url(b_endpoint)
b_url = ''
if b_endpoint != '':
b_url = get_bottom_url(t_ver, t_url, b_ver, b_endpoint)
return {'t_ver': t_ver, 'b_ver': b_ver,
't_url': t_url, 'b_url': b_url}
def forward_req(context, action, b_headers, b_url, b_body):
s = Session()
req = Request(action, b_url,
data=b_body,
headers=b_headers)
prepped = req.prepare()
# do something with prepped.body
# do something with prepped.headers
resp = s.send(prepped,
timeout=60)
return resp

View File

@ -26,8 +26,10 @@ from oslo_config import cfg
from oslo_log import log as logging
from requests import exceptions as r_exceptions
from tricircle.common import constants as cons
from tricircle.common import exceptions
client_opts = [
cfg.IntOpt('cinder_timeout',
default=60,
@ -77,7 +79,7 @@ class ResourceHandle(object):
class GlanceResourceHandle(ResourceHandle):
service_type = 'glance'
service_type = cons.ST_GLANCE
support_resource = {'image': LIST | GET}
def _get_client(self, cxt):
@ -113,7 +115,7 @@ class GlanceResourceHandle(ResourceHandle):
class NeutronResourceHandle(ResourceHandle):
service_type = 'neutron'
service_type = cons.ST_NEUTRON
support_resource = {'network': LIST | CREATE | DELETE | GET,
'subnet': LIST | CREATE | DELETE | GET,
'port': LIST | CREATE | DELETE | GET,
@ -176,7 +178,7 @@ class NeutronResourceHandle(ResourceHandle):
class NovaResourceHandle(ResourceHandle):
service_type = 'nova'
service_type = cons.ST_NOVA
support_resource = {'flavor': LIST,
'server': LIST | CREATE | GET,
'aggregate': LIST | CREATE | DELETE | ACTION}
@ -257,7 +259,7 @@ class NovaResourceHandle(ResourceHandle):
class CinderResourceHandle(ResourceHandle):
service_type = 'cinder'
service_type = cons.ST_CINDER
support_resource = {'volume': GET | ACTION,
'transfer': CREATE | ACTION}

View File

@ -97,6 +97,38 @@ def get_bottom_mappings_by_top_id(context, top_id, resource_type):
return mappings
def get_bottom_mappings_by_tenant_pod(context,
tenant_id,
pod_id,
resource_type):
"""Get resource routing for specific tenant and pod
:param context: context object
:param tenant_id: tenant id to look up
:param pod_id: pod to look up
:param resource_type: specific resource
:return: a dic {top_id : route}
"""
route_filters = [{'key': 'pod_id',
'comparator': 'eq',
'value': pod_id},
{'key': 'project_id',
'comparator': 'eq',
'value': tenant_id},
{'key': 'resource_type',
'comparator': 'eq',
'value': resource_type}]
routings = {}
with context.session.begin():
routes = core.query_resource(
context, models.ResourceRouting, route_filters, [])
for _route in routes:
if not _route['bottom_id']:
continue
routings[_route['top_id']] = _route
return routings
def get_next_bottom_pod(context, current_pod_id=None):
pods = list_pods(context, sorts=[(models.Pod.pod_id, True)])
# NOTE(zhiyuan) number of pods is small, just traverse to filter top pod
@ -107,3 +139,30 @@ def get_next_bottom_pod(context, current_pod_id=None):
if pod['pod_id'] == current_pod_id and index < len(pods) - 1:
return pods[index + 1]
return None
def get_top_pod(context):
filters = [{'key': 'az_name', 'comparator': 'eq', 'value': ''}]
pods = list_pods(context, filters=filters)
# only one should be searched
for pod in pods:
if (pod['pod_name'] != '') and \
(pod['az_name'] == ''):
return pod
return None
def get_pod_by_name(context, pod_name):
filters = [{'key': 'pod_name', 'comparator': 'eq', 'value': pod_name}]
pods = list_pods(context, filters=filters)
# only one should be searched
for pod in pods:
if pod['pod_name'] == pod_name:
return pod
return None

View File

@ -0,0 +1,460 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# 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.
from mock import patch
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from requests import Response
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tricircle.cinder_apigw import app
from tricircle.common import constants as cons
from tricircle.common import context
from tricircle.common import httpclient as hclient
from tricircle.db import api as db_api
from tricircle.db import core
from tricircle.tests import base
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
FAKE_AZ = 'fake_az'
fake_volumes = []
def fake_volumes_forward_req(ctx, action, b_header, b_url, b_req_body):
resp = Response()
resp.status_code = 404
if action == 'POST':
b_body = jsonutils.loads(b_req_body)
if b_body.get('volume'):
vol = b_body['volume']
vol['id'] = uuidutils.generate_uuid()
stored_vol = {
'volume': vol,
'url': b_url
}
fake_volumes.append(stored_vol)
resp.status_code = 202
vol_dict = {'volume': vol}
resp._content = jsonutils.dumps(vol_dict)
# resp.json = vol_dict
return resp
pos = b_url.rfind('/volumes')
op = ''
cmp_url = b_url
if pos > 0:
op = b_url[pos:]
cmp_url = b_url[:pos] + '/volumes'
op = op[len('/volumes'):]
if action == 'GET':
if op == '' or op == '/detail':
tenant_id = b_url[:pos]
pos2 = tenant_id.rfind('/')
if pos2 > 0:
tenant_id = tenant_id[(pos2 + 1):]
else:
resp.status_code = 404
return resp
ret_vols = []
for temp_vol in fake_volumes:
if temp_vol['url'] != cmp_url:
continue
if temp_vol['volume']['project_id'] == tenant_id:
ret_vols.append(temp_vol['volume'])
vol_dicts = {'volumes': ret_vols}
resp._content = jsonutils.dumps(vol_dicts)
resp.status_code = 200
return resp
elif op != '':
if op[0] == '/':
_id = op[1:]
for vol in fake_volumes:
if vol['volume']['id'] == _id:
vol_dict = {'volume': vol['volume']}
resp._content = jsonutils.dumps(vol_dict)
resp.status_code = 200
return resp
if action == 'DELETE':
if op != '':
if op[0] == '/':
_id = op[1:]
for vol in fake_volumes:
if vol['volume']['id'] == _id:
fake_volumes.remove(vol)
resp.status_code = 202
return resp
else:
resp.status_code = 404
return resp
class CinderVolumeFunctionalTest(base.TestCase):
def setUp(self):
super(CinderVolumeFunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.app = self._make_app()
self._init_db()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root':
'tricircle.cinder_apigw.controllers.root.RootController',
'modules': ['tricircle.cinder_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def _init_db(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
# enforce foreign key constraint for sqlite
core.get_engine().execute('pragma foreign_keys=on')
self.context = context.Context()
pod_dict = {
'pod_id': 'fake_pod_id',
'pod_name': 'fake_pod_name',
'az_name': FAKE_AZ
}
config_dict = {
'service_id': 'fake_service_id',
'pod_id': 'fake_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:8774/v2/$(tenant_id)s'
}
pod_dict2 = {
'pod_id': 'fake_pod_id' + '2',
'pod_name': 'fake_pod_name' + '2',
'az_name': FAKE_AZ + '2'
}
config_dict2 = {
'service_id': 'fake_service_id' + '2',
'pod_id': 'fake_pod_id' + '2',
'service_type': cons.ST_CINDER,
'service_url': 'http://10.0.0.2:8774/v2/$(tenant_id)s'
}
top_pod = {
'pod_id': 'fake_top_pod_id',
'pod_name': 'RegionOne',
'az_name': ''
}
top_config = {
'service_id': 'fake_top_service_id',
'pod_id': 'fake_top_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:19998/v2/$(tenant_id)s'
}
db_api.create_pod(self.context, pod_dict)
db_api.create_pod(self.context, pod_dict2)
db_api.create_pod(self.context, top_pod)
db_api.create_pod_service_configuration(self.context, config_dict)
db_api.create_pod_service_configuration(self.context, config_dict2)
db_api.create_pod_service_configuration(self.context, top_config)
def tearDown(self):
super(CinderVolumeFunctionalTest, self).tearDown()
cfg.CONF.unregister_opts(app.common_opts)
pecan.set_config({}, overwrite=True)
core.ModelBase.metadata.drop_all(core.get_engine())
class TestVolumeController(CinderVolumeFunctionalTest):
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_error_case(self):
volumes = [
# no 'volume' parameter
{
"volume_xxx":
{
"name": 'vol_1',
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 400
},
# no AZ parameter
{
"volume":
{
"name": 'vol_1',
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 400
},
# incorrect AZ parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ + FAKE_AZ,
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 500
},
]
self._test_and_check(volumes, 'my_tenant_id')
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_one_and_get_one(self):
tenant1_volumes = [
# normal volume with correct parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, multiple volumes
{
"volume":
{
"name": 'vol_2',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, different az
{
"volume":
{
"name": 'vol_3',
"availability_zone": FAKE_AZ + '2',
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
]
tenant2_volumes = [
# different tenant, same az
{
"volume":
{
"name": 'vol_4',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id_2',
"metadata": {}
},
"expected_error": 202
},
]
self._test_and_check(tenant1_volumes, 'my_tenant_id')
self._test_and_check(tenant2_volumes, 'my_tenant_id_2')
self._test_detail_check('my_tenant_id', 3)
self._test_detail_check('my_tenant_id_2', 1)
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_one_and_delete_one(self):
volumes = [
# normal volume with correct parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
]
self._test_and_check_delete(volumes, 'my_tenant_id')
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_get(self):
response = self.app.get('/v2/my_tenant_id/volumes')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
vols = json_body.get('volumes')
self.assertEqual(0, len(vols))
def _test_and_check(self, volumes, tenant_id):
for test_vol in volumes:
if test_vol.get('volume'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume=test_vol['volume']),
expect_errors=True)
elif test_vol.get('volume_xxx'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume=test_vol['volume_xxx']),
expect_errors=True)
else:
return
self.assertEqual(response.status_int,
test_vol['expected_error'])
if response.status_int == 202:
json_body = jsonutils.loads(response.body)
res_vol = json_body.get('volume')
query_resp = self.app.get(
'/v2/' + tenant_id + '/volumes/' + res_vol['id'])
self.assertEqual(query_resp.status_int, 200)
json_body = jsonutils.loads(query_resp.body)
query_vol = json_body.get('volume')
self.assertEqual(res_vol['id'], query_vol['id'])
self.assertEqual(res_vol['name'], query_vol['name'])
self.assertEqual(res_vol['availability_zone'],
query_vol['availability_zone'])
self.assertIn(res_vol['availability_zone'],
[FAKE_AZ, FAKE_AZ + '2'])
def _test_and_check_delete(self, volumes, tenant_id):
for test_vol in volumes:
if test_vol.get('volume'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume=test_vol['volume']),
expect_errors=True)
self.assertEqual(response.status_int,
test_vol['expected_error'])
if response.status_int == 202:
json_body = jsonutils.loads(response.body)
_id = json_body.get('volume')['id']
query_resp = self.app.get(
'/v2/' + tenant_id + '/volumes/' + _id)
self.assertEqual(query_resp.status_int, 200)
delete_resp = self.app.delete(
'/v2/' + tenant_id + '/volumes/' + _id)
self.assertEqual(delete_resp.status_int, 202)
def _test_detail_check(self, tenant_id, vol_size):
resp = self.app.get(
'/v2/' + tenant_id + '/volumes' + '/detail',
expect_errors=True)
self.assertEqual(resp.status_int, 200)
json_body = jsonutils.loads(resp.body)
ret_vols = json_body.get('volumes')
self.assertEqual(len(ret_vols), vol_size)

View File

@ -0,0 +1,169 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# 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 unittest
from tricircle.common import az_ag
from tricircle.common import context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
FAKE_AZ = 'fake_az'
FAKE_SITE_ID = 'fake_pod_id'
FAKE_SITE_NAME = 'fake_pod_name'
FAKE_SERVICE_ID = 'fake_service_id'
FAKE_SITE_ID_2 = 'fake_pod_id_2'
FAKE_SITE_NAME_2 = 'fake_pod_name_2'
FAKE_SERVICE_ID_2 = 'fake_service_id_2'
FAKE_TOP_NAME = 'RegionOne'
FAKE_TOP_ID = 'fake_top_pod_id'
FAKE_TOP_SERVICE_ID = 'fake_top_service_id'
FAKE_TOP_ENDPOINT = 'http://127.0.0.1:8774/v2/$(tenant_id)s'
FAKE_TYPE = 'fake_type'
FAKE_URL = 'http://127.0.0.1:12345'
FAKE_URL_INVALID = 'http://127.0.0.1:23456'
FAKE_SERVICE_TYPE = 'cinder'
FAKE_SERVICE_ENDPOINT = 'http://127.0.0.1:8774/v2.1/$(tenant_id)s'
FAKE_SERVICE_ENDPOINT_2 = 'http://127.0.0.2:8774/v2.1/$(tenant_id)s'
FAKE_TENANT_ID = 'my tenant'
class FakeException(Exception):
pass
class AZAGTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
# enforce foreign key constraint for sqlite
core.get_engine().execute('pragma foreign_keys=on')
self.context = context.Context()
top_pod = {
'pod_id': FAKE_TOP_ID,
'pod_name': FAKE_TOP_NAME,
'az_name': ''
}
config_dict_top = {
'service_id': FAKE_TOP_SERVICE_ID,
'pod_id': FAKE_TOP_ID,
'service_type': FAKE_SERVICE_TYPE,
'service_url': FAKE_TOP_ENDPOINT
}
pod_dict = {
'pod_id': FAKE_SITE_ID,
'pod_name': FAKE_SITE_NAME,
'az_name': FAKE_AZ
}
pod_dict2 = {
'pod_id': FAKE_SITE_ID_2,
'pod_name': FAKE_SITE_NAME_2,
'az_name': FAKE_AZ
}
config_dict = {
'service_id': FAKE_SERVICE_ID,
'pod_id': FAKE_SITE_ID,
'service_type': FAKE_SERVICE_TYPE,
'service_url': FAKE_SERVICE_ENDPOINT
}
config_dict2 = {
'service_id': FAKE_SERVICE_ID_2,
'pod_id': FAKE_SITE_ID_2,
'service_type': FAKE_SERVICE_TYPE,
'service_url': FAKE_SERVICE_ENDPOINT_2
}
api.create_pod(self.context, pod_dict)
api.create_pod(self.context, pod_dict2)
api.create_pod(self.context, top_pod)
api.create_pod_service_configuration(self.context, config_dict)
api.create_pod_service_configuration(self.context, config_dict2)
api.create_pod_service_configuration(self.context, config_dict_top)
def test_get_pod_by_az_tenant(self):
pod1 = az_ag.get_pod_by_az_tenant(self.context,
FAKE_AZ + FAKE_AZ,
FAKE_TENANT_ID)
self.assertEqual(pod1, None)
pods = az_ag.list_pods_by_tenant(self.context, FAKE_TENANT_ID)
self.assertEqual(len(pods), 0)
# schedule one
pod2 = az_ag.get_pod_by_az_tenant(self.context,
FAKE_AZ,
FAKE_TENANT_ID)
pod_bindings = core.query_resource(self.context,
models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': FAKE_TENANT_ID}],
[])
self.assertIsNotNone(pod_bindings)
if pod_bindings[0]['pod_id'] == FAKE_SITE_ID:
self.assertEqual(pod2['pod_name'], FAKE_SITE_NAME)
self.assertEqual(pod2['pod_id'], FAKE_SITE_ID)
self.assertEqual(pod2['az_name'], FAKE_AZ)
else:
self.assertEqual(pod2['pod_name'], FAKE_SITE_NAME_2)
self.assertEqual(pod2['pod_id'], FAKE_SITE_ID_2)
self.assertEqual(pod2['az_name'], FAKE_AZ)
# scheduled one should always be bound
pod3 = az_ag.get_pod_by_az_tenant(self.context,
FAKE_AZ,
FAKE_TENANT_ID)
self.assertEqual(pod2['pod_name'], pod3['pod_name'])
self.assertEqual(pod2['pod_id'], pod3['pod_id'])
self.assertEqual(pod2['az_name'], pod3['az_name'])
def test_list_pods_by_tenant(self):
pod1 = az_ag.get_pod_by_az_tenant(self.context,
FAKE_AZ + FAKE_AZ,
FAKE_TENANT_ID)
pods = az_ag.list_pods_by_tenant(self.context, FAKE_TENANT_ID)
self.assertEqual(pod1, None)
self.assertEqual(len(pods), 0)
# TODO(joehuang): tenant bound to multiple pods in one AZ
# schedule one
pod2 = az_ag.get_pod_by_az_tenant(self.context,
FAKE_AZ,
FAKE_TENANT_ID)
pods = az_ag.list_pods_by_tenant(self.context, FAKE_TENANT_ID)
self.assertDictEqual(pods[0], pod2)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -0,0 +1,215 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# 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.
from mock import patch
import unittest
from tricircle.common import constants as cons
from tricircle.common import context
from tricircle.common import httpclient as hclient
from tricircle.db import api
from tricircle.db import core
def fake_get_pod_service_endpoint(ctx, pod_name, st):
pod = api.get_pod_by_name(ctx, pod_name)
if pod:
f = [{'key': 'pod_id', 'comparator': 'eq',
'value': pod['pod_id']},
{'key': 'service_type', 'comparator': 'eq',
'value': st}]
pod_services = api.list_pod_service_configurations(
ctx,
filters=f,
sorts=[])
if len(pod_services) != 1:
return ''
return pod_services[0]['service_url']
return ''
class HttpClientTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
# enforce foreign key constraint for sqlite
core.get_engine().execute('pragma foreign_keys=on')
self.context = context.Context()
def test_get_version_from_url(self):
url = 'http://127.0.0.1:8774/v2.1/$(tenant_id)s'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'v2.1')
url = 'http://127.0.0.1:8774/v2.1/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'v2.1')
url = 'http://127.0.0.1:8774/v2.1/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'v2.1')
url = 'https://127.0.0.1:8774/v2.1/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'v2.1')
url = 'https://127.0.0.1/v2.1/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'v2.1')
url = 'https://127.0.0.1/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, '')
url = 'https://127.0.0.1/sss/'
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, 'sss')
url = ''
ver = hclient.get_version_from_url(url)
self.assertEqual(ver, '')
def test_get_bottom_url(self):
b_endpoint = 'http://127.0.0.1:8774/v2.1/$(tenant_id)s'
t_url = 'http://127.0.0.1:8774/v2/my_tenant_id/volumes'
t_ver = hclient.get_version_from_url(t_url)
b_ver = hclient.get_version_from_url(b_endpoint)
self.assertEqual(t_ver, 'v2')
self.assertEqual(b_ver, 'v2.1')
b_url = hclient.get_bottom_url(t_ver, t_url, b_ver, b_endpoint)
self.assertEqual(b_url,
'http://127.0.0.1:8774/v2.1/my_tenant_id/volumes')
b_endpoint = 'http://127.0.0.1:8774/'
b_ver = hclient.get_version_from_url(b_endpoint)
self.assertEqual(b_ver, '')
b_url = hclient.get_bottom_url(t_ver, t_url, b_ver, b_endpoint)
self.assertEqual(b_url,
'http://127.0.0.1:8774/my_tenant_id/volumes')
b_endpoint = 'http://127.0.0.1:8774/v2.1'
b_ver = hclient.get_version_from_url(b_endpoint)
self.assertEqual(b_ver, 'v2.1')
b_url = hclient.get_bottom_url(t_ver, t_url, b_ver, b_endpoint)
self.assertEqual(b_url,
'http://127.0.0.1:8774/v2.1/my_tenant_id/volumes')
b_endpoint = 'http://127.0.0.1:8774/v2.1/'
b_ver = hclient.get_version_from_url(b_endpoint)
self.assertEqual(b_ver, 'v2.1')
b_url = hclient.get_bottom_url(t_ver, t_url, b_ver, b_endpoint)
self.assertEqual(b_url,
'http://127.0.0.1:8774/v2.1/my_tenant_id/volumes')
@patch.object(hclient, 'get_pod_service_endpoint',
new=fake_get_pod_service_endpoint)
def test_get_pod_service_ctx(self):
pod_dict = {
'pod_id': 'fake_pod_id',
'pod_name': 'fake_pod_name',
'az_name': 'fake_az'
}
config_dict = {
'service_id': 'fake_service_id',
'pod_id': 'fake_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:8774/v2.1/$(tenant_id)s'
}
t_url = 'http://127.0.0.1:8774/v2/my_tenant_id/volumes'
api.create_pod(self.context, pod_dict)
api.create_pod_service_configuration(self.context, config_dict)
b_url = 'http://127.0.0.1:8774/v2.1/my_tenant_id/volumes'
b_endpoint = hclient.get_pod_service_endpoint(self.context,
pod_dict['pod_name'],
cons.ST_CINDER)
self.assertEqual(b_endpoint, config_dict['service_url'])
b_ctx = hclient.get_pod_service_ctx(self.context,
t_url,
pod_dict['pod_name'],
cons.ST_CINDER)
self.assertEqual(b_ctx['t_ver'], 'v2')
self.assertEqual(b_ctx['t_url'], t_url)
self.assertEqual(b_ctx['b_ver'], 'v2.1')
self.assertEqual(b_ctx['b_url'], b_url)
# wrong pod name
b_ctx = hclient.get_pod_service_ctx(self.context,
t_url,
pod_dict['pod_name'] + '1',
cons.ST_CINDER)
self.assertEqual(b_ctx['t_ver'], 'v2')
self.assertEqual(b_ctx['t_url'], t_url)
self.assertEqual(b_ctx['b_ver'], '')
self.assertEqual(b_ctx['b_url'], '')
# wrong service_type
b_ctx = hclient.get_pod_service_ctx(self.context,
t_url,
pod_dict['pod_name'],
cons.ST_CINDER + '1')
self.assertEqual(b_ctx['t_ver'], 'v2')
self.assertEqual(b_ctx['t_url'], t_url)
self.assertEqual(b_ctx['b_ver'], '')
self.assertEqual(b_ctx['b_url'], '')
@patch.object(hclient, 'get_pod_service_endpoint',
new=fake_get_pod_service_endpoint)
def test_get_pod_and_endpoint_by_name(self):
pod_dict = {
'pod_id': 'fake_pod_id',
'pod_name': 'fake_pod_name',
'az_name': 'fake_az'
}
api.create_pod(self.context, pod_dict)
pod = api.get_pod_by_name(self.context, pod_dict['pod_name'] + '1')
self.assertEqual(pod, None)
pod = api.get_pod_by_name(self.context, pod_dict['pod_name'])
self.assertEqual(pod['pod_id'], pod_dict['pod_id'])
self.assertEqual(pod['pod_name'], pod_dict['pod_name'])
self.assertEqual(pod['az_name'], pod_dict['az_name'])
config_dict = {
'service_id': 'fake_service_id',
'pod_id': 'fake_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:8774/v2.1/$(tenant_id)s'
}
api.create_pod_service_configuration(self.context, config_dict)
endpoint = hclient.get_pod_service_endpoint(
self.context,
pod_dict['pod_name'],
config_dict['service_type'])
self.assertEqual(endpoint, config_dict['service_url'])
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -58,6 +58,103 @@ class APITest(unittest.TestCase):
self.assertEqual('test_pod_uuid_1', mappings[0][0]['pod_id'])
self.assertEqual('bottom_uuid_1', mappings[0][1])
def test_get_bottom_mappings_by_tenant_pod(self):
for i in xrange(3):
pod = {'pod_id': 'test_pod_uuid_%d' % i,
'pod_name': 'test_pod_%d' % i,
'az_name': 'test_az_uuid_%d' % i}
api.create_pod(self.context, pod)
routes = [
{
'route':
{
'top_id': 'top_uuid',
'pod_id': 'test_pod_uuid_0',
'project_id': 'test_project_uuid_0',
'resource_type': 'port'
},
},
{
'route':
{
'top_id': 'top_uuid_0',
'bottom_id': 'top_uuid_0',
'pod_id': 'test_pod_uuid_0',
'project_id': 'test_project_uuid_0',
'resource_type': 'port'
},
},
{
'route':
{
'top_id': 'top_uuid_1',
'bottom_id': 'top_uuid_1',
'pod_id': 'test_pod_uuid_0',
'project_id': 'test_project_uuid_0',
'resource_type': 'port'
},
},
{
'route':
{
'top_id': 'top_uuid_2',
'bottom_id': 'top_uuid_2',
'pod_id': 'test_pod_uuid_0',
'project_id': 'test_project_uuid_1',
'resource_type': 'port'
},
},
{
'route':
{
'top_id': 'top_uuid_3',
'bottom_id': 'top_uuid_3',
'pod_id': 'test_pod_uuid_1',
'project_id': 'test_project_uuid_1',
'resource_type': 'port'
},
}
]
with self.context.session.begin():
for route in routes:
core.create_resource(
self.context, models.ResourceRouting, route['route'])
routings = api.get_bottom_mappings_by_tenant_pod(
self.context,
'test_project_uuid_0',
'test_pod_uuid_0',
'port'
)
self.assertEqual(len(routings), 2)
self.assertEqual(routings['top_uuid_0']['top_id'], 'top_uuid_0')
self.assertEqual(routings['top_uuid_1']['top_id'], 'top_uuid_1')
routings = api.get_bottom_mappings_by_tenant_pod(
self.context,
'test_project_uuid_1',
'test_pod_uuid_0',
'port'
)
self.assertEqual(len(routings), 1)
self.assertEqual(routings['top_uuid_2']['top_id'], 'top_uuid_2')
self.assertEqual(routings['top_uuid_2']['bottom_id'], 'top_uuid_2')
routings = api.get_bottom_mappings_by_tenant_pod(
self.context,
'test_project_uuid_1',
'test_pod_uuid_1',
'port'
)
self.assertEqual(len(routings), 1)
self.assertEqual(routings['top_uuid_3']['top_id'], 'top_uuid_3')
self.assertEqual(routings['top_uuid_3']['bottom_id'], 'top_uuid_3')
def test_get_next_bottom_pod(self):
next_pod = api.get_next_bottom_pod(self.context)
self.assertIsNone(next_pod)