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:
parent
3b8e791e18
commit
eb72e17653
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -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
|
|
@ -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())
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
Loading…
Reference in New Issue