diff --git a/doc/source/index.rst b/doc/source/index.rst index b5d2067d..1ebdecf2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -19,6 +19,7 @@ Contents configuration networking-guide vlan-aware-vms-guide + service-function-chaining-guide api_v1 contributing diff --git a/doc/source/service-function-chaining-guide.rst b/doc/source/service-function-chaining-guide.rst new file mode 100644 index 00000000..918465a0 --- /dev/null +++ b/doc/source/service-function-chaining-guide.rst @@ -0,0 +1,120 @@ +=============================== +Service Function Chaining Guide +=============================== + +Service Function Chaining provides the ability to define an ordered list of +network services (e.g. firewalls, load balancers). These services are then +“stitched” together in the network to create a service chain. + + +Installation +^^^^^^^^^^^^ + +After installing tricircle, please refer to +https://docs.openstack.org/developer/networking-sfc/installation.html +to install networking-sfc. + +Configuration +^^^^^^^^^^^^^ + +- 1 Configure central Neutron server + + After installing the Tricircle and networing-sfc, enable the service plugins + in central Neutron server by adding them in ``neutron.conf.0`` + (typically found in ``/etc/neutron/``):: + + service_plugins=networking_sfc.services.flowclassifier.plugin.FlowClassifierPlugin,tricircle.network.central_sfc_plugin.TricircleSfcPlugin + + In the same configuration file, specify the driver to use in the plugins. :: + + [sfc] + drivers = tricircle_sfc + + [flowclassifier] + drivers = tricircle_fc + +- 2 Configure local Neutron + + Please refer to https://docs.openstack.org/developer/networking-sfc/installation.html#Configuration + to config local networking-sfc. + + +How to play +^^^^^^^^^^^ + +- 1 Create pods via Tricircle Admin API + +- 2 Create necessary resources in central Neutron server :: + + neutron --os-region-name=CentralRegion net-create --provider:network_type vxlan net1 + neutron --os-region-name=CentralRegion subnet-create net1 10.0.0.0/24 + neutron --os-region-name=CentralRegion port-create net1 --name p1 + neutron --os-region-name=CentralRegion port-create net1 --name p2 + neutron --os-region-name=CentralRegion port-create net1 --name p3 + neutron --os-region-name=CentralRegion port-create net1 --name p4 + neutron --os-region-name=CentralRegion port-create net1 --name p5 + neutron --os-region-name=CentralRegion port-create net1 --name p6 + + Please note that network type must be vxlan. + +- 3 Get image ID and flavor ID which will be used in VM booting. In the following step, + the VM will boot from RegionOne and RegionTwo. :: + + glance --os-region-name=RegionOne image-list + nova --os-region-name=RegionOne flavor-list + glance --os-region-name=RegionTwo image-list + nova --os-region-name=RegionTwo flavor-list + +- 4 Boot virtual machines :: + + openstack --os-region-name=RegionOne server create --flavor 1 --image $image1_id --nic port-id=$p1_id vm_src + openstack --os-region-name=RegionOne server create --flavor 1 --image $image1_id --nic port-id=$p2_id --nic port-id=$p3_id vm_sfc1 + openstack --os-region-name=RegionTwo server create --flavor 1 --image $image2_id --nic port-id=$p4_id --nic port-id=$p5_id vm_sfc2 + openstack --os-region-name=RegionTwo server create --flavor 1 --image $image2_id --nic port-id=$p6_id vm_dst + +- 5 Create port pairs in central Neutron server :: + + neutron --os-region-name=CentralRegion port-pair-create --ingress p2 --egress p3 pp1 + neutron --os-region-name=CentralRegion port-pair-create --ingress p4 --egress p5 pp2 + +- 6 Create port pair groups in central Neutron server :: + + neutron --os-region-name=CentralRegion port-pair-group-create --port-pair pp1 ppg1 + neutron --os-region-name=CentralRegion port-pair-group-create --port-pair pp2 ppg2 + +- 7 Create flow classifier in central Neutron server :: + + neutron --os-region-name=CentralRegion flow-classifier-create --source-ip-prefix 10.0.0.0/24 --logical-source-port p1 fc1 + +- 8 Create port chain in central Neutron server :: + + neutron --os-region-name=CentralRegion port-chain-create --flow-classifier fc1 --port-pair-group ppg1 --port-pair-group ppg2 pc1 + +- 9 Show result in CentralRegion, RegionOne and RegionTwo :: + + neutron --os-region-name=CentralRegion port-chain-list + neutron --os-region-name=RegionOne port-chain-list + neutron --os-region-name=RegionTwo port-chain-list + + You will find a same port chain in each region. + +- 10 Check if the port chain is working + + In vm_dst, ping the p1's ip address, it should fail. + + Enable vm_sfc1, vm_sfc2's forwarding function :: + + sudo sh + echo 1 > /proc/sys/net/ipv4/ip_forward + + Add the following route for vm_sfc1, vm_sfc2 :: + + sudo ip route add $p6_ip_address dev eth1 + + In vm_dst, ping the p1's ip address, it should be successfully this time. + + .. note:: Not all images will bring up the second NIC, so you can ssh into vm, use + "ifconfig -a" to check whether all NICs are up, and bring up all NICs if necessary. + In CirrOS you can type the following command to bring up one NIC. :: + + sudo cirros-dhcpc up $nic_name diff --git a/releasenotes/notes/add-service-function-chaining-fc2cf9a2e8610b91.yaml b/releasenotes/notes/add-service-function-chaining-fc2cf9a2e8610b91.yaml new file mode 100644 index 00000000..8b76edf1 --- /dev/null +++ b/releasenotes/notes/add-service-function-chaining-fc2cf9a2e8610b91.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Support service function chaining creation and deletion based on networking-sfc, + currently all the ports in the port chain need to be in the same network and the + network type must be VxLAN. diff --git a/setup.cfg b/setup.cfg index 3bdfa964..6427092b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,9 +56,12 @@ oslo.config.opts = tricircle.db = tricircle.db.opts:list_opts tricircle.network = tricircle.network.opts:list_opts tricircle.xjob = tricircle.xjob.opts:list_opts - tricircle.network.type_drivers = local = tricircle.network.drivers.type_local:LocalTypeDriver vlan = tricircle.network.drivers.type_vlan:VLANTypeDriver vxlan = tricircle.network.drivers.type_vxlan:VxLANTypeDriver flat = tricircle.network.drivers.type_flat:FlatTypeDriver +networking_sfc.flowclassifier.drivers = + tricircle_fc = tricircle.network.central_fc_driver:TricircleFcDriver +networking_sfc.sfc.drivers = + tricircle_sfc = tricircle.network.central_sfc_driver:TricircleSfcDriver \ No newline at end of file diff --git a/tox.ini b/tox.ini index 411bb4b4..392ff650 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ setenv = deps = -r{toxinidir}/test-requirements.txt -egit+https://git.openstack.org/openstack/neutron@master#egg=neutron + -egit+https://git.openstack.org/openstack/networking-sfc@master#egg=networking-sfc commands = rm -Rf .testrepository/times.dbm python setup.py testr --slowest --testr-args='{posargs}' diff --git a/tricircle/api/controllers/job.py b/tricircle/api/controllers/job.py index bf2112a9..ae676038 100644 --- a/tricircle/api/controllers/job.py +++ b/tricircle/api/controllers/job.py @@ -114,7 +114,8 @@ class AsyncJobController(rest.RestController): # if job_type = seg_rule_setup, we should ensure the project id # is equal to the one from resource. - if job_type == constants.JT_SEG_RULE_SETUP: + if job_type in (constants.JT_SEG_RULE_SETUP, + constants.JT_RESOURCE_RECYCLE): if job['project_id'] != job['resource']['project_id']: msg = (_("Specified project_id %(project_id_1)s and resource's" " project_id %(project_id_2)s are different") % diff --git a/tricircle/common/constants.py b/tricircle/common/constants.py index 483cbd8a..1f2d2e89 100644 --- a/tricircle/common/constants.py +++ b/tricircle/common/constants.py @@ -28,6 +28,10 @@ RT_SD_SUBNET = 'shadow_subnet' RT_PORT = 'port' RT_TRUNK = 'trunk' RT_SD_PORT = 'shadow_port' +RT_PORT_PAIR = 'port_pair' +RT_PORT_PAIR_GROUP = 'port_pair_group' +RT_FLOW_CLASSIFIER = 'flow_classifier' +RT_PORT_CHAIN = 'port_chain' RT_ROUTER = 'router' RT_NS_ROUTER = 'ns_router' RT_SG = 'security_group' @@ -73,6 +77,9 @@ interface_port_device_id = 'reserved_gateway_port' MAX_INT = 0x7FFFFFFF DEFAULT_DESTINATION = '0.0.0.0/0' expire_time = datetime.datetime(2000, 1, 1) +STR_IN_USE = 'in use' +STR_USED_BY = 'used by' +STR_CONFLICTS_WITH = 'conflicts with' # job status JS_New = '3_New' @@ -102,6 +109,8 @@ JT_NETWORK_UPDATE = 'update_network' JT_SUBNET_UPDATE = 'subnet_update' JT_SHADOW_PORT_SETUP = 'shadow_port_setup' JT_TRUNK_SYNC = 'trunk_sync' +JT_SFC_SYNC = 'sfc_sync' +JT_RESOURCE_RECYCLE = 'resource_recycle' # network type NT_LOCAL = 'local' @@ -133,7 +142,11 @@ job_resource_map = { JT_SUBNET_UPDATE: [(None, "pod_id"), (RT_SUBNET, "subnet_id")], JT_SHADOW_PORT_SETUP: [(None, "pod_id"), - (RT_NETWORK, "network_id")] + (RT_NETWORK, "network_id")], + JT_SFC_SYNC: [(None, "pod_id"), + (RT_PORT_CHAIN, "portchain_id"), + (RT_NETWORK, "network_id")], + JT_RESOURCE_RECYCLE: [(None, "project_id")] } # map raw job status to more human readable job status @@ -156,7 +169,9 @@ job_handles = { JT_NETWORK_UPDATE: "update_network", JT_SUBNET_UPDATE: "update_subnet", JT_TRUNK_SYNC: "sync_trunk", - JT_SHADOW_PORT_SETUP: "setup_shadow_ports" + JT_SHADOW_PORT_SETUP: "setup_shadow_ports", + JT_SFC_SYNC: "sync_service_function_chain", + JT_RESOURCE_RECYCLE: "recycle_resources" } # map job type to its primary resource and then we only validate the project_id @@ -170,5 +185,7 @@ job_primary_resource_map = { JT_NETWORK_UPDATE: (RT_NETWORK, "network_id"), JT_SUBNET_UPDATE: (RT_SUBNET, "subnet_id"), JT_TRUNK_SYNC: (RT_TRUNK, "trunk_id"), - JT_SHADOW_PORT_SETUP: (RT_NETWORK, "network_id") + JT_SHADOW_PORT_SETUP: (RT_NETWORK, "network_id"), + JT_SFC_SYNC: (RT_PORT_CHAIN, "portchain_id"), + JT_RESOURCE_RECYCLE: (None, "project_id") } diff --git a/tricircle/common/resource_handle.py b/tricircle/common/resource_handle.py index 16de3bf2..36bc7595 100644 --- a/tricircle/common/resource_handle.py +++ b/tricircle/common/resource_handle.py @@ -94,7 +94,11 @@ class NeutronResourceHandle(ResourceHandle): 'security_group': LIST | CREATE | GET, 'security_group_rule': LIST | CREATE | DELETE, 'floatingip': LIST | CREATE | UPDATE | DELETE, - 'trunk': LIST | CREATE | UPDATE | GET | DELETE | ACTION} + 'trunk': LIST | CREATE | UPDATE | GET | DELETE | ACTION, + 'port_chain': LIST | CREATE | DELETE | GET | UPDATE, + 'port_pair_group': LIST | CREATE | DELETE | GET | UPDATE, + 'port_pair': LIST | CREATE | DELETE | GET | UPDATE, + 'flow_classifier': LIST | CREATE | DELETE | GET | UPDATE} def _get_client(self, cxt): token = cxt.auth_token diff --git a/tricircle/common/xrpcapi.py b/tricircle/common/xrpcapi.py index afcf6df6..83252787 100644 --- a/tricircle/common/xrpcapi.py +++ b/tricircle/common/xrpcapi.py @@ -126,3 +126,17 @@ class XJobAPI(object): self.invoke_method( t_ctx, project_id, constants.job_handles[constants.JT_TRUNK_SYNC], constants.JT_TRUNK_SYNC, '%s#%s' % (pod_id, trunk_id)) + + def sync_service_function_chain(self, ctxt, project_id, portchain_id, + net_id, pod_id): + self.invoke_method( + ctxt, project_id, + constants.job_handles[constants.JT_SFC_SYNC], + constants.JT_SFC_SYNC, + '%s#%s#%s' % (pod_id, portchain_id, net_id)) + + def recycle_resources(self, ctxt, project_id): + self.invoke_method( + ctxt, project_id, + constants.job_handles[constants.JT_RESOURCE_RECYCLE], + constants.JT_RESOURCE_RECYCLE, project_id) diff --git a/tricircle/db/api.py b/tricircle/db/api.py index fb3f96c0..cee59a69 100644 --- a/tricircle/db/api.py +++ b/tricircle/db/api.py @@ -678,3 +678,33 @@ def is_valid_model_filters(model, filters): if not hasattr(model, key): return False return True + + +def create_recycle_resource(context, resource_id, resource_type, project_id): + try: + context.session.begin() + route = core.create_resource(context, models.RecycleResources, + {'resource_id': resource_id, + 'resource_type': resource_type, + 'project_id': project_id}) + context.session.commit() + return route + except db_exc.DBDuplicateEntry: + # entry has already been created + context.session.rollback() + return None + finally: + context.session.close() + + +def list_recycle_resources(context, filters=None, sorts=None): + with context.session.begin(): + resources = core.query_resource( + context, models.RecycleResources, filters or [], sorts or []) + return resources + + +def delete_recycle_resource(context, resource_id): + with context.session.begin(): + return core.delete_resource( + context, models.RecycleResources, resource_id) diff --git a/tricircle/db/migrate_repo/versions/009_recycle_resources.py b/tricircle/db/migrate_repo/versions/009_recycle_resources.py new file mode 100644 index 00000000..579f676c --- /dev/null +++ b/tricircle/db/migrate_repo/versions/009_recycle_resources.py @@ -0,0 +1,36 @@ +# Copyright 2017 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + recycle_resources = sql.Table( + 'recycle_resources', meta, + sql.Column('resource_id', sql.String(length=36), primary_key=True), + sql.Column('resource_type', sql.String(length=64), nullable=False), + sql.Column('project_id', sql.String(length=36), + nullable=False, index=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + recycle_resources.create() + + +def downgrade(migrate_engine): + raise NotImplementedError('downgrade not support') diff --git a/tricircle/db/migrate_repo/versions/010_add_resource_routings_bottom_id_index.py b/tricircle/db/migrate_repo/versions/010_add_resource_routings_bottom_id_index.py new file mode 100644 index 00000000..6f27c1eb --- /dev/null +++ b/tricircle/db/migrate_repo/versions/010_add_resource_routings_bottom_id_index.py @@ -0,0 +1,24 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import MetaData, Table +from sqlalchemy import Index + + +def upgrade(migrate_engine): + meta = MetaData(bind=migrate_engine) + resource_routings = Table('resource_routings', meta, autoload=True) + index = Index('resource_routings0bottom_id', + resource_routings.c.bottom_id) + index.create() diff --git a/tricircle/db/models.py b/tricircle/db/models.py index 159c4bcd..06521413 100644 --- a/tricircle/db/models.py +++ b/tricircle/db/models.py @@ -71,7 +71,7 @@ class ResourceRouting(core.ModelBase, core.DictBase, models.TimestampMixin): id = sql.Column(sql.BigInteger().with_variant(sql.Integer(), 'sqlite'), primary_key=True, autoincrement=True) top_id = sql.Column('top_id', sql.String(length=127), nullable=False) - bottom_id = sql.Column('bottom_id', sql.String(length=36)) + bottom_id = sql.Column('bottom_id', sql.String(length=36), index=True) pod_id = sql.Column('pod_id', sql.String(length=36), sql.ForeignKey('pods.pod_id'), nullable=False) @@ -134,3 +134,16 @@ class ShadowAgent(core.ModelBase, core.DictBase): type = sql.Column('type', sql.String(length=36), nullable=False) # considering IPv6 address, set the length to 48 tunnel_ip = sql.Column('tunnel_ip', sql.String(length=48), nullable=False) + + +class RecycleResources(core.ModelBase, core.DictBase): + __tablename__ = 'recycle_resources' + + attributes = ['resource_id', 'resource_type', 'project_id'] + + resource_id = sql.Column('resource_id', + sql.String(length=36), primary_key=True) + resource_type = sql.Column('resource_type', + sql.String(length=64), nullable=False) + project_id = sql.Column('project_id', + sql.String(length=36), nullable=False, index=True) diff --git a/tricircle/network/central_fc_driver.py b/tricircle/network/central_fc_driver.py new file mode 100644 index 00000000..f0f47837 --- /dev/null +++ b/tricircle/network/central_fc_driver.py @@ -0,0 +1,84 @@ +# Copyright 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import helpers as log_helpers +from oslo_log import log + +from networking_sfc.services.flowclassifier.drivers import base as fc_driver + +from neutronclient.common import exceptions as client_exceptions + +import tricircle.common.client as t_client +import tricircle.common.constants as t_constants +import tricircle.common.context as t_context +from tricircle.common import xrpcapi +import tricircle.db.api as db_api + + +LOG = log.getLogger(__name__) + + +class TricircleFcDriver(fc_driver.FlowClassifierDriverBase): + + def __init__(self): + self.xjob_handler = xrpcapi.XJobAPI() + self.clients = {} + + def initialize(self): + pass + + def _get_client(self, region_name): + if region_name not in self.clients: + self.clients[region_name] = t_client.Client(region_name) + return self.clients[region_name] + + @log_helpers.log_method_call + def create_flow_classifier(self, context): + pass + + @log_helpers.log_method_call + def update_flow_classifier(self, context): + pass + + @log_helpers.log_method_call + def delete_flow_classifier(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + flowclassifier_id = context.current['id'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, flowclassifier_id, t_constants.RT_FLOW_CLASSIFIER) + for b_pod, b_classifier_id in mappings: + b_region_name = b_pod['region_name'] + b_client = self._get_client(b_region_name) + try: + b_client.delete_flow_classifiers(t_ctx, b_classifier_id) + except client_exceptions.NotFound: + LOG.debug(('flow classifier: %(classifier_id)s not found, ' + 'region name: %(name)s'), + {'classifier_id': flowclassifier_id, + 'name': b_region_name}) + db_api.delete_mappings_by_bottom_id(t_ctx, b_classifier_id) + + def delete_flow_classifier_precommit(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + flowclassifier_id = context.current['id'] + db_api.create_recycle_resource( + t_ctx, flowclassifier_id, t_constants.RT_FLOW_CLASSIFIER, + t_ctx.project_id) + + @log_helpers.log_method_call + def create_flow_classifier_precommit(self, context): + pass diff --git a/tricircle/network/central_sfc_driver.py b/tricircle/network/central_sfc_driver.py new file mode 100644 index 00000000..5221c832 --- /dev/null +++ b/tricircle/network/central_sfc_driver.py @@ -0,0 +1,181 @@ +# Copyright 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import helpers as log_helpers + +from networking_sfc.services.sfc.drivers import base as sfc_driver + +from oslo_log import log + +from neutron_lib.plugins import directory +from neutronclient.common import exceptions as client_exceptions + +import tricircle.common.client as t_client +import tricircle.common.constants as t_constants +import tricircle.common.context as t_context +from tricircle.common import xrpcapi +import tricircle.db.api as db_api +from tricircle.network import central_plugin + + +LOG = log.getLogger(__name__) + + +class TricircleSfcDriver(sfc_driver.SfcDriverBase): + + def __init__(self): + self.xjob_handler = xrpcapi.XJobAPI() + self.clients = {} + + def initialize(self): + pass + + def _get_client(self, region_name): + if region_name not in self.clients: + self.clients[region_name] = t_client.Client(region_name) + return self.clients[region_name] + + def _get_net_id_by_portpairgroups(self, context, + sfc_plugin, port_pair_groups): + if not port_pair_groups: + return None + port_pairs = sfc_plugin.get_port_pairs( + context, {'portpairgroup_id': port_pair_groups}) + if not port_pairs: + return None + # currently we only support port pairs in the same network + first_ingress = port_pairs[0]['ingress'] + core_plugin = directory.get_plugin() + ingress_port = super(central_plugin.TricirclePlugin, core_plugin + ).get_port(context, first_ingress) + return ingress_port['network_id'] + + @log_helpers.log_method_call + def create_port_chain(self, context): + pass + + @log_helpers.log_method_call + def create_port_chain_precommit(self, context): + plugin_context = context._plugin_context + t_ctx = t_context.get_context_from_neutron_context(plugin_context) + port_chain = context.current + net_id = self._get_net_id_by_portpairgroups( + plugin_context, context._plugin, port_chain['port_pair_groups']) + if net_id: + self.xjob_handler.sync_service_function_chain( + t_ctx, port_chain['project_id'], port_chain['id'], net_id, + t_constants.POD_NOT_SPECIFIED) + + @log_helpers.log_method_call + def delete_port_chain(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portchain_id = context.current['id'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, portchain_id, t_constants.RT_PORT_CHAIN) + for b_pod, b_porchain_id in mappings: + b_region_name = b_pod['region_name'] + b_client = self._get_client(region_name=b_region_name) + try: + b_client.delete_port_chains(t_ctx, b_porchain_id) + except client_exceptions.NotFound: + LOG.debug(('port chain: %(portchain_id)s not found, ' + 'region name: %(name)s'), + {'portchain_id': portchain_id, + 'name': b_region_name}) + db_api.delete_mappings_by_bottom_id(t_ctx, b_porchain_id) + + @log_helpers.log_method_call + def delete_port_chain_precommit(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portchain_id = context.current['id'] + db_api.create_recycle_resource( + t_ctx, portchain_id, t_constants.RT_PORT_CHAIN, + t_ctx.project_id) + + @log_helpers.log_method_call + def delete_port_pair_group(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portpairgroup_id = context.current['id'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, portpairgroup_id, t_constants.RT_PORT_PAIR_GROUP) + for b_pod, b_portpairgroup_id in mappings: + b_region_name = b_pod['region_name'] + b_client = self._get_client(b_region_name) + + try: + b_client.delete_port_pair_groups(t_ctx, b_portpairgroup_id) + except client_exceptions.NotFound: + LOG.debug(('port pair group: %(portpairgroup_id)s not found, ' + 'region name: %(name)s'), + {'portpairgroup_id': portpairgroup_id, + 'name': b_region_name}) + db_api.delete_mappings_by_bottom_id(t_ctx, b_portpairgroup_id) + + def delete_port_pair_group_precommit(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portpairgroup_id = context.current['id'] + db_api.create_recycle_resource( + t_ctx, portpairgroup_id, t_constants.RT_PORT_PAIR_GROUP, + t_ctx.project_id) + + @log_helpers.log_method_call + def delete_port_pair(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portpair_id = context.current['id'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, portpair_id, t_constants.RT_PORT_PAIR) + for b_pod, b_portpair_id in mappings: + b_region_name = b_pod['region_name'] + b_client = self._get_client(b_region_name) + try: + b_client.delete_port_pairs(t_ctx, b_portpair_id) + except client_exceptions.NotFound: + LOG.debug(('port pair: %(portpair_id)s not found, ' + 'region name: %(name)s'), + {'portpair_id': portpair_id, 'name': b_region_name}) + db_api.delete_mappings_by_bottom_id(t_ctx, b_portpair_id) + + def delete_port_pair_precommit(self, context): + t_ctx = t_context.get_context_from_neutron_context( + context._plugin_context) + portpair_id = context.current['id'] + db_api.create_recycle_resource( + t_ctx, portpair_id, t_constants.RT_PORT_PAIR, + t_ctx.project_id) + + @log_helpers.log_method_call + def update_port_chain(self, context): + pass + + @log_helpers.log_method_call + def create_port_pair_group(self, context): + pass + + @log_helpers.log_method_call + def update_port_pair_group(self, context): + pass + + @log_helpers.log_method_call + def create_port_pair(self, context): + pass + + @log_helpers.log_method_call + def update_port_pair(self, context): + pass diff --git a/tricircle/network/central_sfc_plugin.py b/tricircle/network/central_sfc_plugin.py new file mode 100644 index 00000000..7aceecdd --- /dev/null +++ b/tricircle/network/central_sfc_plugin.py @@ -0,0 +1,41 @@ +# Copyright 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from oslo_log import log + +from networking_sfc.extensions import sfc as sfc_ext +from networking_sfc.services.sfc import plugin as sfc_plugin +from neutron_lib import exceptions as n_exc +from neutron_lib.plugins import directory + + +LOG = log.getLogger(__name__) + + +class TricircleSfcPlugin(sfc_plugin.SfcPlugin): + + def __init__(self): + super(TricircleSfcPlugin, self).__init__() + + # TODO(xiulin): Tricircle's top region can not get port's + # binding information well now, so override this function, + # we will improve this later. + def _get_port(self, context, id): + core_plugin = directory.get_plugin() + try: + return core_plugin.get_port(context, id) + except n_exc.PortNotFound: + raise sfc_ext.PortPairPortNotFound(id=id) diff --git a/tricircle/network/drivers/type_vxlan.py b/tricircle/network/drivers/type_vxlan.py index f4c26afc..f464111f 100644 --- a/tricircle/network/drivers/type_vxlan.py +++ b/tricircle/network/drivers/type_vxlan.py @@ -18,9 +18,12 @@ from oslo_log import log from neutron.plugins.ml2 import driver_api from neutron.plugins.ml2.drivers import type_vxlan +from neutron_lib import constants as q_lib_constants from neutron_lib import exceptions as n_exc from tricircle.common import constants +import tricircle.common.context as t_context +import tricircle.db.api as db_api LOG = log.getLogger(__name__) @@ -54,3 +57,13 @@ class VxLANTypeDriver(type_vxlan.VxlanTypeDriver): def get_mtu(self, physical_network=None): pass + + def get_endpoint_by_host(self, host): + LOG.debug("get_endpoint_by_host() called for host %s", host) + host_endpoint = {'ip_address': None} + context = t_context.get_db_context() + agents = db_api.get_agent_by_host_type( + context, host, q_lib_constants.AGENT_TYPE_OVS) + if agents: + host_endpoint['ip_address'] = agents['tunnel_ip'] + return host_endpoint diff --git a/tricircle/network/helper.py b/tricircle/network/helper.py index bce1d1be..9b5b9b92 100644 --- a/tricircle/network/helper.py +++ b/tricircle/network/helper.py @@ -928,6 +928,7 @@ class NetworkHelper(object): 'fixed_ips'][0]['ip_address']}], 'mac_address': port_body['mac_address'], 'device_owner': t_constants.DEVICE_OWNER_SHADOW, + 'device_id': port_body['device_id'], portbindings.HOST_ID: host } } diff --git a/tricircle/network/local_plugin.py b/tricircle/network/local_plugin.py index 1f3ae558..8572bee1 100644 --- a/tricircle/network/local_plugin.py +++ b/tricircle/network/local_plugin.py @@ -590,6 +590,9 @@ class TricirclePlugin(plugin.Ml2Plugin): agent_state = helper.NetworkHelper.construct_agent_data( agent_type, agent_host, tunnel_ip) self.core_plugin.create_or_update_agent(context, agent_state) + driver = self.core_plugin.type_manager.drivers.get('vxlan') + if driver: + driver.obj.add_endpoint(tunnel_ip, agent_host) def _fill_agent_info_in_profile(self, context, port_id, host, profile_dict): diff --git a/tricircle/tests/functional/api/controllers/test_job.py b/tricircle/tests/functional/api/controllers/test_job.py index 35e8c251..2d0b502f 100644 --- a/tricircle/tests/functional/api/controllers/test_job.py +++ b/tricircle/tests/functional/api/controllers/test_job.py @@ -747,7 +747,8 @@ class TestAsyncJobController(API_FunctionalTest): # prepare the project id for job creation, currently job parameter # contains job type and job resource information. job_type = job['type'] - if job_type == constants.JT_SEG_RULE_SETUP: + if job_type in (constants.JT_SEG_RULE_SETUP, + constants.JT_RESOURCE_RECYCLE): project_id = job['resource']['project_id'] else: project_id = uuidutils.generate_uuid() diff --git a/tricircle/tests/unit/api/controllers/test_job.py b/tricircle/tests/unit/api/controllers/test_job.py index e7f8c849..22a9e34d 100644 --- a/tricircle/tests/unit/api/controllers/test_job.py +++ b/tricircle/tests/unit/api/controllers/test_job.py @@ -593,7 +593,8 @@ class AsyncJobControllerTest(unittest.TestCase): # prepare the project id for job creation, currently job parameter # contains job type and job resource information. job_type = job['type'] - if job_type == constants.JT_SEG_RULE_SETUP: + if job_type in (constants.JT_SEG_RULE_SETUP, + constants.JT_RESOURCE_RECYCLE): project_id = job['resource']['project_id'] else: project_id = uuidutils.generate_uuid() diff --git a/tricircle/tests/unit/network/test_central_plugin.py b/tricircle/tests/unit/network/test_central_plugin.py index 1a67c67c..83b9b2ff 100644 --- a/tricircle/tests/unit/network/test_central_plugin.py +++ b/tricircle/tests/unit/network/test_central_plugin.py @@ -2925,6 +2925,7 @@ class PluginTest(unittest.TestCase, 'network_id': db_api.get_bottom_id_by_top_id_region_name( t_ctx, t_net_id, 'pod_1', constants.RT_NETWORK), 'mac_address': 'fa:16:3e:96:41:03', + 'device_id': None, 'fixed_ips': [ {'subnet_id': db_api.get_bottom_id_by_top_id_region_name( t_ctx, t_subnet_id, 'pod_1', constants.RT_SUBNET), diff --git a/tricircle/tests/unit/network/test_central_sfc_plugin.py b/tricircle/tests/unit/network/test_central_sfc_plugin.py new file mode 100644 index 00000000..582f3106 --- /dev/null +++ b/tricircle/tests/unit/network/test_central_sfc_plugin.py @@ -0,0 +1,873 @@ +# Copyright 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from mock import patch +import six +import unittest + +from networking_sfc.db import sfc_db +from networking_sfc.services.flowclassifier import plugin as fc_plugin + +import neutron.conf.common as q_config +from neutron.db import db_base_plugin_v2 +import neutron_lib.context as q_context +from neutron_lib.plugins import directory +from neutronclient.common import exceptions as client_exceptions + +from oslo_config import cfg +from oslo_utils import uuidutils + +from tricircle.common import client +from tricircle.common import constants +from tricircle.common import context +import tricircle.db.api as db_api +from tricircle.db import core +from tricircle.db import models +import tricircle.network.central_fc_driver as fc_driver +from tricircle.network import central_plugin +import tricircle.network.central_sfc_driver as sfc_driver +import tricircle.network.central_sfc_plugin as sfc_plugin +from tricircle.network import helper +import tricircle.tests.unit.utils as test_utils +from tricircle.xjob import xmanager + + +_resource_store = test_utils.get_resource_store() +TOP_PORTS = _resource_store.TOP_PORTS +TOP_PORTPAIRS = _resource_store.TOP_SFC_PORT_PAIRS +TOP_PORTPAIRGROUPS = _resource_store.TOP_SFC_PORT_PAIR_GROUPS +TOP_PORTCHAINS = _resource_store.TOP_SFC_PORT_CHAINS +TOP_FLOWCLASSIFIERS = _resource_store.TOP_SFC_FLOW_CLASSIFIERS +BOTTOM1_PORTS = _resource_store.BOTTOM1_PORTS +BOTTOM2_PORTS = _resource_store.BOTTOM2_PORTS +BOTTOM1_PORTPAIRS = _resource_store.BOTTOM1_SFC_PORT_PAIRS +BOTTOM2_PORTPAIRS = _resource_store.BOTTOM2_SFC_PORT_PAIRS +BOTTOM1_PORTPAIRGROUPS = _resource_store.BOTTOM1_SFC_PORT_PAIR_GROUPS +BOTTOM2_PORTPAIRGROUPS = _resource_store.BOTTOM2_SFC_PORT_PAIR_GROUPS +BOTTOM1_PORTCHAINS = _resource_store.BOTTOM1_SFC_PORT_CHAINS +BOTTOM2_PORTCHAINS = _resource_store.BOTTOM2_SFC_PORT_CHAINS +BOTTOM1_FLOWCLASSIFIERS = _resource_store.BOTTOM1_SFC_FLOW_CLASSIFIERS +BOTTOM2_FLOWCLASSIFIERS = _resource_store.BOTTOM2_SFC_FLOW_CLASSIFIERS +TEST_TENANT_ID = test_utils.TEST_TENANT_ID +DotDict = test_utils.DotDict + + +class FakeNetworkHelper(helper.NetworkHelper): + def __init__(self): + super(FakeNetworkHelper, self).__init__() + + def _get_client(self, region_name=None): + return FakeClient(region_name) + + +class FakeBaseXManager(xmanager.XManager): + def __init__(self): + self.clients = {constants.TOP: client.Client()} + self.helper = FakeNetworkHelper() + + def _get_client(self, region_name=None): + return FakeClient(region_name) + + def sync_service_function_chain(self, ctx, payload): + (b_pod_id, t_port_chain_id, net_id) = payload[ + constants.JT_SFC_SYNC].split('#') + + if b_pod_id == constants.POD_NOT_SPECIFIED: + mappings = db_api.get_bottom_mappings_by_top_id( + ctx, net_id, constants.RT_NETWORK) + b_pods = [mapping[0] for mapping in mappings] + for b_pod in b_pods: + payload = '%s#%s#%s' % (b_pod['pod_id'], t_port_chain_id, + net_id) + super(FakeBaseXManager, self).sync_service_function_chain( + ctx, {constants.JT_SFC_SYNC: payload}) + else: + super(FakeBaseXManager, self).sync_service_function_chain( + ctx, payload) + + +class FakeXManager(FakeBaseXManager): + def __init__(self, fake_plugin): + super(FakeXManager, self).__init__() + self.xjob_handler = FakeBaseRPCAPI(fake_plugin) + + +class FakeBaseRPCAPI(object): + def __init__(self, fake_plugin): + self.xmanager = FakeBaseXManager() + + def sync_service_function_chain(self, ctxt, project_id, portchain_id, + net_id, pod_id): + combine_id = '%s#%s#%s' % (pod_id, portchain_id, net_id) + self.xmanager.sync_service_function_chain( + ctxt, + payload={constants.JT_SFC_SYNC: combine_id}) + + def recycle_resources(self, ctx, project_id): + self.xmanager.recycle_resources(ctx, payload={ + constants.JT_RESOURCE_RECYCLE: project_id}) + + +class FakeRPCAPI(FakeBaseRPCAPI): + def __init__(self, fake_plugin): + self.xmanager = FakeXManager(fake_plugin) + + +class FakeClient(test_utils.FakeClient): + + def delete_resources(self, _type, ctx, _id): + if _type == constants.RT_PORT_PAIR: + pp = self.get_resource(constants.RT_PORT_PAIR, ctx, _id) + if not pp: + raise client_exceptions.NotFound() + if pp['portpairgroup_id']: + raise client_exceptions.Conflict(constants.STR_IN_USE) + elif _type == constants.RT_FLOW_CLASSIFIER: + pc_list = self._res_map[self.region_name][constants.RT_PORT_CHAIN] + for pc in pc_list: + if _id in pc['flow_classifiers']: + raise client_exceptions.Conflict(constants.STR_IN_USE) + + return super(FakeClient, self).delete_resources(_type, ctx, _id) + + def create_resources(self, _type, ctx, body): + if _type == constants.RT_PORT_PAIR: + pp_list = self._res_map[self.region_name][constants.RT_PORT_PAIR] + for pp in pp_list: + if body[_type]['ingress'] == pp['ingress']: + raise client_exceptions.BadRequest(constants.STR_USED_BY) + elif _type == constants.RT_PORT_PAIR_GROUP: + ppg_list = self._res_map[self.region_name][ + constants.RT_PORT_PAIR_GROUP] + for pp in body[_type]['port_pairs']: + for ppg in ppg_list: + if pp in ppg['port_pairs']: + raise client_exceptions.Conflict(constants.STR_IN_USE) + elif _type == constants.RT_FLOW_CLASSIFIER: + fc_list = self._res_map[self.region_name][ + constants.RT_FLOW_CLASSIFIER] + for fc in fc_list: + if (body[_type]['logical_source_port'] == + fc['logical_source_port']): + raise client_exceptions.BadRequest( + constants.STR_CONFLICTS_WITH) + elif _type == constants.RT_PORT_CHAIN: + pc_list = self._res_map[self.region_name][constants.RT_PORT_CHAIN] + for fc in body[_type]['flow_classifiers']: + for pc in pc_list: + if fc in pc['flow_classifiers']: + raise client_exceptions.Conflict(constants.STR_IN_USE) + + return super(FakeClient, self).create_resources(_type, ctx, body) + + def get_port_chains(self, ctx, portchain_id): + return self.get_resource('port_chain', ctx, portchain_id) + + def get_port_pair_groups(self, ctx, portpairgroup_id): + return self.get_resource('port_pair_group', ctx, portpairgroup_id) + + def get_flow_classifiers(self, ctx, flowclassifier_id): + return self.get_resource('flow_classifier', ctx, flowclassifier_id) + + def list_port_pairs(self, ctx, filters=None): + return self.list_resources('port_pair', ctx, filters) + + def list_flow_classifiers(self, ctx, filters=None): + return self.list_resources('flow_classifier', ctx, filters) + + def list_port_chains(self, ctx, filters=None): + return self.list_resources('port_chain', ctx, filters) + + def list_port_pair_groups(self, ctx, filters=None): + return self.list_resources('port_pair_group', ctx, filters) + + def update_port_pair_groups(self, ctx, id, port_pair_group): + filters = [{'key': 'portpairgroup_id', + 'comparator': 'eq', + 'value': id}] + pps = self.list_port_pairs(ctx, filters) + for pp in pps: + pp['portpairgroup_id'] = None + return self.update_resources('port_pair_group', + ctx, id, port_pair_group) + + def get_ports(self, ctx, port_id): + return self.get_resource('port', ctx, port_id) + + def delete_port_chains(self, context, portchain_id): + pc = self.get_resource('port_chain', context, portchain_id) + if not pc: + raise client_exceptions.NotFound() + self.delete_resources('port_chain', context, portchain_id) + + def delete_port_pairs(self, context, portpair_id): + pp = self.get_resource('port_pair', context, portpair_id) + if not pp: + raise client_exceptions.NotFound() + pp = self.get_resource('port_pair', context, portpair_id) + if pp and pp.get('portpairgroup_id'): + raise client_exceptions.Conflict("in use") + self.delete_resources('port_pair', context, portpair_id) + + def delete_port_pair_groups(self, context, portpairgroup_id): + ppg = self.get_resource('port_pair_group', context, portpairgroup_id) + if not ppg: + raise client_exceptions.NotFound() + for pc in BOTTOM1_PORTCHAINS: + if portpairgroup_id in pc['port_pair_groups']: + raise client_exceptions.Conflict("in use") + self.delete_resources('port_pair_group', context, portpairgroup_id) + + def delete_flow_classifiers(self, context, flowclassifier_id): + fc = self.get_resource('flow_classifier', context, flowclassifier_id) + if not fc: + raise client_exceptions.NotFound() + for pc in BOTTOM1_PORTCHAINS: + if flowclassifier_id in pc['flow_classifiers']: + raise client_exceptions.Conflict("in use") + self.delete_resources('flow_classifier', context, flowclassifier_id) + + +class FakeNeutronContext(q_context.Context): + def __init__(self): + self._session = None + self.is_admin = True + self.is_advsvc = False + self.tenant_id = TEST_TENANT_ID + + @property + def session(self): + if not self._session: + self._session = FakeSession() + return self._session + + def elevated(self): + return self + + +class FakeSession(test_utils.FakeSession): + + def _fill_port_chain_dict(self, port_chain, model_dict, fields=None): + model_dict['port_pair_groups'] = [ + assoc['portpairgroup_id'] + for assoc in port_chain['chain_group_associations']] + model_dict['flow_classifiers'] = [ + assoc['flowclassifier_id'] + for assoc in port_chain['chain_classifier_associations']] + + def add_hook(self, model_obj, model_dict): + if model_obj.__tablename__ == 'sfc_port_chains': + self._fill_port_chain_dict(model_obj, model_dict) + + +class FakeDriver(object): + def __init__(self, driver, name): + self.obj = driver + self.name = name + + +class FakeSfcDriver(sfc_driver.TricircleSfcDriver): + def __init__(self): + self.xjob_handler = FakeRPCAPI(self) + self.helper = helper.NetworkHelper(self) + + def _get_client(self, region_name): + return FakeClient(region_name) + + +class FakeFcDriver(fc_driver.TricircleFcDriver): + def __init__(self): + self.xjob_handler = FakeRPCAPI(self) + self.helper = helper.NetworkHelper(self) + + def _get_client(self, region_name): + return FakeClient(region_name) + + +class FakeFcPlugin(fc_plugin.FlowClassifierPlugin): + def __init__(self): + super(FakeFcPlugin, self).__init__() + self.driver_manager.ordered_drivers = [FakeDriver( + FakeFcDriver(), "tricircle_fc")] + + +class FakeSfcPlugin(sfc_plugin.TricircleSfcPlugin): + def __init__(self): + super(FakeSfcPlugin, self).__init__() + self.driver_manager.ordered_drivers = [FakeDriver( + FakeSfcDriver(), "tricircle_sfc")] + + def _get_client(self, region_name): + return FakeClient(region_name) + + def get_port_pairs(self, context, filters=None): + client = self._get_client('top') + _filter = [] + for key, values in six.iteritems(filters): + for v in values: + _filter.append( + {'key': key, 'comparator': 'eq', 'value': v}) + return client.list_resources('port_pair', context, _filter) + + def get_port_chain(self, context, id, fields=None): + client = self._get_client('top') + filter = [{'key': 'id', 'comparator': 'eq', 'value': id}] + portchains = client.list_resources('port_chain', context, filter) + if portchains: + return portchains[0] + return None + + +def fake_get_context_from_neutron_context(q_context): + ctx = context.get_db_context() + ctx.project_id = q_context.project_id + return ctx + + +def fake_make_port_pair_group_dict(self, port_pair_group, fields=None): + return port_pair_group + + +def fake_make_port_pair_dict(self, port_pair, fields=None): + return port_pair + + +class FakeCorePlugin(central_plugin.TricirclePlugin): + def __init__(self): + pass + + def get_port(self, ctx, _id): + return self._get_port(ctx, _id) + + def _get_port(self, ctx, _id): + top_client = FakeClient() + _filters = [{'key': 'id', 'comparator': 'eq', 'value': _id}] + return top_client.list_resources('port', ctx, _filters)[0] + + +def fake_get_plugin(alias='core'): + if alias == 'sfc': + return FakeSfcPlugin() + return FakeCorePlugin() + + +class PluginTest(unittest.TestCase): + def setUp(self): + core.initialize() + core.ModelBase.metadata.create_all(core.get_engine()) + core.get_engine().execute('pragma foreign_keys=on') + self.context = context.Context() + xmanager.IN_TEST = True + directory.get_plugin = fake_get_plugin + + def _basic_pod_setup(self): + pod1 = {'pod_id': 'pod_id_1', + 'region_name': 'pod_1', + 'az_name': 'az_name_1'} + pod2 = {'pod_id': 'pod_id_2', + 'region_name': 'pod_2', + 'az_name': 'az_name_2'} + pod3 = {'pod_id': 'pod_id_0', + 'region_name': 'top_pod', + 'az_name': ''} + for pod in (pod1, pod2, pod3): + db_api.create_pod(self.context, pod) + + def _prepare_net_test(self, project_id, ctx, pod_name): + t_net_id = uuidutils.generate_uuid() + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(ctx, models.ResourceRouting, + {'top_id': t_net_id, + 'bottom_id': t_net_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': constants.RT_NETWORK}) + return t_net_id + + def _prepare_port_test(self, tenant_id, ctx, pod_name, net_id): + t_port_id = uuidutils.generate_uuid() + t_port = { + 'id': t_port_id, + 'network_id': net_id + } + TOP_PORTS.append(DotDict(t_port)) + b_port = { + 'id': t_port_id, + 'network_id': net_id + } + if pod_name == 'pod_1': + BOTTOM1_PORTS.append(DotDict(b_port)) + else: + BOTTOM2_PORTS.append(DotDict(b_port)) + + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(ctx, models.ResourceRouting, + {'top_id': t_port_id, + 'bottom_id': t_port_id, + 'pod_id': pod_id, + 'project_id': tenant_id, + 'resource_type': constants.RT_PORT}) + + return t_port_id + + def _update_port_pair_test(self, ppg_mappings, port_pairs): + for pp_id, ppg_id in six.iteritems(ppg_mappings): + for pp in port_pairs: + if pp['id'] == pp_id: + pp['portpairgroup_id'] = ppg_id + + def _prepare_port_pair_test(self, project_id, t_ctx, pod_name, + index, ingress, egress, create_bottom, + portpairgroup_id=None): + t_pp_id = uuidutils.generate_uuid() + b_pp_id = uuidutils.generate_uuid() + top_pp = { + 'id': t_pp_id, + 'project_id': project_id, + 'tenant_id': project_id, + 'ingress': ingress, + 'egress': egress, + 'name': 'top_pp_%d' % index, + 'service_function_parameters': {"weight": 1, "correlation": None}, + 'description': "description", + 'portpairgroup_id': portpairgroup_id + } + TOP_PORTPAIRS.append(DotDict(top_pp)) + if create_bottom: + btm_pp = { + 'id': b_pp_id, + 'project_id': project_id, + 'tenant_id': project_id, + 'ingress': ingress, + 'egress': egress, + 'name': 'btm_pp_%d' % index, + 'service_function_parameters': {"weight": 1, + "correlation": None}, + 'description': "description", + 'portpairgroup_id': portpairgroup_id + } + if pod_name == 'pod_1': + BOTTOM1_PORTPAIRS.append(DotDict(btm_pp)) + else: + BOTTOM2_PORTPAIRS.append(DotDict(btm_pp)) + + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(t_ctx, models.ResourceRouting, + {'top_id': t_pp_id, + 'bottom_id': b_pp_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': constants.RT_PORT_PAIR}) + + return t_pp_id, b_pp_id + + def _prepare_port_pair_group_test(self, project_id, t_ctx, pod_name, index, + t_pp_ids, create_bottom, b_pp_ids): + t_ppg_id = uuidutils.generate_uuid() + b_ppg_id = uuidutils.generate_uuid() + + top_ppg = { + "group_id": 1, + "description": "", + "tenant_id": project_id, + "port_pair_group_parameters": {"lb_fields": []}, + "port_pairs": t_pp_ids, + "project_id": project_id, + "id": t_ppg_id, + "name": 'top_ppg_%d' % index} + TOP_PORTPAIRGROUPS.append(DotDict(top_ppg)) + if create_bottom: + btm_ppg = { + "group_id": 1, + "description": "", + "tenant_id": project_id, + "port_pair_group_parameters": {"lb_fields": []}, + "port_pairs": b_pp_ids, + "project_id": project_id, + "id": b_ppg_id, + "name": 'btm_ppg_%d' % index} + if pod_name == 'pod_1': + BOTTOM1_PORTPAIRGROUPS.append(DotDict(btm_ppg)) + else: + BOTTOM2_PORTPAIRGROUPS.append(DotDict(btm_ppg)) + + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(t_ctx, models.ResourceRouting, + {'top_id': t_ppg_id, + 'bottom_id': b_ppg_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': + constants.RT_PORT_PAIR_GROUP}) + + return t_ppg_id, b_ppg_id + + def _prepare_flow_classifier_test(self, project_id, t_ctx, pod_name, + index, src_port_id, create_bottom): + t_fc_id = uuidutils.generate_uuid() + b_fc_id = uuidutils.generate_uuid() + + top_fc = { + "source_port_range_min": None, + "destination_ip_prefix": None, + "protocol": None, + "description": "", + "l7_parameters": {}, + "source_port_range_max": None, + "id": t_fc_id, + "name": "t_fc_%s" % index, + "ethertype": "IPv4", + "tenant_id": project_id, + "source_ip_prefix": "1.0.0.0/24", + "logical_destination_port": None, + "destination_port_range_min": None, + "destination_port_range_max": None, + "project_id": project_id, + "logical_source_port": src_port_id} + + TOP_FLOWCLASSIFIERS.append(DotDict(top_fc)) + if create_bottom: + btm_fc = { + "source_port_range_min": None, + "destination_ip_prefix": None, + "protocol": None, + "description": "", + "l7_parameters": {}, + "source_port_range_max": None, + "id": b_fc_id, + "name": "b_fc_%s" % index, + "ethertype": "IPv4", + "tenant_id": project_id, + "source_ip_prefix": "1.0.0.0/24", + "logical_destination_port": None, + "destination_port_range_min": None, + "destination_port_range_max": None, + "project_id": project_id, + "logical_source_port": src_port_id} + if pod_name == 'pod_1': + BOTTOM1_FLOWCLASSIFIERS.append(DotDict(btm_fc)) + else: + BOTTOM2_FLOWCLASSIFIERS.append(DotDict(btm_fc)) + + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(t_ctx, models.ResourceRouting, + {'top_id': t_fc_id, + 'bottom_id': b_fc_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': + constants.RT_FLOW_CLASSIFIER}) + + return t_fc_id, b_fc_id + + def _prepare_port_chain_test(self, project_id, t_ctx, pod_name, + index, create_bottom, ids): + t_pc_id = uuidutils.generate_uuid() + b_pc_id = uuidutils.generate_uuid() + + top_pc = { + "tenant_id": project_id, + "name": "t_pc_%s" % index, + "chain_parameters": { + "symmetric": False, "correlation": "mpls"}, + "port_pair_groups": ids['t_ppg_id'], + "flow_classifiers": ids['t_fc_id'], + "project_id": project_id, + "chain_id": 1, + "description": "", + "id": t_pc_id} + + TOP_PORTCHAINS.append(DotDict(top_pc)) + if create_bottom: + btm_pc = { + "tenant_id": project_id, + "name": "b_pc_%s" % index, + "chain_parameters": { + "symmetric": False, "correlation": "mpls"}, + "port_pair_groups": ids['b_ppg_id'], + "flow_classifiers": ids['b_fc_id'], + "project_id": project_id, + "chain_id": 1, + "description": "", + "id": b_pc_id} + if pod_name == 'pod_1': + BOTTOM1_PORTCHAINS.append(DotDict(btm_pc)) + else: + BOTTOM2_PORTCHAINS.append(DotDict(btm_pc)) + + pod_id = 'pod_id_1' if pod_name == 'pod_1' else 'pod_id_2' + core.create_resource(t_ctx, models.ResourceRouting, + {'top_id': t_pc_id, + 'bottom_id': b_pc_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': constants.RT_PORT_CHAIN}) + + return t_pc_id, b_pc_id + + def test_get_client(self): + driver = fc_driver.TricircleFcDriver() + t_client = driver._get_client('top') + self.assertEqual(t_client.region_name, 'top') + + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + @patch.object(directory, 'get_plugin', new=fake_get_plugin) + def test_get_port(self): + self._basic_pod_setup() + project_id = TEST_TENANT_ID + fake_plugin = FakeSfcPlugin() + t_ctx = context.get_db_context() + port_id = self._prepare_port_test(project_id, t_ctx, 'pod_1', None) + port = fake_plugin._get_port(context, port_id) + self.assertIsNotNone(port) + + @patch.object(db_base_plugin_v2.NeutronDbPluginV2, 'get_port', + new=FakeCorePlugin.get_port) + @patch.object(sfc_db.SfcDbPlugin, 'get_port_pairs', + new=FakeSfcPlugin.get_port_pairs) + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + def test_create_port_chain(self): + project_id = TEST_TENANT_ID + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + self._basic_pod_setup() + fake_plugin = FakeSfcPlugin() + + t_net_id = self._prepare_net_test(project_id, t_ctx, 'pod_1') + ingress = self._prepare_port_test(project_id, t_ctx, 'pod_1', t_net_id) + egress = self._prepare_port_test(project_id, t_ctx, 'pod_1', t_net_id) + src_port_id = self._prepare_port_test(project_id, + t_ctx, 'pod_1', t_net_id) + t_pp1_id, _ = self._prepare_port_pair_test( + project_id, t_ctx, 'pod_1', 0, ingress, egress, False) + t_ppg1_id, _ = self._prepare_port_pair_group_test( + project_id, t_ctx, 'pod_1', 0, [t_pp1_id], False, None) + ppg1_mapping = {t_pp1_id: t_ppg1_id} + self._update_port_pair_test(ppg1_mapping, TOP_PORTPAIRS) + t_fc1_id, _ = self._prepare_flow_classifier_test( + project_id, t_ctx, 'pod_1', 0, src_port_id, False) + body = {"port_chain": { + "tenant_id": project_id, + "name": "pc1", + "chain_parameters": { + "symmetric": False, "correlation": "mpls"}, + "port_pair_groups": [t_ppg1_id], + "flow_classifiers": [t_fc1_id], + "project_id": project_id, + "chain_id": 1, + "description": ""}} + t_pc1 = fake_plugin.create_port_chain(q_ctx, body) + pp1_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pp1_id, constants.RT_PORT_PAIR) + ppg1_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_ppg1_id, constants.RT_PORT_PAIR_GROUP) + fc1_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_fc1_id, constants.RT_FLOW_CLASSIFIER) + pc1_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pc1['id'], constants.RT_PORT_CHAIN) + btm1_pp_ids = [btm_pp['id'] for btm_pp in BOTTOM1_PORTPAIRS] + btm1_ppg_ids = [btm_ppg['id'] for btm_ppg in BOTTOM1_PORTPAIRGROUPS] + btm1_fc_ids = [btm_fc['id'] for btm_fc in BOTTOM1_FLOWCLASSIFIERS] + btm1_pc_ids = [btm_pc['id'] for btm_pc in BOTTOM1_PORTCHAINS] + b_pp1_id = pp1_mappings[0][1] + b_ppg1_id = ppg1_mappings[0][1] + b_fc1_id = fc1_mappings[0][1] + b_pc1_id = pc1_mappings[0][1] + self.assertEqual([b_pp1_id], btm1_pp_ids) + self.assertEqual([b_ppg1_id], btm1_ppg_ids) + self.assertEqual([b_fc1_id], btm1_fc_ids) + self.assertEqual([b_pc1_id], btm1_pc_ids) + + # make conflict + TOP_PORTCHAINS.pop() + TOP_FLOWCLASSIFIERS.pop() + TOP_PORTPAIRGROUPS.pop() + TOP_PORTPAIRS.pop() + b_ppg1_mapping = {b_pp1_id: b_ppg1_id} + self._update_port_pair_test(b_ppg1_mapping, BOTTOM1_PORTPAIRS) + db_api.create_recycle_resource( + t_ctx, t_ppg1_id, constants.RT_PORT_PAIR_GROUP, q_ctx.project_id) + + t_pp2_id, _ = self._prepare_port_pair_test( + project_id, t_ctx, 'pod_1', 0, ingress, egress, False) + t_ppg2_id, _ = self._prepare_port_pair_group_test( + project_id, t_ctx, 'pod_1', 0, [t_pp2_id], False, None) + ppg2_mapping = {t_pp2_id: t_ppg2_id} + self._update_port_pair_test(ppg2_mapping, TOP_PORTPAIRS) + t_fc2_id, _ = self._prepare_flow_classifier_test( + project_id, t_ctx, 'pod_1', 0, src_port_id, False) + body2 = {"port_chain": { + "tenant_id": project_id, + "name": "pc1", + "chain_parameters": { + "symmetric": False, "correlation": "mpls"}, + "port_pair_groups": [t_ppg2_id], + "flow_classifiers": [t_fc2_id], + "project_id": project_id, + "chain_id": 1, + "description": ""}} + t_pc2 = fake_plugin.create_port_chain(q_ctx, body2) + pp2_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pp2_id, constants.RT_PORT_PAIR) + ppg2_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_ppg2_id, constants.RT_PORT_PAIR_GROUP) + fc2_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_fc2_id, constants.RT_FLOW_CLASSIFIER) + pc2_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pc2['id'], constants.RT_PORT_CHAIN) + btm1_pp_ids = [btm_pp['id'] for btm_pp in BOTTOM1_PORTPAIRS] + btm1_ppg_ids = [btm_ppg['id'] for btm_ppg in BOTTOM1_PORTPAIRGROUPS] + btm1_fc_ids = [btm_fc['id'] for btm_fc in BOTTOM1_FLOWCLASSIFIERS] + btm1_pc_ids = [btm_pc['id'] for btm_pc in BOTTOM1_PORTCHAINS] + b_pp2_id = pp2_mappings[0][1] + b_ppg2_id = ppg2_mappings[0][1] + b_fc2_id = fc2_mappings[0][1] + b_pc2_id = pc2_mappings[0][1] + self.assertEqual([b_pp2_id], btm1_pp_ids) + self.assertEqual([b_ppg2_id], btm1_ppg_ids) + self.assertEqual([b_fc2_id], btm1_fc_ids) + self.assertEqual([b_pc2_id], btm1_pc_ids) + + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + def test_delete_port_chain(self): + project_id = TEST_TENANT_ID + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + self._basic_pod_setup() + fake_plugin = FakeSfcPlugin() + ids = {'t_ppg_id': [uuidutils.generate_uuid()], + 'b_ppg_id': [uuidutils.generate_uuid()], + 't_fc_id': [uuidutils.generate_uuid()], + 'b_fc_id': [uuidutils.generate_uuid()]} + t_pc_id1, _ = self._prepare_port_chain_test( + project_id, t_ctx, 'pod_1', 0, True, ids) + + fake_plugin.delete_port_chain(q_ctx, t_pc_id1) + pc_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pc_id1, constants.RT_PORT_CHAIN) + self.assertEqual(len(TOP_PORTCHAINS), 0) + self.assertEqual(len(BOTTOM1_PORTCHAINS), 0) + self.assertEqual(len(pc_mappings), 0) + + t_pc_id2, _ = self._prepare_port_chain_test( + project_id, t_ctx, 'pod_1', 0, True, ids) + BOTTOM1_PORTCHAINS.pop() + fake_plugin.delete_port_chain(q_ctx, t_pc_id2) + pc_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pc_id2, constants.RT_PORT_CHAIN) + self.assertEqual(len(TOP_PORTCHAINS), 0) + self.assertEqual(len(pc_mappings), 0) + + @patch.object(sfc_db.SfcDbPlugin, '_make_port_pair_group_dict', + new=fake_make_port_pair_group_dict) + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + def test_delete_port_pair_group(self): + project_id = TEST_TENANT_ID + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + self._basic_pod_setup() + fake_plugin = FakeSfcPlugin() + + t_pp_id = uuidutils.generate_uuid() + b_pp_id = uuidutils.generate_uuid() + + t_ppg_id1, _ = self._prepare_port_pair_group_test( + project_id, t_ctx, 'pod_1', 0, [t_pp_id], True, [b_pp_id]) + fake_plugin.delete_port_pair_group(q_ctx, t_ppg_id1) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_ppg_id1, constants.RT_PORT_PAIR_GROUP) + self.assertEqual(len(TOP_PORTPAIRGROUPS), 0) + self.assertEqual(len(BOTTOM1_PORTPAIRGROUPS), 0) + self.assertEqual(len(ppg_mappings), 0) + + t_ppg_id2, _ = self._prepare_port_pair_group_test( + project_id, t_ctx, 'pod_1', 0, [t_pp_id], True, [b_pp_id]) + BOTTOM1_PORTPAIRGROUPS.pop() + fake_plugin.delete_port_pair_group(q_ctx, t_ppg_id2) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_ppg_id2, constants.RT_PORT_PAIR_GROUP) + self.assertEqual(len(TOP_PORTPAIRGROUPS), 0) + self.assertEqual(len(ppg_mappings), 0) + + @patch.object(sfc_db.SfcDbPlugin, '_make_port_pair_dict', + new=fake_make_port_pair_dict) + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + def test_delete_port_pair(self): + project_id = TEST_TENANT_ID + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + self._basic_pod_setup() + fake_plugin = FakeSfcPlugin() + + ingress = uuidutils.generate_uuid() + egress = uuidutils.generate_uuid() + t_pp1_id, _ = self._prepare_port_pair_test( + project_id, t_ctx, 'pod_1', 0, ingress, egress, True) + fake_plugin.delete_port_pair(q_ctx, t_pp1_id) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pp1_id, constants.RT_PORT_PAIR_GROUP) + self.assertEqual(len(TOP_PORTPAIRS), 0) + self.assertEqual(len(BOTTOM1_PORTPAIRS), 0) + self.assertEqual(len(ppg_mappings), 0) + + t_pp2_id, _ = self._prepare_port_pair_test( + project_id, t_ctx, 'pod_1', 0, ingress, egress, True) + BOTTOM1_PORTPAIRS.pop() + fake_plugin.delete_port_pair(q_ctx, t_pp2_id) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_pp2_id, constants.RT_PORT_PAIR_GROUP) + self.assertEqual(len(TOP_PORTPAIRS), 0) + self.assertEqual(len(ppg_mappings), 0) + + @patch.object(context, 'get_context_from_neutron_context', + new=fake_get_context_from_neutron_context) + def test_delete_flow_classifier(self): + project_id = TEST_TENANT_ID + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + self._basic_pod_setup() + fake_plugin = FakeFcPlugin() + + src_port_id = uuidutils.generate_uuid() + + t_fc_id1, _ = self._prepare_flow_classifier_test( + project_id, t_ctx, 'pod_1', 0, src_port_id, True) + fake_plugin.delete_flow_classifier(q_ctx, t_fc_id1) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_fc_id1, constants.RT_FLOW_CLASSIFIER) + self.assertEqual(len(TOP_FLOWCLASSIFIERS), 0) + self.assertEqual(len(BOTTOM1_FLOWCLASSIFIERS), 0) + self.assertEqual(len(ppg_mappings), 0) + + t_fc_id2, _ = self._prepare_flow_classifier_test( + project_id, t_ctx, 'pod_1', 0, src_port_id, True) + BOTTOM1_FLOWCLASSIFIERS.pop() + fake_plugin.delete_flow_classifier(q_ctx, t_fc_id2) + ppg_mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_fc_id2, constants.RT_FLOW_CLASSIFIER) + self.assertEqual(len(TOP_FLOWCLASSIFIERS), 0) + self.assertEqual(len(ppg_mappings), 0) + + def tearDown(self): + core.ModelBase.metadata.drop_all(core.get_engine()) + test_utils.get_resource_store().clean() + cfg.CONF.unregister_opts(q_config.core_opts) + xmanager.IN_TEST = False diff --git a/tricircle/tests/unit/network/test_helper.py b/tricircle/tests/unit/network/test_helper.py index a3ef8a99..b229bf53 100644 --- a/tricircle/tests/unit/network/test_helper.py +++ b/tricircle/tests/unit/network/test_helper.py @@ -111,7 +111,8 @@ class HelperTest(unittest.TestCase): 'id': 'port-id-%d' % i, 'fixed_ips': [{'ip_address': '10.0.1.%d' % i}], 'mac_address': 'fa:16:3e:d4:01:%02x' % i, - 'binding:host_id': 'host1' + 'binding:host_id': 'host1', + 'device_id': None } for i in range(1, 20)] agents = [{'type': 'Open vSwitch agent', 'tunnel_ip': '192.168.1.101'} for _ in range(1, 20)] diff --git a/tricircle/tests/unit/network/test_local_plugin.py b/tricircle/tests/unit/network/test_local_plugin.py index 86d2072e..bcb640a7 100644 --- a/tricircle/tests/unit/network/test_local_plugin.py +++ b/tricircle/tests/unit/network/test_local_plugin.py @@ -90,9 +90,18 @@ def list_resource(_type, is_top, filters=None): return ret +class FakeTypeManager(object): + + def __init__(self): + self.drivers = {} + + class FakeCorePlugin(object): supported_extension_aliases = ['agent'] + def __init__(self): + self.type_manager = FakeTypeManager() + def create_network(self, context, network): create_resource('network', False, network['network']) return network['network'] diff --git a/tricircle/tests/unit/utils.py b/tricircle/tests/unit/utils.py index 04be2fc9..2c274104 100644 --- a/tricircle/tests/unit/utils.py +++ b/tricircle/tests/unit/utils.py @@ -15,6 +15,7 @@ import copy +from oslo_utils import uuidutils import six from sqlalchemy.orm import attributes from sqlalchemy.orm import exc @@ -50,7 +51,11 @@ class ResourceStore(object): ('dnsnameservers', None), ('trunks', 'trunk'), ('subports', None), - ('agents', 'agent')] + ('agents', 'agent'), + ('sfc_port_pairs', constants.RT_PORT_PAIR), + ('sfc_port_pair_groups', constants.RT_PORT_PAIR_GROUP), + ('sfc_port_chains', constants.RT_PORT_CHAIN), + ('sfc_flow_classifiers', constants.RT_FLOW_CLASSIFIER)] def __init__(self): self.store_list = [] @@ -549,6 +554,8 @@ class FakeClient(object): def create_resources(self, _type, ctx, body): res_list = self._res_map[self.region_name][_type] res = dict(body[_type]) + if 'id' not in res: + res['id'] = uuidutils.generate_uuid() res_list.append(res) return res diff --git a/tricircle/tests/unit/xjob/test_xmanager.py b/tricircle/tests/unit/xjob/test_xmanager.py index f8a08134..8549f521 100644 --- a/tricircle/tests/unit/xjob/test_xmanager.py +++ b/tricircle/tests/unit/xjob/test_xmanager.py @@ -809,6 +809,7 @@ class XManagerTest(unittest.TestCase): 'device_owner': 'compute:None', 'binding:vif_type': 'ovs', 'binding:host_id': 'host1', + 'device_id': None, 'mac_address': 'fa:16:3e:d4:01:03', 'fixed_ips': [{'subnet_id': subnet1_id, 'ip_address': '10.0.1.3'}]}) @@ -817,6 +818,7 @@ class XManagerTest(unittest.TestCase): 'device_owner': 'compute:None', 'binding:vif_type': 'ovs', 'binding:host_id': 'host2', + 'device_id': None, 'mac_address': 'fa:16:3e:d4:01:03', 'fixed_ips': [{'subnet_id': subnet1_id, 'ip_address': '10.0.1.4'}]}) diff --git a/tricircle/xjob/xmanager.py b/tricircle/xjob/xmanager.py index 1d4a00cd..9da1babb 100644 --- a/tricircle/xjob/xmanager.py +++ b/tricircle/xjob/xmanager.py @@ -19,6 +19,7 @@ import eventlet import netaddr import random import six +import time from oslo_config import cfg from oslo_log import log as logging @@ -151,6 +152,9 @@ class XManager(PeriodicTasks): constants.JT_CONFIGURE_ROUTE: self.configure_route, constants.JT_ROUTER_SETUP: self.setup_bottom_router, constants.JT_PORT_DELETE: self.delete_server_port, + constants.JT_SFC_SYNC: + self.sync_service_function_chain, + constants.JT_RESOURCE_RECYCLE: self.recycle_resources, constants.JT_SEG_RULE_SETUP: self.configure_security_group_rules, constants.JT_NETWORK_UPDATE: self.update_network, constants.JT_SUBNET_UPDATE: self.update_subnet, @@ -1045,7 +1049,8 @@ class XManager(PeriodicTasks): {'key': 'fields', 'comparator': 'eq', 'value': ['id', 'binding:vif_type', 'binding:host_id', 'fixed_ips', - 'device_owner', 'mac_address']}]) + 'device_owner', 'device_id', + 'mac_address']}]) LOG.debug('Shadow ports %s in pod %s %s', b_sw_ports, target_pod_id, run_label) LOG.debug('Ports %s in pod %s %s', @@ -1253,3 +1258,272 @@ class XManager(PeriodicTasks): except q_cli_exceptions.NotFound: LOG.error('trunk: %(trunk_id)s not found, pod name: %(name)s', {'trunk_id': b_trunk_id, 'name': b_region_name}) + + def _delete_port_pair_by_ingress(self, ctx, b_client, ingress, project_id): + filters = [{'key': 'ingress', + 'comparator': 'eq', + 'value': ingress}, + {'key': 'project_id', + 'comparator': 'eq', + 'value': project_id} + ] + pps = b_client.list_port_pairs(ctx, filters=filters) + if not pps: + return + self._delete_bottom_resource_by_id( + ctx, constants.RT_PORT_PAIR, pps[0]['id'], + b_client, project_id) + + def _delete_flow_classifier_by_src_port(self, ctx, b_client, + port_id, project_id): + filters = [{'key': 'logical_source_port', + 'comparator': 'eq', + 'value': port_id}, + {'key': 'project_id', + 'comparator': 'eq', + 'value': project_id} + ] + fcs = b_client.list_flow_classifiers(ctx, filters=filters) + if not fcs: + return + self._delete_bottom_resource_by_id( + ctx, constants.RT_FLOW_CLASSIFIER, fcs[0]['id'], + b_client, project_id) + + def _delete_portchain_by_fc_id(self, ctx, b_client, fc_id, project_id): + filters = [{'key': 'project_id', + 'comparator': 'eq', + 'value': project_id}] + pcs = b_client.list_port_chains(ctx, filters=filters) + for pc in pcs: + if fc_id in pc['flow_classifiers']: + self._delete_bottom_resource_by_id( + ctx, constants.RT_PORT_CHAIN, pc['id'], + b_client, project_id) + return + + def _clear_bottom_portpairgroup_portpairs(self, ctx, b_client, + pp_ids, project_id): + filters = [{'key': 'project_id', + 'comparator': 'eq', + 'value': project_id}] + ppgs = b_client.list_port_pair_groups(ctx, filters=filters) + for pp_id in pp_ids: + for ppg in ppgs: + if pp_id in ppg['port_pairs']: + ppg_body = {'port_pair_group': { + 'port_pairs': [] + }} + b_client.update_port_pair_groups(ctx, ppg['id'], ppg_body) + break + + def _delete_bottom_resource_by_id(self, ctx, + res_type, res_id, b_client, project_id): + try: + b_client.delete_resources(res_type, ctx, res_id) + except q_cli_exceptions.NotFound: + LOG.debug(('%(res_type)s: %(id)s not found, ' + 'region name: %(name)s'), + {'res_type': res_type, + 'id': res_id, + 'name': b_client.region_name}) + except q_cli_exceptions.Conflict as e: + if constants.STR_IN_USE in e.message: + LOG.debug(('%(res_type)s: %(id)s in use, ' + 'region name: %(name)s'), + {'res_type': res_type, + 'id': res_id, + 'name': b_client.region_name}) + if res_type == constants.RT_FLOW_CLASSIFIER: + self._delete_portchain_by_fc_id( + ctx, b_client, res_id, project_id) + self._delete_bottom_resource_by_id( + ctx, constants.RT_FLOW_CLASSIFIER, + res_id, b_client, project_id) + # we are deleting the port pair, meaning that the port pair + # should be no longer used, so we remove it from + # its port pair group, if any. + elif res_type == constants.RT_PORT_PAIR: + self._clear_bottom_portpairgroup_portpairs( + ctx, b_client, [res_id], project_id) + self._delete_bottom_resource_by_id( + ctx, constants.RT_PORT_PAIR, + res_id, b_client, project_id) + # conflict exception is not expected to be raised when + # deleting port pair group, because port pair group is only + # deleted during resource recycling, and we guarantee that + # its port chain will be deleted before. + # and, deleting port chain will not raise conflict exception + else: + raise + else: + raise + db_api.delete_mappings_by_bottom_id(ctx, res_id) + + @_job_handle(constants.JT_RESOURCE_RECYCLE) + def recycle_resources(self, ctx, payload): + project_id = payload[constants.JT_RESOURCE_RECYCLE] + filters = [{'key': 'project_id', + 'comparator': 'eq', + 'value': project_id}] + resources = db_api.list_recycle_resources(ctx, filters) + if not resources: + return + max_retries = 4 + # recycle_resources is triggered at the end of the + # sync_service_function_chain function, need to consider the + # situation which recycle_resources has been run but + # sync_service_function_chain function has not ended. + filters = [{'key': 'type', + 'comparator': 'eq', + 'value': constants.JT_SFC_SYNC}] + for i in range(max_retries): + sync_sfc_job = db_api.list_jobs(ctx, filters) + if sync_sfc_job: + if i == max_retries - 1: + return + time.sleep(5) + + res_map = collections.defaultdict(list) + for res in resources: + res_map[res['resource_type']].append(res['resource_id']) + + resource_types = [constants.RT_PORT_CHAIN, + constants.RT_FLOW_CLASSIFIER, + constants.RT_PORT_PAIR_GROUP, + constants.RT_PORT_PAIR] + + for res_type in resource_types: + for res_id in res_map[res_type]: + b_resources = db_api.get_bottom_mappings_by_top_id( + ctx, res_id, res_type) + for b_pod, b_res_id in b_resources: + b_client = self._get_client(b_pod['region_name']) + self._delete_bottom_resource_by_id( + ctx, res_type, b_res_id, b_client, ctx.project_id) + db_api.delete_recycle_resource(ctx, res_id) + + def _prepare_sfc_bottom_element(self, ctx, project_id, b_pod, ele, + res_type, body, b_client, **kwargs): + max_retries = 2 + for i in range(max_retries): + try: + _, b_res_id = self.helper.prepare_bottom_element( + ctx, project_id, b_pod, ele, res_type, body) + return b_res_id + except q_cli_exceptions.BadRequest as e: + if i == max_retries - 1: + raise + if (constants.STR_USED_BY not in e.message and + constants.STR_CONFLICTS_WITH not in e.message): + raise + if res_type == constants.RT_PORT_PAIR: + self._delete_port_pair_by_ingress( + ctx, b_client, kwargs['ingress'], project_id) + elif res_type == constants.RT_FLOW_CLASSIFIER: + self._delete_flow_classifier_by_src_port( + ctx, b_client, kwargs['logical_source_port'], + project_id) + else: + raise + except q_cli_exceptions.Conflict as e: + if i == max_retries - 1: + raise + if constants.STR_IN_USE not in e.message: + raise + if res_type == constants.RT_PORT_PAIR_GROUP: + self._clear_bottom_portpairgroup_portpairs( + ctx, b_client, kwargs['port_pairs'], project_id) + elif res_type == constants.RT_PORT_CHAIN: + self._delete_portchain_by_fc_id( + ctx, b_client, kwargs['fc_id'], project_id) + else: + raise + + @_job_handle(constants.JT_SFC_SYNC) + def sync_service_function_chain(self, ctx, payload): + (b_pod_id, t_port_chain_id, net_id) = payload[ + constants.JT_SFC_SYNC].split('#') + + if b_pod_id == constants.POD_NOT_SPECIFIED: + mappings = db_api.get_bottom_mappings_by_top_id( + ctx, net_id, constants.RT_NETWORK) + b_pods = [mapping[0] for mapping in mappings] + for b_pod in b_pods: + self.xjob_handler.sync_service_function_chain( + ctx, ctx.project_id, t_port_chain_id, + net_id, b_pod['pod_id']) + return + + # abbreviation, pp: port pair, ppg: port pair group, + # pc: port chain, fc: flow classifier + t_client = self._get_client() + t_pc = t_client.get_port_chains(ctx, t_port_chain_id) + b_pod = db_api.get_pod(ctx, b_pod_id) + region_name = b_pod['region_name'] + b_client = self._get_client(region_name) + # delete action + if not t_pc: + self.xjob_handler.recycle_resources(ctx, ctx.project_id) + return + + t_pps = {} + t_ppgs = [] + for ppg_id in t_pc['port_pair_groups']: + ppg = t_client.get_port_pair_groups(ctx, ppg_id) + if not ppg: + LOG.error('port pair group: %(ppg_id)s not found, ' + 'pod name: %(name)s', {'ppg_id': ppg_id, + 'name': region_name}) + raise + t_ppgs.append(ppg) + + for ppg in t_ppgs: + filters = [{'key': 'portpairgroup_id', + 'comparator': 'eq', + 'value': ppg['id']}] + pp = t_client.list_port_pairs(ctx, filters=filters) + if pp: + t_pps[ppg['id']] = pp + b_pp_ids = {} + for key, value in six.iteritems(t_pps): + b_pp_ids[key] = [] + for pp in value: + pp_id = pp.pop('id') + b_pp_id = self._prepare_sfc_bottom_element( + ctx, pp['project_id'], b_pod, {'id': pp_id}, + constants.RT_PORT_PAIR, {'port_pair': pp}, b_client, + ingress=pp['ingress']) + b_pp_ids[key].append(b_pp_id) + + b_ppg_ids = [] + for ppg in t_ppgs: + ppg['port_pairs'] = b_pp_ids.get(ppg['id'], []) + ppg_id = ppg.pop('id') + ppg.pop('group_id') + b_ppg_id = self._prepare_sfc_bottom_element( + ctx, ppg['project_id'], b_pod, {'id': ppg_id}, + constants.RT_PORT_PAIR_GROUP, {'port_pair_group': ppg}, + b_client, port_pairs=ppg['port_pairs']) + b_ppg_ids.append(b_ppg_id) + + b_fc_ids = [] + for fc_id in t_pc['flow_classifiers']: + fc = t_client.get_flow_classifiers(ctx, fc_id) + if fc: + fc_id = fc.pop('id') + b_fc_id = self._prepare_sfc_bottom_element( + ctx, ppg['project_id'], b_pod, {'id': fc_id}, + constants.RT_FLOW_CLASSIFIER, {'flow_classifier': fc}, + b_client, logical_source_port=fc['logical_source_port']) + b_fc_ids.append(b_fc_id) + + t_pc.pop('id') + t_pc['port_pair_groups'] = b_ppg_ids + t_pc['flow_classifiers'] = b_fc_ids + self._prepare_sfc_bottom_element( + ctx, t_pc['project_id'], b_pod, {'id': t_port_chain_id}, + constants.RT_PORT_CHAIN, {'port_chain': t_pc}, b_client, + fc_id=b_fc_ids[0]) + + self.xjob_handler.recycle_resources(ctx, t_pc['project_id'])