diff --git a/tricircle/common/resource_handle.py b/tricircle/common/resource_handle.py index c3fde336..8340c505 100644 --- a/tricircle/common/resource_handle.py +++ b/tricircle/common/resource_handle.py @@ -117,13 +117,14 @@ class GlanceResourceHandle(ResourceHandle): class NeutronResourceHandle(ResourceHandle): service_type = cons.ST_NEUTRON - support_resource = {'network': LIST | CREATE | DELETE | GET, - 'subnet': LIST | CREATE | DELETE | GET | UPDATE, - 'port': LIST | CREATE | DELETE | GET, - 'router': LIST | CREATE | ACTION | GET | UPDATE, - 'security_group': LIST | CREATE | GET, - 'security_group_rule': LIST | CREATE | DELETE, - 'floatingip': LIST | CREATE | UPDATE | DELETE} + support_resource = { + 'network': LIST | CREATE | DELETE | GET, + 'subnet': LIST | CREATE | DELETE | GET | UPDATE, + 'port': LIST | CREATE | DELETE | GET, + 'router': LIST | CREATE | DELETE | ACTION | GET | UPDATE, + 'security_group': LIST | CREATE | GET, + 'security_group_rule': LIST | CREATE | DELETE, + 'floatingip': LIST | CREATE | UPDATE | DELETE} def _get_client(self, cxt): return q_client.Client('2.0', diff --git a/tricircle/db/api.py b/tricircle/db/api.py index 9705be44..dd2c39e8 100644 --- a/tricircle/db/api.py +++ b/tricircle/db/api.py @@ -184,6 +184,22 @@ def get_bottom_mappings_by_tenant_pod(context, return routings +def delete_mappings_by_top_id(context, top_id): + with context.session.begin(): + core.delete_resources( + context, models.ResourceRouting, + filters=[{'key': 'top_id', 'comparator': 'eq', + 'value': top_id}]) + + +def delete_mappings_by_bottom_id(context, bottom_id): + with context.session.begin(): + core.delete_resources( + context, models.ResourceRouting, + filters=[{'key': 'bottom_id', 'comparator': 'eq', + 'value': bottom_id}]) + + def get_next_bottom_pod(context, current_pod_id=None): pods = list_pods(context, sorts=[(models.Pod.pod_id, True)]) # NOTE(zhiyuan) number of pods is small, just traverse to filter top pod diff --git a/tricircle/network/plugin.py b/tricircle/network/plugin.py index 17c9707c..a915d37e 100644 --- a/tricircle/network/plugin.py +++ b/tricircle/network/plugin.py @@ -690,7 +690,127 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, def create_router(self, context, router): return super(TricirclePlugin, self).create_router(context, router) + def _delete_top_bridge_resource(self, t_ctx, q_ctx, resource_type, + resource_id, resource_name): + with t_ctx.session.begin(): + core.update_resources( + t_ctx, models.ResourceRouting, + [{'key': 'bottom_id', 'comparator': 'eq', + 'value': resource_id}], + {'bottom_id': None, + 'created_at': t_constants.expire_time, + 'updated_at': t_constants.expire_time}) + if resource_type == t_constants.RT_PORT: + getattr(super(TricirclePlugin, self), 'delete_%s' % resource_type)( + q_ctx, resource_id) + else: + getattr(self, 'delete_%s' % resource_type)(q_ctx, resource_id) + with t_ctx.session.begin(): + core.delete_resources(t_ctx, models.ResourceRouting, + [{'key': 'top_id', + 'comparator': 'eq', + 'value': resource_name}]) + + def _delete_top_bridge_network_subnet(self, t_ctx, q_ctx): + project_id = t_ctx.project_id + ew_bridge_subnet_name = t_constants.ew_bridge_subnet_name % project_id + ns_bridge_subnet_name = t_constants.ns_bridge_subnet_name % project_id + subnet_names = [ew_bridge_subnet_name, ns_bridge_subnet_name] + for subnet_name in subnet_names: + bridge_subnets = super(TricirclePlugin, self).get_subnets( + q_ctx, {'name': [subnet_name]}) + if bridge_subnets: + self._delete_top_bridge_resource( + t_ctx, q_ctx, t_constants.RT_SUBNET, + bridge_subnets[0]['id'], subnet_name) + ew_bridge_net_name = t_constants.ew_bridge_net_name % project_id + ns_bridge_net_name = t_constants.ns_bridge_net_name % project_id + net_names = [ew_bridge_net_name, ns_bridge_net_name] + for net_name in net_names: + bridge_nets = super(TricirclePlugin, self).get_networks( + q_ctx, {'name': [net_name]}) + if bridge_nets: + self._delete_top_bridge_resource( + t_ctx, q_ctx, t_constants.RT_NETWORK, bridge_nets[0]['id'], + net_name) + + def _delete_top_bridge_port(self, t_ctx, q_ctx, bridge_port_id, + bridge_port_name): + self._delete_top_bridge_resource(t_ctx, q_ctx, t_constants.RT_PORT, + bridge_port_id, bridge_port_name) + def delete_router(self, context, _id): + router = super(TricirclePlugin, + self)._ensure_router_not_in_use(context, _id) + project_id = router['tenant_id'] + t_ctx = t_context.get_context_from_neutron_context(context) + mappings = db_api.get_bottom_mappings_by_top_id(t_ctx, _id, + t_constants.RT_ROUTER) + for pod, b_router_id in mappings: + b_client = self._get_client(pod['pod_name']) + + ew_port_name = t_constants.ew_bridge_port_name % (project_id, + b_router_id) + ew_ports = super(TricirclePlugin, self).get_ports( + context, {'name': [ew_port_name]}) + if ew_ports: + t_ew_port_id = ew_ports[0]['id'] + b_ew_port_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, t_ew_port_id, pod['pod_name'], t_constants.RT_PORT) + if b_ew_port_id: + request_body = {'port_id': b_ew_port_id} + try: + b_client.action_routers(t_ctx, 'remove_interface', + b_router_id, request_body) + except Exception as e: + if e.status_code == 404: + # 404 error means that the router interface has + # been already detached, skip this exception + pass + raise + db_api.delete_mappings_by_top_id(t_ctx, t_ew_port_id) + self._delete_top_bridge_port(t_ctx, context, t_ew_port_id, + ew_port_name) + + ns_port_name = t_constants.ns_bridge_port_name % ( + project_id, b_router_id, None) + ns_ports = super(TricirclePlugin, self).get_ports( + context, {'name': [ns_port_name]}) + if ns_ports: + t_ns_port_id = ns_ports[0]['id'] + b_client.action_routers(t_ctx, 'remove_gateway', b_router_id) + self._delete_top_bridge_port(t_ctx, context, t_ns_port_id, + ns_port_name) + else: + ns_subnet_name = t_constants.ns_bridge_subnet_name % project_id + ns_subnets = super(TricirclePlugin, + self).get_subnets( + context, {'name': [ns_subnet_name]}) + if ns_subnets: + t_ns_subnet_id = ns_subnets[0]['id'] + b_ns_subnet_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, t_ns_subnet_id, pod['pod_name'], + t_constants.RT_SUBNET) + if b_ns_subnet_id: + request_body = {'subnet_id': b_ns_subnet_id} + try: + b_client.action_routers(t_ctx, 'remove_interface', + b_router_id, request_body) + except Exception as e: + if e.status_code == 404: + # 404 error means that the router interface has + # been already detached, skip this exception + pass + raise + + b_client.delete_routers(t_ctx, b_router_id) + db_api.delete_mappings_by_bottom_id(t_ctx, b_router_id) + + routers = super(TricirclePlugin, self).get_routers( + context, {'tenant_id': [project_id]}) + if len(routers) <= 1: + self._delete_top_bridge_network_subnet(t_ctx, context) + super(TricirclePlugin, self).delete_router(context, _id) def _prepare_top_element(self, t_ctx, q_ctx, diff --git a/tricircle/tests/unit/network/test_plugin.py b/tricircle/tests/unit/network/test_plugin.py index c8a02bb8..1837c61b 100644 --- a/tricircle/tests/unit/network/test_plugin.py +++ b/tricircle/tests/unit/network/test_plugin.py @@ -199,6 +199,11 @@ class DotDict(dict): return self.get(item) +class DotList(list): + def all(self): + return self + + class FakeNeutronClient(object): _res_map = {'top': {'port': TOP_PORTS}, @@ -367,6 +372,9 @@ class FakeClient(object): 'comparator': 'eq', 'value': net_id}])[0] + def delete_networks(self, ctx, net_id): + self.delete_resources('network', ctx, net_id) + def list_subnets(self, ctx, filters=None): return self.list_resources('subnet', ctx, filters) @@ -399,6 +407,13 @@ class FakeClient(object): def delete_ports(self, ctx, port_id): self.delete_resources('port', ctx, port_id) + index = -1 + for i, allocation in enumerate(TOP_IPALLOCATIONS): + if allocation['port_id'] == port_id: + index = i + break + if index != -1: + del TOP_IPALLOCATIONS[index] def add_gateway_routers(self, ctx, *args, **kwargs): # only for mock purpose @@ -441,6 +456,9 @@ class FakeClient(object): 'value': router_id}])[0] return _fill_external_gateway_info(router) + def delete_routers(self, ctx, router_id): + self.delete_resources('router', ctx, router_id) + def action_routers(self, ctx, action, *args, **kwargs): # divide into three functions for test purpose if action == 'add_interface': @@ -669,7 +687,7 @@ class FakeQuery(object): filtered_list.append(record) return FakeQuery(filtered_list, self.table) - def delete(self): + def delete(self, synchronize_session=False): for model_obj in self.records: unlink_models(RES_MAP['routers'], model_obj, 'router_id', 'id', 'attached_ports', 'port_id', 'port_id') @@ -1381,7 +1399,7 @@ class PluginTest(unittest.TestCase, 'name': 'top_router', 'distributed': False, 'tenant_id': tenant_id, - 'attached_ports': [] + 'attached_ports': DotList() } TOP_ROUTERS.append(DotDict(t_router)) else: @@ -1875,7 +1893,7 @@ class PluginTest(unittest.TestCase, 'name': 'router', 'distributed': False, 'tenant_id': tenant_id, - 'attached_ports': [] + 'attached_ports': DotList() } TOP_ROUTERS.append(DotDict(t_router)) @@ -1957,7 +1975,7 @@ class PluginTest(unittest.TestCase, 'name': 'router', 'distributed': False, 'tenant_id': tenant_id, - 'attached_ports': [] + 'attached_ports': DotList() } TOP_ROUTERS.append(DotDict(t_router)) @@ -2214,7 +2232,6 @@ class PluginTest(unittest.TestCase, (t_port_id, b_port_id, fip, e_net) = self._prepare_associate_floatingip_test(t_ctx, q_ctx, fake_plugin) - # associate floating ip fip_body = {'port_id': t_port_id} fake_plugin.update_floatingip(q_ctx, fip['id'], @@ -2241,6 +2258,58 @@ class PluginTest(unittest.TestCase, # check routing for bridge port in top pod is deleted self.assertIsNone(mapping) + @patch.object(driver.Pool, 'get_instance', new=fake_get_instance) + @patch.object(ipam_pluggable_backend.IpamPluggableBackend, + '_allocate_ips_for_port', new=fake_allocate_ips_for_port) + @patch.object(l3_db.L3_NAT_dbonly_mixin, '_make_router_dict', + new=fake_make_router_dict) + @patch.object(db_base_plugin_common.DbBasePluginCommon, + '_make_subnet_dict', new=fake_make_subnet_dict) + @patch.object(l3_db.L3_NAT_dbonly_mixin, 'update_floatingip', + new=update_floatingip) + @patch.object(context, 'get_context_from_neutron_context') + def test_delete_router(self, mock_context): + fake_plugin = FakePlugin() + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + t_ctx.project_id = 'test_tenant_id' + mock_context.return_value = t_ctx + + (t_port_id, b_port_id, + fip, e_net) = self._prepare_associate_floatingip_test(t_ctx, q_ctx, + fake_plugin) + # associate floating ip + fip_body = {'port_id': t_port_id} + fake_plugin.update_floatingip(q_ctx, fip['id'], + {'floatingip': fip_body}) + # disassociate floating ip + fip_body = {'port_id': None} + fake_plugin.update_floatingip(q_ctx, fip['id'], + {'floatingip': fip_body}) + + t_router_id = TOP_ROUTERS[0]['id'] + for port in TOP_PORTS: + if port['id'] == t_port_id: + t_subnet_id = port['fixed_ips'][0]['subnet_id'] + fake_plugin.remove_router_interface(q_ctx, t_router_id, + {'subnet_id': t_subnet_id}) + fake_plugin.update_router(q_ctx, t_router_id, + {'router': {'external_gateway_info': {}}}) + + top_res_sets = [TOP_NETS, TOP_SUBNETS, TOP_PORTS] + top_res_nums = [len(top_res_set) for top_res_set in top_res_sets] + top_pre_created_res_nums = [0, 0, 0] + for i, top_res_set in enumerate(top_res_sets): + for top_res in top_res_set: + if top_res.get('name', '').find('bridge') != -1: + top_pre_created_res_nums[i] += 1 + fake_plugin.delete_router(q_ctx, t_router_id) + + # check pre-created networks, subnets and ports are all deleted + for i, top_res_set in enumerate(top_res_sets): + self.assertEqual(top_res_nums[i] - top_pre_created_res_nums[i], + len(top_res_set)) + @patch.object(context, 'get_context_from_neutron_context') def test_create_security_group_rule(self, mock_context): self._basic_pod_route_setup()