Tricircle Neutron Plugin for Stateless

Implement Neutron plugin for stateless architecture. For creating operation,
it simply stores a resource entry in top layer database; for updating and
deleting operations it checks if there exists a resource routing entry, if
so, it not only updates database in top layer but also sends request to bottom
layers; for query, it returns data in bottom layers if routing entry exists,
otherwise returns data in top layer.

Plugin does not maintain routing entries itself, the creator of bottom layer
resources is responsible to add routing entries.

For test purpose, DevStack plugin script is updated to start two other Neutron
servers to simulate bottom layer services.

Blueprint: https://blueprints.launchpad.net/tricircle/+spec/implement-stateless
Change-Id: I238a8274e1d54df7c82c477321d4c827265db67d
This commit is contained in:
zhiyuan_cai 2015-12-11 09:52:41 +08:00
parent 3b8e791e18
commit eb72e17653
21 changed files with 969 additions and 31 deletions

View File

@ -86,6 +86,7 @@ function create_tricircle_cache_dir {
}
function configure_tricircle_api {
if is_service_enabled t-api ; then
echo "Configuring Tricircle API"
@ -93,7 +94,7 @@ function configure_tricircle_api {
iniset $TRICIRCLE_API_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
iniset $TRICIRCLE_API_CONF DEFAULT verbose True
iniset $TRICIRCLE_API_CONF DEFAULT use_syslog $SYSLOG
iniset $TRICIRCLE_API_CONF database connection `database_connection_url tricircle`
iniset $TRICIRCLE_API_CONF DEFAULT tricircle_db_connection `database_connection_url tricircle`
iniset $TRICIRCLE_API_CONF client admin_username admin
iniset $TRICIRCLE_API_CONF client admin_password $ADMIN_PASSWORD
@ -128,7 +129,7 @@ function configure_tricircle_nova_apigw {
iniset $TRICIRCLE_NOVA_APIGW_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
iniset $TRICIRCLE_NOVA_APIGW_CONF DEFAULT verbose True
iniset $TRICIRCLE_NOVA_APIGW_CONF DEFAULT use_syslog $SYSLOG
iniset $TRICIRCLE_NOVA_APIGW_CONF database connection `database_connection_url tricircle`
iniset $TRICIRCLE_NOVA_APIGW_CONF DEFAULT tricircle_db_connection `database_connection_url tricircle`
iniset $TRICIRCLE_NOVA_APIGW_CONF oslo_concurrency lock_path $TRICIRCLE_STATE_PATH/lock
@ -149,7 +150,6 @@ function configure_tricircle_nova_apigw {
fi
}
function configure_tricircle_cinder_apigw {
if is_service_enabled t-cgw ; then
echo "Configuring Tricircle Cinder APIGW"
@ -158,7 +158,7 @@ function configure_tricircle_cinder_apigw {
iniset $TRICIRCLE_CINDER_APIGW_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
iniset $TRICIRCLE_CINDER_APIGW_CONF DEFAULT verbose True
iniset $TRICIRCLE_CINDER_APIGW_CONF DEFAULT use_syslog $SYSLOG
iniset $TRICIRCLE_CINDER_APIGW_CONF database connection `database_connection_url tricircle`
iniset $TRICIRCLE_CINDER_APIGW_CONF DEFAULT tricircle_db_connection `database_connection_url tricircle`
iniset $TRICIRCLE_CINDER_APIGW_CONF oslo_concurrency lock_path $TRICIRCLE_STATE_PATH/lock
@ -196,6 +196,29 @@ function configure_tricircle_xjob {
fi
}
function start_new_neutron_server {
local server_index=$1
local region_name=$2
local q_port=$3
get_or_create_service "neutron" "network" "Neutron Service"
get_or_create_endpoint "network" \
"$region_name" \
"$Q_PROTOCOL://$SERVICE_HOST:$q_port/" \
"$Q_PROTOCOL://$SERVICE_HOST:$q_port/" \
"$Q_PROTOCOL://$SERVICE_HOST:$q_port/"
cp $NEUTRON_CONF $NEUTRON_CONF.$server_index
iniset $NEUTRON_CONF.$server_index database connection `database_connection_url $Q_DB_NAME$server_index`
iniset $NEUTRON_CONF.$server_index DEFAULT bind_port $q_port
iniset $NEUTRON_CONF.$server_index DEFAULT service_plugins ""
recreate_database $Q_DB_NAME$server_index
$NEUTRON_BIN_DIR/neutron-db-manage --config-file $NEUTRON_CONF.$server_index --config-file /$Q_PLUGIN_CONF_FILE upgrade head
run_process q-svc$server_index "$NEUTRON_BIN_DIR/neutron-server --config-file $NEUTRON_CONF.$server_index --config-file /$Q_PLUGIN_CONF_FILE"
}
if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then
if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then
@ -217,6 +240,24 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then
recreate_database tricircle
python "$TRICIRCLE_DIR/cmd/manage.py" "$TRICIRCLE_API_CONF"
if is_service_enabled q-svc ; then
start_new_neutron_server 1 Site1 20001
start_new_neutron_server 2 Site2 20002
# reconfigure neutron server to use our own plugin
echo "Configuring Neutron plugin for Tricircle"
Q_PLUGIN_CLASS="tricircle.network.plugin.TricirclePlugin"
iniset $NEUTRON_CONF DEFAULT core_plugin "$Q_PLUGIN_CLASS"
iniset $NEUTRON_CONF DEFAULT service_plugins ""
iniset $NEUTRON_CONF DEFAULT tricircle_db_connection `database_connection_url tricircle`
iniset $NEUTRON_CONF client admin_username admin
iniset $NEUTRON_CONF client admin_password $ADMIN_PASSWORD
iniset $NEUTRON_CONF client admin_tenant demo
iniset $NEUTRON_CONF client auto_refresh_endpoint True
iniset $NEUTRON_CONF client top_site_name $TRICIRCLE_REGION_NAME
fi
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
echo_summary "Initializing Tricircle Service"

View File

@ -2,7 +2,8 @@
output_file = etc/api.conf.sample
wrap_width = 79
namespace = tricircle.api
namespace = tricircle.common_opts
namespace = tricircle.common
namespace = tricircle.db
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy

View File

@ -2,7 +2,7 @@
output_file = etc/cinder_apigw.conf.sample
wrap_width = 79
namespace = tricircle.cinder_apigw
namespace = tricircle.common_opts
namespace = tricircle.common
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy

View File

@ -2,7 +2,7 @@
output_file = etc/nova_apigw.conf.sample
wrap_width = 79
namespace = tricircle.nova_apigw
namespace = tricircle.common_opts
namespace = tricircle.common
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy

View File

@ -2,7 +2,7 @@
output_file = etc/xjob.conf.sample
wrap_width = 79
namespace = tricircle.xjob
namespace = tricircle.common_opts
namespace = tricircle.common
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy

View File

@ -48,12 +48,10 @@ output_file = tricircle/locale/tricircle.pot
[entry_points]
oslo.config.opts =
tricircle.common_opts = tricircle.common.opts:list_opts
tricircle.api = tricircle.api.opts:list_opts
tricircle.common = tricircle.common.opts:list_opts
tricircle.db = tricircle.db.opts:list_opts
tricircle.nova_apigw = tricircle.nova_apigw.opts:list_opts
tricircle.cinder_apigw = tricircle.cinder_apigw.opts:list_opts
tricircle.xjob = tricircle.xjob.opts:list_opts

View File

@ -9,7 +9,9 @@ usedevelop = True
install_command = pip install -U --force-reinstall {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
deps =
-r{toxinidir}/test-requirements.txt
-egit+https://git.openstack.org/openstack/neutron@master#egg=neutron
commands = python setup.py testr --slowest --testr-args='{posargs}'
whitelist_externals = rm

View File

@ -133,7 +133,7 @@ class SitesController(rest.RestController):
@expose()
def get_all(self):
context = _extract_context_from_environ(_get_environment())
sites = db_api.list_sites(context, [])
sites = db_api.list_sites(context)
return {'sites': sites}
@expose()

View File

@ -113,6 +113,7 @@ class Client(object):
self.service_handle_map[handle_obj.service_type] = handle_obj
for resource in handle_obj.support_resource:
self.resource_service_map[resource] = handle_obj.service_type
self.operation_resources_map['client'].add(resource)
for operation, index in six.iteritems(
resource_handle.operation_index_map):
# add parentheses to emphasize we mean to do bitwise and
@ -277,6 +278,24 @@ class Client(object):
"""
self._update_endpoint_from_keystone(cxt, False)
@_safe_operation('client')
def get_native_client(self, resource, cxt):
"""Get native python client instance
Use this function only when for complex operations
:param resource: resource type
:param cxt: resource type
:return: client instance
"""
if cxt.is_admin and not cxt.auth_token:
cxt.auth_token = self._get_admin_token()
cxt.tenant = self._get_admin_project_id()
service = self.resource_service_map[resource]
handle = self.service_handle_map[service]
return handle._get_client(cxt)
@_safe_operation('list')
def list_resources(self, resource, cxt, filters=None):
"""Query resource in site of top layer
@ -288,7 +307,7 @@ class Client(object):
of each ResourceHandle class.
:param resource: resource type
:param cxt: context object
:param cxt: resource type
:param filters: list of dict with key 'key', 'comparator', 'value'
like {'key': 'name', 'comparator': 'eq', 'value': 'private'}, 'key'
is the field name of resources
@ -374,7 +393,7 @@ class Client(object):
service = self.resource_service_map[resource]
handle = self.service_handle_map[service]
handle.handle_get(cxt, resource, resource_id)
return handle.handle_get(cxt, resource, resource_id)
@_safe_operation('action')
def action_resources(self, resource, cxt, action, *args, **kwargs):

View File

@ -51,6 +51,17 @@ def extract_context_from_environ():
return Context(**context_paras)
def get_context_from_neutron_context(context):
ctx = Context()
ctx.auth_token = context.auth_token
ctx.user = context.user_id
ctx.tenant = context.tenant_id
ctx.tenant_name = context.tenant_name
ctx.user_name = context.user_name
ctx.resource_uuid = context.resource_uuid
return ctx
class ContextBase(oslo_ctx.RequestContext):
def __init__(self, auth_token=None, user_id=None, tenant_id=None,
is_admin=False, request_id=None, overwrite=True,

View File

@ -101,9 +101,9 @@ class GlanceResourceHandle(ResourceHandle):
class NeutronResourceHandle(ResourceHandle):
service_type = 'neutron'
support_resource = {'network': LIST,
'subnet': LIST,
'port': LIST,
support_resource = {'network': LIST | DELETE,
'subnet': LIST | DELETE,
'port': LIST | DELETE | GET,
'router': LIST,
'security_group': LIST,
'security_group_rule': LIST}
@ -127,6 +127,30 @@ class NeutronResourceHandle(ResourceHandle):
raise exceptions.EndpointNotAvailable(
'neutron', client.httpclient.endpoint_url)
def handle_get(self, cxt, resource, resource_id):
try:
client = self._get_client(cxt)
return getattr(client, 'show_%s' % resource)(resource_id)[resource]
except q_exceptions.ConnectionFailed:
self.endpoint_url = None
raise exceptions.EndpointNotAvailable(
'neutron', client.httpclient.endpoint_url)
except q_exceptions.NotFound:
LOG.debug("%(resource)s %(resource_id)s not found",
{'resource': resource, 'resource_id': resource_id})
def handle_delete(self, cxt, resource, resource_id):
try:
client = self._get_client(cxt)
return getattr(client, 'delete_%s' % resource)(resource_id)
except q_exceptions.ConnectionFailed:
self.endpoint_url = None
raise exceptions.EndpointNotAvailable(
'neutron', client.httpclient.endpoint_url)
except q_exceptions.NotFound:
LOG.debug("Delete %(resource)s %(resource_id)s which not found",
{'resource': resource, 'resource_id': resource_id})
class NovaResourceHandle(ResourceHandle):
service_type = 'nova'

View File

@ -33,9 +33,10 @@ def get_site(context, site_id):
return core.get_resource(context, models.Site, site_id)
def list_sites(context, filters):
def list_sites(context, filters=None, sorts=None):
with context.session.begin():
return core.query_resource(context, models.Site, filters)
return core.query_resource(context, models.Site, filters or [],
sorts or [])
def update_site(context, site_id, update_dict):
@ -61,13 +62,48 @@ def get_site_service_configuration(context, config_id):
config_id)
def list_site_service_configurations(context, filters):
def list_site_service_configurations(context, filters=None, sorts=None):
with context.session.begin():
return core.query_resource(context, models.SiteServiceConfiguration,
filters)
filters or [], sorts or [])
def update_site_service_configuration(context, config_id, update_dict):
with context.session.begin():
return core.update_resource(
context, models.SiteServiceConfiguration, config_id, update_dict)
def get_bottom_mappings_by_top_id(context, top_id, resource_type):
"""Get resource id and site name on bottom
:param context: context object
:param top_id: resource id on top
:return: a list of tuple (site dict, bottom_id)
"""
route_filters = [{'key': 'top_id', 'comparator': 'eq', 'value': top_id},
{'key': 'resource_type',
'comparator': 'eq',
'value': resource_type}]
mappings = []
with context.session.begin():
routes = core.query_resource(
context, models.ResourceRouting, route_filters, [])
for route in routes:
if not route['bottom_id']:
continue
site = core.get_resource(context, models.Site, route['site_id'])
mappings.append((site, route['bottom_id']))
return mappings
def get_next_bottom_site(context, current_site_id=None):
sites = list_sites(context, sorts=[(models.Site.site_id, True)])
# NOTE(zhiyuan) number of sites is small, just traverse to filter top site
sites = [site for site in sites if site['az_id']]
for index, site in enumerate(sites):
if not current_site_id:
return site
if site['site_id'] == current_site_id and index < len(sites) - 1:
return sites[index + 1]
return None

View File

@ -24,6 +24,13 @@ from sqlalchemy.inspection import inspect
from tricircle.common import exceptions
db_opts = [
cfg.StrOpt('tricircle_db_connection',
help='db connection string for tricircle'),
]
cfg.CONF.register_opts(db_opts)
_engine_facade = None
ModelBase = declarative.declarative_base()
@ -60,8 +67,14 @@ def _get_engine_facade():
global _engine_facade
if not _engine_facade:
_engine_facade = db_session.EngineFacade.from_config(cfg.CONF)
connection = cfg.CONF.database.connection
t_connection = cfg.CONF.tricircle_db_connection
if connection.startswith('sqlite'):
_engine_facade = db_session.EngineFacade.from_config(cfg.CONF)
else:
cfg.CONF.set_override('connection', t_connection, group='database')
_engine_facade = db_session.EngineFacade.from_config(cfg.CONF)
cfg.CONF.clear_override('connection', group='database')
return _engine_facade
@ -104,10 +117,13 @@ def initialize():
connection='sqlite:///:memory:')
def query_resource(context, model, filters):
def query_resource(context, model, filters, sorts):
query = context.session.query(model)
objs = _filter_query(model, query, filters)
return [obj.to_dict() for obj in objs]
query = _filter_query(model, query, filters)
for sort_key, sort_dir in sorts:
sort_dir_func = sql.asc if sort_dir else sql.desc
query = query.order_by(sort_dir_func(sort_key))
return [obj.to_dict() for obj in query]
def update_resource(context, model, pk_value, update_dict):

View File

@ -151,6 +151,8 @@ def upgrade(migrate_engine):
sql.Column('top_id', sql.String(length=36), nullable=False),
sql.Column('bottom_id', sql.String(length=36)),
sql.Column('site_id', sql.String(length=64), nullable=False),
sql.Column('project_id', sql.String(length=36)),
sql.Column('resource_type', sql.String(length=64), nullable=False),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
mysql_engine='InnoDB',

View File

@ -258,8 +258,8 @@ class ResourceRouting(core.ModelBase, core.DictBase, models.TimestampMixin):
'top_id', 'site_id',
name='cascaded_sites_resource_routing0top_id0site_id'),
)
attributes = ['id', 'top_id', 'bottom_id', 'site_id',
'created_at', 'updated_at']
attributes = ['id', 'top_id', 'bottom_id', 'site_id', 'project_id',
'resource_type', 'created_at', 'updated_at']
id = sql.Column('id', sql.Integer, primary_key=True)
top_id = sql.Column('top_id', sql.String(length=36), nullable=False)
@ -267,3 +267,6 @@ class ResourceRouting(core.ModelBase, core.DictBase, models.TimestampMixin):
site_id = sql.Column('site_id', sql.String(length=64),
sql.ForeignKey('cascaded_sites.site_id'),
nullable=False)
project_id = sql.Column('project_id', sql.String(length=36))
resource_type = sql.Column('resource_type', sql.String(length=64),
nullable=False)

22
tricircle/db/opts.py Normal file
View File

@ -0,0 +1,22 @@
# 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 tricircle.db.core
def list_opts():
return [
('DEFAULT', tricircle.db.core.db_opts),
]

View File

364
tricircle/network/plugin.py Normal file
View File

@ -0,0 +1,364 @@
# 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 oslo_log.helpers as log_helpers
from oslo_log import log
from neutron.common import rpc as n_rpc
from neutron.common import topics
from neutron.db import agentschedulers_db
from neutron.db import db_base_plugin_v2
from neutron.db import external_net_db
from neutron.db import extradhcpopt_db
from neutron.db import l3_db
from neutron.db import models_v2
from neutron.db import portbindings_db
from neutron.db import securitygroups_db
from neutron.db import sqlalchemyutils
from sqlalchemy import sql
import tricircle.common.client as t_client
import tricircle.common.context as t_context
from tricircle.common.i18n import _LI
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
LOG = log.getLogger(__name__)
class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2,
securitygroups_db.SecurityGroupDbMixin,
l3_db.L3_NAT_dbonly_mixin,
external_net_db.External_net_db_mixin,
portbindings_db.PortBindingMixin,
extradhcpopt_db.ExtraDhcpOptMixin,
agentschedulers_db.DhcpAgentSchedulerDbMixin):
__native_bulk_support = True
__native_pagination_support = True
__native_sorting_support = True
supported_extension_aliases = ["quotas",
"extra_dhcp_opt",
"binding",
"security-group",
"external-net",
"router"]
def __init__(self):
super(TricirclePlugin, self).__init__()
LOG.info(_LI("Starting Tricircle Neutron Plugin"))
self.clients = {}
self._setup_rpc()
def _setup_rpc(self):
self.endpoints = []
def _get_client(self, site_name):
if site_name not in self.clients:
self.clients[site_name] = t_client.Client(site_name)
return self.clients[site_name]
@log_helpers.log_method_call
def start_rpc_listeners(self):
self.topic = topics.PLUGIN
self.conn = n_rpc.create_connection(new=True)
self.conn.create_consumer(self.topic, self.endpoints, fanout=False)
return self.conn.consume_in_threads()
def create_network(self, context, network):
return super(TricirclePlugin, self).create_network(context, network)
def delete_network(self, context, network_id):
t_ctx = t_context.get_context_from_neutron_context(context)
try:
mappings = db_api.get_bottom_mappings_by_top_id(
t_ctx, network_id, 'network')
for mapping in mappings:
site_name = mapping[0]['site_name']
bottom_network_id = mapping[1]
self._get_client(site_name).delete_networks(
t_ctx, bottom_network_id)
except Exception:
raise
super(TricirclePlugin, self).delete_network(context, network_id)
def update_network(self, context, network_id, network):
return super(TricirclePlugin, self).update_network(
context, network_id, network)
def create_subnet(self, context, subnet):
return super(TricirclePlugin, self).create_subnet(context, subnet)
def delete_subnet(self, context, subnet_id):
t_ctx = t_context.get_context_from_neutron_context(context)
try:
mappings = db_api.get_bottom_mappings_by_top_id(
t_ctx, subnet_id, 'network')
for mapping in mappings:
site_name = mapping[0]['site_name']
bottom_subnet_id = mapping[1]
self._get_client(site_name).delete_subnets(
t_ctx, bottom_subnet_id)
except Exception:
raise
super(TricirclePlugin, self).delete_subnet(context, subnet_id)
def update_subnet(self, context, subnet_id, subnet):
return super(TricirclePlugin, self).update_network(
context, subnet_id, subnet)
def create_port(self, context, port):
return super(TricirclePlugin, self).create_port(context, port)
def delete_port(self, context, port_id):
t_ctx = t_context.get_context_from_neutron_context(context)
try:
mappings = db_api.get_bottom_mappings_by_top_id(t_ctx,
port_id, 'port')
if mappings:
site_name = mappings[0][0]['site_name']
bottom_port_id = mappings[0][1]
self._get_client(site_name).delete_ports(
t_ctx, bottom_port_id)
except Exception:
raise
super(TricirclePlugin, self).delete_port(context, port_id)
def update_port(self, context, port_id, port):
return super(TricirclePlugin, self).update_port(
context, port_id, port)
def get_port(self, context, port_id, fields=None):
t_ctx = t_context.get_context_from_neutron_context(context)
mappings = db_api.get_bottom_mappings_by_top_id(t_ctx,
port_id, 'port')
if mappings:
site_name = mappings[0][0]['site_name']
bottom_port_id = mappings[0][1]
port = self._get_client(site_name).get_ports(
t_ctx, bottom_port_id)
port['id'] = port_id
if fields:
return dict(
[(k, v) for k, v in port.iteritems() if k in fields])
else:
return port
else:
return super(TricirclePlugin, self).get_port(context,
port_id, fields)
@staticmethod
def _apply_ports_filters(query, model, filters):
if not filters:
return query
for key, value in filters.iteritems():
column = getattr(model, key, None)
if column is not None:
if not value:
query = query.filter(sql.false())
return query
query = query.filter(column.in_(value))
return query
def _get_ports_from_db_with_number(self, context,
number, last_port_id, top_bottom_map,
filters=None):
query = context.session.query(models_v2.Port)
# set step as two times of number to have better chance to obtain all
# ports we need
search_step = number * 2
if search_step < 100:
search_step = 100
query = self._apply_ports_filters(query, models_v2.Port, filters)
query = sqlalchemyutils.paginate_query(
query, models_v2.Port, search_step, [('id', False)],
# create a dummy port object
marker_obj=models_v2.Port(
id=last_port_id) if last_port_id else None)
total = 0
ret = []
for port in query:
total += 1
if port['id'] not in top_bottom_map:
ret.append(port)
if len(ret) == number:
return ret
# NOTE(zhiyuan) we have traverse all the ports
if total < search_step:
return ret
else:
ret.extend(self._get_ports_from_db_with_number(
context, number - len(ret), ret[-1]['id'], top_bottom_map))
def _get_ports_from_top_with_number(self, context,
number, last_port_id, top_bottom_map,
filters=None):
with context.session.begin():
ret = self._get_ports_from_db_with_number(
context, number, last_port_id, top_bottom_map, filters)
return {'ports': ret}
def _get_ports_from_top(self, context, top_bottom_map, filters=None):
with context.session.begin():
ret = []
query = context.session.query(models_v2.Port)
query = self._apply_ports_filters(query, models_v2.Port, filters)
for port in query:
if port['id'] not in top_bottom_map:
ret.append(port)
return ret
@staticmethod
def _map_ports_from_bottom_to_top(res, bottom_top_map):
for port in res['ports']:
port['id'] = bottom_top_map[port['id']]
def _get_ports_from_site_with_number(self, context,
current_site, number, last_port_id,
bottom_top_map, top_bottom_map,
filters=None):
# NOTE(zhiyuan) last_port_id is top id, also id in returned port dict
# also uses top id. when interacting with bottom site, need to map
# top to bottom in request and map bottom to top in response
t_ctx = t_context.get_context_from_neutron_context(context)
q_client = self._get_client(
current_site['site_name']).get_native_client('port', t_ctx)
params = {'limit': number}
if filters:
if 'id' in filters:
_filters = dict(filters)
id_list = []
for _id in _filters['id']:
if _id in top_bottom_map:
id_list.append(top_bottom_map[_id])
else:
id_list.append(_id)
_filters['id'] = id_list
params.update(_filters)
else:
params.update(filters)
if last_port_id:
# map top id to bottom id in request
params['marker'] = top_bottom_map[last_port_id]
res = q_client.get(q_client.ports_path, params=params)
# map bottom id to top id in client response
self._map_ports_from_bottom_to_top(res, bottom_top_map)
if len(res['ports']) == number:
return res
else:
next_site = db_api.get_next_bottom_site(
t_ctx, current_site_id=current_site['site_id'])
if not next_site:
# _get_ports_from_top_with_number uses top id, no need to map
next_res = self._get_ports_from_top_with_number(
context, number - len(res['ports']), '', top_bottom_map,
filters)
next_res['ports'].extend(res['ports'])
return next_res
else:
# _get_ports_from_site_with_number itself returns top id, no
# need to map
next_res = self._get_ports_from_site_with_number(
context, next_site, number - len(res['ports']), '',
bottom_top_map, top_bottom_map, filters)
next_res['ports'].extend(res['ports'])
return next_res
def get_ports(self, context, filters=None, fields=None, sorts=None,
limit=None, marker=None, page_reverse=False):
t_ctx = t_context.get_context_from_neutron_context(context)
with t_ctx.session.begin():
route_filters = [{'key': 'resource_type',
'comparator': 'eq',
'value': 'port'}]
routes = core.query_resource(t_ctx, models.ResourceRouting,
route_filters, [])
bottom_top_map = {}
top_bottom_map = {}
for route in routes:
# port mapping should not have empty bottom id
bottom_top_map[route['bottom_id']] = route['top_id']
top_bottom_map[route['top_id']] = route['bottom_id']
if limit:
if marker:
mappings = db_api.get_bottom_mappings_by_top_id(t_ctx,
marker, 'port')
# NOTE(zhiyuan) if mapping exists, we retrieve port information
# from bottom, otherwise from top
if mappings:
site_id = mappings[0][0]['site_id']
current_site = db_api.get_site(t_ctx, site_id)
res = self._get_ports_from_site_with_number(
context, current_site, limit, marker,
bottom_top_map, top_bottom_map, filters)
else:
res = self._get_ports_from_top_with_number(
context, limit, marker, top_bottom_map, filters)
else:
current_site = db_api.get_next_bottom_site(t_ctx)
# only top site registered
if current_site:
res = self._get_ports_from_site_with_number(
context, current_site, limit, '',
bottom_top_map, top_bottom_map, filters)
else:
res = self._get_ports_from_top_with_number(
context, limit, marker, top_bottom_map, filters)
# NOTE(zhiyuan) we can safely return ports, neutron controller will
# generate links for us so we do not need to worry about it.
#
# _get_ports_from_site_with_number already traverses all the sites
# to try to get ports equal to limit, so site is transparent for
# controller.
return res['ports']
else:
ret = []
sites = db_api.list_sites(t_ctx)
for site in sites:
if not site['az_id']:
continue
_filters = []
if filters:
for key, value in filters.iteritems():
if key == 'id':
id_list = []
for _id in value:
if _id in top_bottom_map:
id_list.append(top_bottom_map[_id])
else:
id_list.append(_id)
_filters.append({'key': key,
'comparator': 'eq',
'value': id_list})
else:
_filters.append({'key': key,
'comparator': 'eq',
'value': value})
client = self._get_client(site['site_name'])
ret.extend(client.list_ports(t_ctx, filters=_filters))
self._map_ports_from_bottom_to_top({'ports': ret}, bottom_top_map)
ret.extend(self._get_ports_from_top(context, top_bottom_map,
filters))
return ret

View File

@ -0,0 +1,83 @@
# 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 context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
class APITest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
def test_get_bottom_mappings_by_top_id(self):
for i in xrange(3):
site = {'site_id': 'test_site_uuid_%d' % i,
'site_name': 'test_site_%d' % i,
'az_id': 'test_az_uuid_%d' % i}
api.create_site(self.context, site)
route1 = {
'top_id': 'top_uuid',
'site_id': 'test_site_uuid_0',
'resource_type': 'port'}
route2 = {
'top_id': 'top_uuid',
'site_id': 'test_site_uuid_1',
'bottom_id': 'bottom_uuid_1',
'resource_type': 'port'}
route3 = {
'top_id': 'top_uuid',
'site_id': 'test_site_uuid_2',
'bottom_id': 'bottom_uuid_2',
'resource_type': 'neutron'}
routes = [route1, route2, route3]
with self.context.session.begin():
for route in routes:
core.create_resource(
self.context, models.ResourceRouting, route)
mappings = api.get_bottom_mappings_by_top_id(self.context,
'top_uuid', 'port')
self.assertEqual('test_site_uuid_1', mappings[0][0]['site_id'])
self.assertEqual('bottom_uuid_1', mappings[0][1])
def test_get_next_bottom_site(self):
next_site = api.get_next_bottom_site(self.context)
self.assertIsNone(next_site)
sites = []
for i in xrange(5):
site = {'site_id': 'test_site_uuid_%d' % i,
'site_name': 'test_site_%d' % i,
'az_id': 'test_az_uuid_%d' % i}
api.create_site(self.context, site)
sites.append(site)
next_site = api.get_next_bottom_site(self.context)
self.assertEqual(next_site, sites[0])
next_site = api.get_next_bottom_site(
self.context, current_site_id='test_site_uuid_2')
self.assertEqual(next_site, sites[3])
next_site = api.get_next_bottom_site(
self.context, current_site_id='test_site_uuid_4')
self.assertIsNone(next_site)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -178,6 +178,23 @@ class ModelsTest(unittest.TestCase):
sites = api.list_sites(self.context, filters)
self.assertEqual(len(sites), 0)
def test_sort(self):
site1 = {'site_id': 'test_site1_uuid',
'site_name': 'test_site1',
'az_id': 'test_az1_uuid'}
site2 = {'site_id': 'test_site2_uuid',
'site_name': 'test_site2',
'az_id': 'test_az2_uuid'}
site3 = {'site_id': 'test_site3_uuid',
'site_name': 'test_site3',
'az_id': 'test_az3_uuid'}
sites = [site1, site2, site3]
for site in sites:
api.create_site(self.context, site)
sites = api.list_sites(self.context,
sorts=[(models.Site.site_id, False)])
self.assertEqual(sites, [site3, site2, site1])
def test_resources(self):
"""Create all the resources to test model definition"""
try:
@ -200,7 +217,8 @@ class ModelsTest(unittest.TestCase):
'az_id': 'test_az1_uuid'}
api.create_site(self.context, site)
routing = {'top_id': 'top_uuid',
'site_id': 'test_site1_uuid'}
'site_id': 'test_site1_uuid',
'resource_type': 'port'}
with self.context.session.begin():
core.create_resource(self.context, models.ResourceRouting, routing)
self.assertRaises(oslo_db.exception.DBDuplicateEntry,

View File

@ -0,0 +1,298 @@
# 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 mock
from mock import patch
import unittest
from neutron.db import db_base_plugin_v2
import tricircle.common.client as t_client
from tricircle.common import context
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
from tricircle.network import plugin
class FakeNeutronClient(object):
def __init__(self, site_name):
self.site_name = site_name
self.ports_path = ''
def _get(self, params=None):
site_index = self.site_name.split('_')[1]
bottom_id = 'bottom_id_%s' % site_index
if not params:
return {'ports': [{'id': bottom_id, 'name': 'bottom'}]}
if params.get('marker') == bottom_id:
return {'ports': []}
if 'filters' in params and params['filters'].get('id', []):
if bottom_id in params['filters']['id']:
return {'ports': [{'id': bottom_id, 'name': 'bottom'}]}
else:
return {'ports': []}
return {'ports': [{'id': bottom_id, 'name': 'bottom'}]}
def get(self, path, params=None):
if self.site_name == 'site_1' or self.site_name == 'site_2':
return self._get(params)
else:
raise Exception()
class FakeClient(object):
def __init__(self, site_name):
self.site_name = site_name
self.client = FakeNeutronClient(self.site_name)
def get_native_client(self, resource, ctx):
return self.client
def list_ports(self, ctx, filters=None):
filter_dict = {}
filters = filters or []
for query_filter in filters:
key = query_filter['key']
value = query_filter['value']
filter_dict[key] = value
return self.client.get('', {'filters': filter_dict})['ports']
def get_ports(self, ctx, port_id):
return self.client.get('')['ports'][0]
class FakeNeutronContext(object):
def __init__(self):
self._session = None
@property
def session(self):
if not self._session:
self._session = FakeSession()
return self._session
class FakeQuery(object):
def __init__(self, records):
self.records = records
self.index = 0
def _handle_pagination_by_id(self, record_id):
for i, record in enumerate(self.records):
if record['id'] == record_id:
if i + 1 < len(self.records):
return FakeQuery(self.records[i + 1:])
else:
return FakeQuery([])
return FakeQuery([])
def _handle_filter_by_id(self, record_id):
for i, record in enumerate(self.records):
if record['id'] == record_id:
return FakeQuery(self.records[i:i + 1])
return FakeQuery([])
def filter(self, criteria):
if hasattr(criteria.right, 'value'):
record_id = criteria.right.value
return self._handle_pagination_by_id(record_id)
else:
record_id = criteria.expression.right.element.clauses[0].value
return self._handle_filter_by_id(record_id)
def order_by(self, func):
self.records.sort(key=lambda x: x['id'])
return FakeQuery(self.records)
def limit(self, limit):
return FakeQuery(self.records[:limit])
def next(self):
if self.index >= len(self.records):
raise StopIteration
self.index += 1
return self.records[self.index - 1]
def __iter__(self):
return self
class FakeSession(object):
class WithWrapper(object):
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
pass
def begin(self):
return FakeSession.WithWrapper()
def query(self, model):
return FakeQuery([{'id': 'top_id_0', 'name': 'top'},
{'id': 'top_id_1', 'name': 'top'},
{'id': 'top_id_2', 'name': 'top'},
{'id': 'top_id_3', 'name': 'top'}])
class FakePlugin(plugin.TricirclePlugin):
def __init__(self):
self.clients = {'site_1': t_client.Client('site_1'),
'site_2': t_client.Client('site_2')}
def fake_get_context_from_neutron_context(q_context):
return context.get_db_context()
def fake_get_client(self, site_name):
return FakeClient(site_name)
def fake_get_ports_from_db_with_number(self, ctx, number,
last_port_id, top_set):
return [{'id': 'top_id_0'}]
def fake_get_ports_from_top(self, context, top_bottom_map):
return [{'id': 'top_id_0'}]
class ModelsTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
def _basic_site_route_setup(self):
site1 = {'site_id': 'site_id_1',
'site_name': 'site_1',
'az_id': 'az_id_1'}
site2 = {'site_id': 'site_id_2',
'site_name': 'site_2',
'az_id': 'az_id_2'}
site3 = {'site_id': 'site_id_0',
'site_name': 'top_site',
'az_id': ''}
for site in (site1, site2, site3):
db_api.create_site(self.context, site)
route1 = {
'top_id': 'top_id_1',
'site_id': 'site_id_1',
'bottom_id': 'bottom_id_1',
'resource_type': 'port'}
route2 = {
'top_id': 'top_id_2',
'site_id': 'site_id_2',
'bottom_id': 'bottom_id_2',
'resource_type': 'port'}
with self.context.session.begin():
core.create_resource(self.context, models.ResourceRouting, route1)
core.create_resource(self.context, models.ResourceRouting, route2)
@patch.object(context, 'get_context_from_neutron_context',
new=fake_get_context_from_neutron_context)
@patch.object(plugin.TricirclePlugin, '_get_client',
new=fake_get_client)
@patch.object(db_base_plugin_v2.NeutronDbPluginV2, 'get_port')
def test_get_port(self, mock_plugin_method):
self._basic_site_route_setup()
fake_plugin = FakePlugin()
neutron_context = FakeNeutronContext()
fake_plugin.get_port(neutron_context, 'top_id_0')
port1 = fake_plugin.get_port(neutron_context, 'top_id_1')
port2 = fake_plugin.get_port(neutron_context, 'top_id_2')
fake_plugin.get_port(neutron_context, 'top_id_3')
self.assertEqual({'id': 'top_id_1', 'name': 'bottom'}, port1)
self.assertEqual({'id': 'top_id_2', 'name': 'bottom'}, port2)
calls = [mock.call(neutron_context, 'top_id_0', None),
mock.call(neutron_context, 'top_id_3', None)]
mock_plugin_method.assert_has_calls(calls)
@patch.object(context, 'get_context_from_neutron_context',
new=fake_get_context_from_neutron_context)
@patch.object(plugin.TricirclePlugin, '_get_client',
new=fake_get_client)
def test_get_ports_pagination(self):
self._basic_site_route_setup()
fake_plugin = FakePlugin()
neutron_context = FakeNeutronContext()
ports1 = fake_plugin.get_ports(neutron_context, limit=1)
ports2 = fake_plugin.get_ports(neutron_context, limit=1,
marker=ports1[-1]['id'])
ports3 = fake_plugin.get_ports(neutron_context, limit=1,
marker=ports2[-1]['id'])
ports4 = fake_plugin.get_ports(neutron_context, limit=1,
marker=ports3[-1]['id'])
ports = []
expected_ports = [{'id': 'top_id_0', 'name': 'top'},
{'id': 'top_id_1', 'name': 'bottom'},
{'id': 'top_id_2', 'name': 'bottom'},
{'id': 'top_id_3', 'name': 'top'}]
for _ports in (ports1, ports2, ports3, ports4):
ports.extend(_ports)
self.assertItemsEqual(expected_ports, ports)
ports = fake_plugin.get_ports(neutron_context)
self.assertItemsEqual(expected_ports, ports)
@patch.object(context, 'get_context_from_neutron_context',
new=fake_get_context_from_neutron_context)
@patch.object(plugin.TricirclePlugin, '_get_client',
new=fake_get_client)
def test_get_ports_filters(self):
self._basic_site_route_setup()
fake_plugin = FakePlugin()
neutron_context = FakeNeutronContext()
ports1 = fake_plugin.get_ports(neutron_context,
filters={'id': ['top_id_0']})
ports2 = fake_plugin.get_ports(neutron_context,
filters={'id': ['top_id_1']})
ports3 = fake_plugin.get_ports(neutron_context,
filters={'id': ['top_id_4']})
self.assertEqual([{'id': 'top_id_0', 'name': 'top'}], ports1)
self.assertEqual([{'id': 'top_id_1', 'name': 'bottom'}], ports2)
self.assertEqual([], ports3)
@patch.object(context, 'get_context_from_neutron_context')
@patch.object(db_base_plugin_v2.NeutronDbPluginV2, 'delete_port')
@patch.object(t_client.Client, 'delete_resources')
def test_delete_port(self, mock_client_method, mock_plugin_method,
mock_context_method):
self._basic_site_route_setup()
fake_plugin = FakePlugin()
neutron_context = FakeNeutronContext()
tricircle_context = context.get_db_context()
mock_context_method.return_value = tricircle_context
fake_plugin.delete_port(neutron_context, 'top_id_0')
fake_plugin.delete_port(neutron_context, 'top_id_1')
calls = [mock.call(neutron_context, 'top_id_0'),
mock.call(neutron_context, 'top_id_1')]
mock_plugin_method.assert_has_calls(calls)
mock_client_method.assert_called_once_with('port', tricircle_context,
'bottom_id_1')
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())