Chaoyi Huang 81b45f2c1d Move statless design from experiment to master branch
The statless design was developed in the experiment branch, the experiment
shows advantage in removing the status synchronization, uuid mapping
compared to the stateful design, and also fully reduce the coupling with
OpenStack services like Nova, Cinder. The overhead query latency for
resources also acceptable. It's time to move the statless design to the
master branch

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

Change-Id: I51bbb60dc07da5b2e79f25e02209aa2eb72711ac
Signed-off-by: Chaoyi Huang <joehuang@huawei.com>
2016-01-14 12:56:57 +08:00

333 lines
11 KiB
Python

# 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, pod_az = 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