From 08be0eba7ecb0f92f0c7cf9dc267eefd7789eb0e Mon Sep 17 00:00:00 2001 From: zhiyuan_cai Date: Fri, 6 May 2016 17:35:05 +0800 Subject: [PATCH] Implement floating ip disassociation According to the current design of cross pod L3 networking, user needs to specify a pod when creating an external network and the external network will be located in this pod. For VMs located in other pods to access the external network, we need a bridge network to connect these pods. We assign the bridge network a CIDR allocated from a CIDR pool. In the pod hosting the VM, say Pod_vm, a bridge external network is created with the CIDR, so we can allocate a floating ip from the CIDR and bind it to the VM port. In the pod hosting the real external network(say "real" here to distinguish with the bridge external network), say Pod_extnet, a bridge internal network is created with the CIDR, so we can create a port with the same ip as floating ip in Pod_vm, and bind it to the real floating ip in Pod_extnet. With the bridge network, via two-step DNAT, the VM can be accessed from the real external network. For example, let's say we have an internal network with CIDR 10.0.1.0/24 and an external network with CIDR 162.3.124.0/24, the CIDR of bridge network is 100.0.1.0/24, when binding a VM ip 10.0.1.4 to a floating ip 162.3.124.5, the VM ip is first bound to 100.0.1.4, which is allocated from 100.0.1.0/24, then 100.0.1.4 is bound to 162.3.124.5. In the case that VM and external network are in the same pod, bridge network is not needed. So plugin needs to distinguish these two cases when handling floating ip disassociation. If VM and external network are in the same pod, plugin only disassociates the binding; if they are in different pods, plugin also needs to release the ip allocated from the bridge network. Change-Id: Ibae353ec81aceda53016b6ea8aba1872d6d514be --- tricircle/common/resource_handle.py | 2 +- tricircle/network/plugin.py | 363 ++++++++++++++------ tricircle/tests/unit/network/test_plugin.py | 141 +++++++- 3 files changed, 388 insertions(+), 118 deletions(-) diff --git a/tricircle/common/resource_handle.py b/tricircle/common/resource_handle.py index 68978864..6096a0c9 100644 --- a/tricircle/common/resource_handle.py +++ b/tricircle/common/resource_handle.py @@ -122,7 +122,7 @@ class NeutronResourceHandle(ResourceHandle): 'router': LIST | CREATE | ACTION | UPDATE, 'security_group': LIST | CREATE | GET, 'security_group_rule': LIST | CREATE | DELETE, - 'floatingip': LIST | CREATE} + 'floatingip': LIST | CREATE | UPDATE | DELETE} def _get_client(self, cxt): return q_client.Client('2.0', diff --git a/tricircle/network/plugin.py b/tricircle/network/plugin.py index f3960292..5240ef34 100644 --- a/tricircle/network/plugin.py +++ b/tricircle/network/plugin.py @@ -47,6 +47,7 @@ import tricircle.common.constants as t_constants import tricircle.common.context as t_context import tricircle.common.exceptions as t_exceptions from tricircle.common.i18n import _ +from tricircle.common.i18n import _LE from tricircle.common.i18n import _LI import tricircle.common.lock_handle as t_lock from tricircle.common import utils @@ -54,6 +55,7 @@ from tricircle.common import xrpcapi import tricircle.db.api as db_api from tricircle.db import core from tricircle.db import models +import tricircle.network.exceptions as t_network_exc from tricircle.network import security_groups @@ -631,11 +633,13 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, def _prepare_top_element(self, t_ctx, q_ctx, project_id, pod, ele, _type, body): def list_resources(t_ctx_, q_ctx_, pod_, ele_, _type_): - return getattr(self, 'get_%ss' % _type_)( - q_ctx_, filters={'name': ele_['id']}) + return getattr(super(TricirclePlugin, self), + 'get_%ss' % _type_)(q_ctx_, + filters={'name': [ele_['id']]}) def create_resources(t_ctx_, q_ctx_, pod_, body_, _type_): - return getattr(self, 'create_%s' % _type_)(q_ctx_, body_) + return getattr(super(TricirclePlugin, self), + 'create_%s' % _type_)(q_ctx_, body_) return t_lock.get_or_create_element( t_ctx, q_ctx, @@ -817,7 +821,7 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, } _, port_id = self._prepare_top_element( t_ctx, q_ctx, project_id, pod, port_ele, 'port', port_body) - return self.get_port(q_ctx, port_id) + return super(TricirclePlugin, self).get_port(q_ctx, port_id) def _get_bottom_bridge_elements(self, q_ctx, project_id, pod, t_net, is_external, t_subnet, t_port): @@ -908,6 +912,8 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, # az hint parameter, so tricircle plugin knows where to create the # corresponding bottom external network. here we get bottom external # network ID from resource routing table. + if not network.get(az_ext.AZ_HINTS): + raise t_exceptions.ExternalNetPodNotSpecify() pod_name = network[az_ext.AZ_HINTS][0] pod = db_api.get_pod_by_name(t_ctx, pod_name) b_net_id = db_api.get_bottom_id_by_top_id_pod_name( @@ -916,6 +922,10 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, # create corresponding bottom router in the pod where external network # is located. t_router = self._get_router(context, router_id) + + # TODO(zhiyuan) decide router is distributed or not from pod table + # currently "distributed" is set to False, should add a metadata field + # to pod table, and decide distributed or not from the metadata later body = {'router': {'name': router_id, 'distributed': False}} _, b_router_id = self._prepare_bottom_element( @@ -1224,7 +1234,7 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, return return_info @staticmethod - def _safe_create_bottom_floatingip(t_ctx, client, fip_net_id, + def _safe_create_bottom_floatingip(t_ctx, pod, client, fip_net_id, fip_address, port_id): try: client.create_floatingips( @@ -1237,127 +1247,270 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, 'comparator': 'eq', 'value': fip_address}]) # NOTE(zhiyuan) if the internal port associated with the existing - # fip is what we expect, just ignore this exception - if fips[0].get('port_id') == port_id: + # fip is what we expect, just ignore this exception; or if the + # existing fip is not associated with any internal port, update the + # fip to add association + if not fips: + # this is rare case that we got IpAddressInUseClient exception + # a second ago but now the floating ip is missing + raise t_network_exc.BottomPodOperationFailure( + resource='floating ip', pod_name=pod['pod_name']) + associated_port_id = fips[0].get('port_id') + if associated_port_id == port_id: pass + elif not associated_port_id: + client.update_floatingips(t_ctx, fips[0]['id'], + {'floatingip': {'port_id': port_id}}) else: raise @staticmethod - def _disassociate_floatingip(context, _id): - with context.session.begin(): - fip_qry = context.session.query(l3_db.FloatingIP) - floating_ips = fip_qry.filter_by(id=_id) - for floating_ip in floating_ips: - floating_ip.update({'fixed_port_id': None, - 'fixed_ip_address': None, - 'router_id': None}) + def _rollback_floatingip_data(context, _id, org_data): + """Rollback the data of floating ip object to the original one + + :param context: request context + :param _id: ID of the floating ip + :param org_data: data of floating ip we rollback to + :return: None + """ + try: + with context.session.begin(): + fip_qry = context.session.query(l3_db.FloatingIP) + floating_ips = fip_qry.filter_by(id=_id) + for floating_ip in floating_ips: + floating_ip.update(org_data) + except Exception as e: + # log the exception and re-raise it + LOG.exception(_LE('Fail to rollback floating ip data, reason: ' + '%(reason)s') % {'reason': e.message}) + raise def update_floatingip(self, context, _id, floatingip): + """Update floating ip object in top and bottom pods + + :param context: request context + :param _id: ID of the floating ip + :param floatingip: data of floating ip we update to + :return: updated floating ip ojbect + """ + org_floatingip_dict = self._make_floatingip_dict( + self._get_floatingip(context, _id)) + res = super(TricirclePlugin, self).update_floatingip( context, _id, floatingip) - try: - t_ctx = t_context.get_context_from_neutron_context(context) - - fip = floatingip['floatingip'] - floatingip_db = self._get_floatingip(context, _id) - int_port_id = fip['port_id'] - project_id = floatingip_db['tenant_id'] - fip_address = floatingip_db['floating_ip_address'] - mappings = db_api.get_bottom_mappings_by_top_id( - t_ctx, int_port_id, t_constants.RT_PORT) - if not mappings: - int_port = self.get_port(context, int_port_id) - int_network = self.get_network(context, int_port['network_id']) - if az_ext.AZ_HINTS not in int_network: - raise Exception('Cross pods L3 networking not support') - self._validate_availability_zones( - context, int_network[az_ext.AZ_HINTS], False) - int_net_pod, _ = az_ag.get_pod_by_az_tenant( - t_ctx, int_network[az_ext.AZ_HINTS][0], project_id) - b_int_net_id = db_api.get_bottom_id_by_top_id_pod_name( - t_ctx, int_network['id'], int_net_pod['pod_name'], - t_constants.RT_NETWORK) - b_int_port_body = { - 'port': { - 'tenant_id': project_id, - 'admin_state_up': True, - 'name': int_port['id'], - 'network_id': b_int_net_id, - 'mac_address': int_port['mac_address'], - 'fixed_ips': [{'ip_address': int_port['fixed_ips'][0][ - 'ip_address']}] - } - } - # TODO(zhiyuan) handle DHCP port ip address conflict problem - _, b_int_port_id = self._prepare_bottom_element( - t_ctx, project_id, int_net_pod, int_port, - t_constants.RT_PORT, b_int_port_body) + if floatingip['floatingip']['port_id']: + self._associate_floatingip(context, _id, floatingip) else: - int_net_pod, b_int_port_id = mappings[0] - ext_net_id = floatingip_db['floating_network_id'] - ext_net = self.get_network(context, ext_net_id) - ext_net_pod = db_api.get_pod_by_name(t_ctx, - ext_net[az_ext.AZ_HINTS][0]) + self._disassociate_floatingip(context, org_floatingip_dict) + return res + except Exception as e: + # NOTE(zhiyuan) when exception occurs, we update floating ip object + # to rollback fixed_port_id, fixed_ip_address, router_id + LOG.exception( + _LE('Fail to update floating ip, reason: ' + '%(reason)s, rollback floating ip data') % { + 'reason': e.message}) + org_data = { + 'fixed_port_id': org_floatingip_dict['port_id'], + 'fixed_ip_address': org_floatingip_dict['fixed_ip_address'], + 'router_id': org_floatingip_dict['router_id']} + self._rollback_floatingip_data(context, _id, org_data) + raise - # external network and internal network are in the same pod, no - # need to use bridge network. - if int_net_pod['pod_name'] == ext_net_pod['pod_name']: - client = self._get_client(int_net_pod['pod_name']) - b_ext_net_id = db_api.get_bottom_id_by_top_id_pod_name( - t_ctx, ext_net_id, ext_net_pod['pod_name'], - t_constants.RT_NETWORK) - self._safe_create_bottom_floatingip( - t_ctx, client, b_ext_net_id, fip_address, b_int_port_id) + def _associate_floatingip(self, context, _id, floatingip): + t_ctx = t_context.get_context_from_neutron_context(context) - return res - - # below handle the case that external network and internal network - # are in different pods - int_client = self._get_client(int_net_pod['pod_name']) - ext_client = self._get_client(ext_net_pod['pod_name']) - ns_bridge_net_name = t_constants.ns_bridge_net_name % project_id - ns_bridge_net = self.get_networks( - context, {'name': [ns_bridge_net_name]})[0] - int_bridge_net_id = db_api.get_bottom_id_by_top_id_pod_name( - t_ctx, ns_bridge_net['id'], int_net_pod['pod_name'], + fip = floatingip['floatingip'] + floatingip_db = self._get_floatingip(context, _id) + int_port_id = fip['port_id'] + project_id = floatingip_db['tenant_id'] + fip_address = floatingip_db['floating_ip_address'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, int_port_id, t_constants.RT_PORT) + if not mappings: + int_port = self.get_port(context, int_port_id) + int_network = self.get_network(context, int_port['network_id']) + if az_ext.AZ_HINTS not in int_network: + raise Exception('Cross pods L3 networking not support') + self._validate_availability_zones( + context, int_network[az_ext.AZ_HINTS], False) + int_net_pod, _ = az_ag.get_pod_by_az_tenant( + t_ctx, int_network[az_ext.AZ_HINTS][0], project_id) + b_int_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, int_network['id'], int_net_pod['pod_name'], t_constants.RT_NETWORK) - ext_bridge_net_id = db_api.get_bottom_id_by_top_id_pod_name( - t_ctx, ns_bridge_net['id'], ext_net_pod['pod_name'], - t_constants.RT_NETWORK) - - t_pod = db_api.get_top_pod(t_ctx) - t_ns_bridge_port = self._get_bridge_interface( - t_ctx, context, project_id, t_pod, ns_bridge_net['id'], - None, b_int_port_id, False) - port_body = { + b_int_port_body = { 'port': { 'tenant_id': project_id, 'admin_state_up': True, - 'name': 'ns_bridge_port', - 'network_id': ext_bridge_net_id, - 'fixed_ips': [{'ip_address': t_ns_bridge_port[ - 'fixed_ips'][0]['ip_address']}] + 'name': int_port['id'], + 'network_id': b_int_net_id, + 'mac_address': int_port['mac_address'], + 'fixed_ips': [{'ip_address': int_port['fixed_ips'][0][ + 'ip_address']}] } } - _, b_ns_bridge_port_id = self._prepare_bottom_element( - t_ctx, project_id, ext_net_pod, t_ns_bridge_port, - t_constants.RT_PORT, port_body) + # TODO(zhiyuan) handle DHCP port ip address conflict problem + _, b_int_port_id = self._prepare_bottom_element( + t_ctx, project_id, int_net_pod, int_port, + t_constants.RT_PORT, b_int_port_body) + else: + int_net_pod, b_int_port_id = mappings[0] + ext_net_id = floatingip_db['floating_network_id'] + ext_net = self.get_network(context, ext_net_id) + ext_net_pod = db_api.get_pod_by_name(t_ctx, + ext_net[az_ext.AZ_HINTS][0]) + + # external network and internal network are in the same pod, no + # need to use bridge network. + if int_net_pod['pod_name'] == ext_net_pod['pod_name']: + client = self._get_client(int_net_pod['pod_name']) b_ext_net_id = db_api.get_bottom_id_by_top_id_pod_name( t_ctx, ext_net_id, ext_net_pod['pod_name'], t_constants.RT_NETWORK) self._safe_create_bottom_floatingip( - t_ctx, ext_client, b_ext_net_id, fip_address, - b_ns_bridge_port_id) - self._safe_create_bottom_floatingip( - t_ctx, int_client, int_bridge_net_id, - t_ns_bridge_port['fixed_ips'][0]['ip_address'], b_int_port_id) + t_ctx, int_net_pod, client, b_ext_net_id, fip_address, + b_int_port_id) + return - return res - except Exception: - # NOTE(zhiyuan) currently we just handle floating ip association - # in this function, so when exception occurs, we update floating - # ip object to unset fixed_port_id, fixed_ip_address, router_id - self._disassociate_floatingip(context, _id) - raise + # below handle the case that external network and internal network + # are in different pods + int_client = self._get_client(int_net_pod['pod_name']) + ext_client = self._get_client(ext_net_pod['pod_name']) + ns_bridge_net_name = t_constants.ns_bridge_net_name % project_id + ns_bridge_net = self.get_networks( + context, {'name': [ns_bridge_net_name]})[0] + int_bridge_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, ns_bridge_net['id'], int_net_pod['pod_name'], + t_constants.RT_NETWORK) + ext_bridge_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, ns_bridge_net['id'], ext_net_pod['pod_name'], + t_constants.RT_NETWORK) + + t_pod = db_api.get_top_pod(t_ctx) + t_ns_bridge_port = self._get_bridge_interface( + t_ctx, context, project_id, t_pod, ns_bridge_net['id'], + None, b_int_port_id, False) + port_body = { + 'port': { + 'tenant_id': project_id, + 'admin_state_up': True, + 'name': 'ns_bridge_port', + 'network_id': ext_bridge_net_id, + 'fixed_ips': [{'ip_address': t_ns_bridge_port[ + 'fixed_ips'][0]['ip_address']}] + } + } + _, b_ns_bridge_port_id = self._prepare_bottom_element( + t_ctx, project_id, ext_net_pod, t_ns_bridge_port, + t_constants.RT_PORT, port_body) + b_ext_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, ext_net_id, ext_net_pod['pod_name'], + t_constants.RT_NETWORK) + self._safe_create_bottom_floatingip( + t_ctx, ext_net_pod, ext_client, b_ext_net_id, fip_address, + b_ns_bridge_port_id) + self._safe_create_bottom_floatingip( + t_ctx, int_net_pod, int_client, int_bridge_net_id, + t_ns_bridge_port['fixed_ips'][0]['ip_address'], b_int_port_id) + + def _disassociate_floatingip(self, context, ori_floatingip_db): + if not ori_floatingip_db['port_id']: + # floating ip has not been associated with fixed ip, no + # operation in bottom pod needed + return + + t_ctx = t_context.get_context_from_neutron_context(context) + project_id = ori_floatingip_db['tenant_id'] + + t_int_port_id = ori_floatingip_db['port_id'] + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, t_int_port_id, t_constants.RT_PORT) + if not mappings: + # floating ip in top pod is associated but no mapping between + # top and bottom internal port, this is an inconsistent state, + # but since bottom internal port does not exist, no operation + # in bottom pod is required + LOG.warning(_LI('Internal port associated with floating ip ' + 'does not exist in bottom pod.')) + return + + b_int_net_pod, b_int_port_id = mappings[0] + t_ext_net_id = ori_floatingip_db['floating_network_id'] + t_ext_net = self.get_network(context, t_ext_net_id) + b_ext_net_pod = db_api.get_pod_by_name(t_ctx, + t_ext_net[az_ext.AZ_HINTS][0]) + b_ext_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, t_ext_net_id, b_ext_net_pod['pod_name'], + t_constants.RT_NETWORK) + + # external network and internal network are in the same pod, so + # bridge network is not created in this pod + if b_int_net_pod['pod_name'] == b_ext_net_pod['pod_name']: + b_client = self._get_client(b_int_net_pod['pod_name']) + b_fips = b_client.list_floatingips( + t_ctx, + [{'key': 'floating_ip_address', + 'comparator': 'eq', + 'value': ori_floatingip_db['floating_ip_address']}, + {'key': 'floating_network_id', + 'comparator': 'eq', + 'value': b_ext_net_id}]) + if not b_fips: + return + b_client.update_floatingips(t_ctx, b_fips[0]['id'], + {'floatingip': {'port_id': None}}) + return + + # below handle the case that external network and internal network + # are in different pods + b_int_client = self._get_client(b_int_net_pod['pod_name']) + b_ext_client = self._get_client(b_ext_net_pod['pod_name']) + ns_bridge_net_name = t_constants.ns_bridge_net_name % project_id + t_ns_bridge_net = self.get_networks( + context, {'name': [ns_bridge_net_name]})[0] + b_int_bridge_net_id = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, t_ns_bridge_net['id'], b_int_net_pod['pod_name'], + t_constants.RT_NETWORK) + t_pod = db_api.get_top_pod(t_ctx) + t_ns_bridge_port = self._get_bridge_interface( + t_ctx, context, project_id, t_pod, t_ns_bridge_net['id'], + None, b_int_port_id, False) + + b_int_fips = b_int_client.list_floatingips( + t_ctx, + [{'key': 'floating_ip_address', + 'comparator': 'eq', + 'value': t_ns_bridge_port['fixed_ips'][0]['ip_address']}, + {'key': 'floating_network_id', + 'comparator': 'eq', + 'value': b_int_bridge_net_id}]) + b_ext_fips = b_ext_client.list_floatingips( + t_ctx, + [{'key': 'floating_ip_address', + 'comparator': 'eq', + 'value': ori_floatingip_db['floating_ip_address']}, + {'key': 'floating_network_id', + 'comparator': 'eq', + 'value': b_ext_net_id}]) + + if b_int_fips: + b_int_client.delete_floatingips( + t_ctx, b_int_fips[0]['id']) + if b_ext_fips: + b_ext_client.update_floatingips( + t_ctx, b_ext_fips[0]['id'], + {'floatingip': {'port_id': None}}) + # delete bridge port + self.delete_port(context, t_ns_bridge_port['id'], l3_port_check=False) + # for bridge port, we have two resource routing entries, one for bridge + # port in top pod, another for bridge port in bottom pod. calling + # delete_port above will delete bridge port in bottom pod as well as + # routing entry for it, but we also need to remove routing entry for + # bridge port in top pod + # bridge network will be deleted when deleting router + with t_ctx.session.begin(): + core.delete_resources(t_ctx, models.ResourceRouting, + [{'key': 'top_id', 'comparator': 'eq', + 'value': t_ns_bridge_port['name']}]) diff --git a/tricircle/tests/unit/network/test_plugin.py b/tricircle/tests/unit/network/test_plugin.py index 341bec32..20208b7d 100644 --- a/tricircle/tests/unit/network/test_plugin.py +++ b/tricircle/tests/unit/network/test_plugin.py @@ -17,6 +17,7 @@ import copy import mock from mock import patch +import netaddr import unittest from sqlalchemy.orm import attributes @@ -67,19 +68,21 @@ BOTTOM1_SUBNETS = [] BOTTOM1_PORTS = [] BOTTOM1_ROUTERS = [] BOTTOM1_SGS = [] +BOTTOM1_FIPS = [] BOTTOM2_NETS = [] BOTTOM2_SUBNETS = [] BOTTOM2_PORTS = [] BOTTOM2_ROUTERS = [] BOTTOM2_SGS = [] +BOTTOM2_FIPS = [] RES_LIST = [TOP_NETS, TOP_SUBNETS, TOP_PORTS, TOP_ROUTERS, TOP_ROUTERPORT, TOP_SUBNETPOOLS, TOP_SUBNETPOOLPREFIXES, TOP_IPALLOCATIONS, TOP_VLANALLOCATIONS, TOP_SEGMENTS, TOP_EXTNETS, TOP_FLOATINGIPS, TOP_SGS, TOP_SG_RULES, BOTTOM1_NETS, BOTTOM1_SUBNETS, BOTTOM1_PORTS, BOTTOM1_ROUTERS, - BOTTOM1_SGS, + BOTTOM1_SGS, BOTTOM1_FIPS, BOTTOM2_NETS, BOTTOM2_SUBNETS, BOTTOM2_PORTS, BOTTOM2_ROUTERS, - BOTTOM2_SGS] + BOTTOM2_SGS, BOTTOM2_FIPS] RES_MAP = {'networks': TOP_NETS, 'subnets': TOP_SUBNETS, 'ports': TOP_PORTS, @@ -155,12 +158,14 @@ class FakeClient(object): 'subnet': BOTTOM1_SUBNETS, 'port': BOTTOM1_PORTS, 'router': BOTTOM1_ROUTERS, - 'security_group': BOTTOM1_SGS}, + 'security_group': BOTTOM1_SGS, + 'floatingip': BOTTOM1_FIPS}, 'pod_2': {'network': BOTTOM2_NETS, 'subnet': BOTTOM2_SUBNETS, 'port': BOTTOM2_PORTS, 'router': BOTTOM2_ROUTERS, - 'security_group': BOTTOM2_SGS}} + 'security_group': BOTTOM2_SGS, + 'floatingip': BOTTOM2_FIPS}} def __init__(self, pod_name): self.pod_name = pod_name @@ -191,10 +196,12 @@ class FakeClient(object): fixed_ip['ip_address']) fixed_ips = body[_type].get('fixed_ips', []) for fixed_ip in fixed_ips: - # just skip ip address check when subnet_id not given - # currently test case doesn't need to cover such situation - if 'subnet_id' not in fixed_ip: - continue + for subnet in self._res_map[self.pod_name]['subnet']: + ip_range = netaddr.IPNetwork(subnet['cidr']) + ip = netaddr.IPAddress(fixed_ip['ip_address']) + if ip in ip_range: + fixed_ip['subnet_id'] = subnet['id'] + break if fixed_ip['ip_address'] in subnet_ips_map.get( fixed_ip['subnet_id'], set()): raise q_exceptions.IpAddressInUseClient() @@ -249,7 +256,33 @@ class FakeClient(object): return self.add_gateway_routers(ctx, args, kwargs) def create_floatingips(self, ctx, body): - # only for mock purpose + fip = self.create_resources('floatingip', ctx, body) + for key in ['fixed_port_id']: + if key not in fip: + fip[key] = None + return fip + + def list_floatingips(self, ctx, filters=None): + filters = filters or [] + return_list = [] + for fip in self._res_map[self.pod_name]['floatingip']: + is_skip = False + for filter in filters: + if filter['key'] not in fip: + is_skip = True + break + if fip[filter['key']] != filter['value']: + is_skip = True + break + if is_skip: + continue + return_list.append(copy.copy(fip)) + return return_list + + def update_floatingips(self, ctx, _id, body): + pass + + def delete_floatingips(self, ctx, _id): pass def create_security_group_rules(self, ctx, body): @@ -350,6 +383,25 @@ def unlink_models(res_list, model_dict, foreign_key, key, link_prop, return +def update_floatingip(self, context, _id, floatingip): + for fip in TOP_FLOATINGIPS: + if fip['id'] != _id: + continue + update_dict = floatingip['floatingip'] + if not floatingip['floatingip']['port_id']: + update_dict['fixed_port_id'] = None + update_dict['fixed_ip_address'] = None + fip.update(update_dict) + return + for port in TOP_PORTS: + if port['id'] != floatingip['floatingip']['port_id']: + continue + update_dict['fixed_port_id'] = port['id'] + update_dict[ + 'fixed_ip_address'] = port['fixed_ips'][0]['ip_address'] + fip.update(update_dict) + + class FakeQuery(object): def __init__(self, records, table): self.records = records @@ -1842,11 +1894,11 @@ class PluginTest(unittest.TestCase, new=mock.Mock) @patch.object(l3_db.L3_NAT_dbonly_mixin, 'update_floatingip', new=mock.Mock) - @patch.object(FakePlugin, '_disassociate_floatingip') + @patch.object(FakePlugin, '_rollback_floatingip_data') @patch.object(FakeClient, 'create_floatingips') @patch.object(context, 'get_context_from_neutron_context') def test_associate_floatingip_port_exception( - self, mock_context, mock_create, mock_disassociate): + self, mock_context, mock_create, mock_rollback): plugin_path = 'tricircle.tests.unit.network.test_plugin.FakePlugin' cfg.CONF.set_override('core_plugin', plugin_path) @@ -1865,7 +1917,72 @@ class PluginTest(unittest.TestCase, self.assertRaises(q_exceptions.ConnectionFailed, fake_plugin.update_floatingip, q_ctx, fip['id'], {'floatingip': fip_body}) - mock_disassociate.assert_called_once_with(q_ctx, fip['id']) + data = {'fixed_port_id': None, + 'fixed_ip_address': None, + 'router_id': None} + mock_rollback.assert_called_once_with(q_ctx, fip['id'], data) + # check the association information is cleared + self.assertIsNone(TOP_FLOATINGIPS[0]['fixed_port_id']) + self.assertIsNone(TOP_FLOATINGIPS[0]['fixed_ip_address']) + self.assertIsNone(TOP_FLOATINGIPS[0]['router_id']) + + @patch.object(ipam_non_pluggable_backend.IpamNonPluggableBackend, + '_allocate_specific_ip', new=_allocate_specific_ip) + @patch.object(ipam_non_pluggable_backend.IpamNonPluggableBackend, + '_generate_ip', new=fake_generate_ip) + @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(subnet_alloc.SubnetAllocator, '_lock_subnetpool', + new=mock.Mock) + @patch.object(l3_db.L3_NAT_dbonly_mixin, 'update_floatingip', + new=update_floatingip) + @patch.object(FakeClient, 'delete_floatingips') + @patch.object(FakeClient, 'update_floatingips') + @patch.object(context, 'get_context_from_neutron_context') + def test_disassociate_floatingip(self, mock_context, mock_update, + mock_delete): + plugin_path = 'tricircle.tests.unit.network.test_plugin.FakePlugin' + cfg.CONF.set_override('core_plugin', plugin_path) + + fake_plugin = FakePlugin() + q_ctx = FakeNeutronContext() + t_ctx = context.get_db_context() + 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}) + + bridge_port_name = constants.ns_bridge_port_name % ( + e_net['tenant_id'], None, b_port_id) + t_pod = db_api.get_top_pod(t_ctx) + mapping = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, bridge_port_name, t_pod['pod_name'], constants.RT_PORT) + # check routing for bridge port in top pod exists + self.assertIsNotNone(mapping) + + # disassociate floating ip + fip_body = {'port_id': None} + fake_plugin.update_floatingip(q_ctx, fip['id'], + {'floatingip': fip_body}) + + fip_id1 = BOTTOM1_FIPS[0]['id'] + fip_id2 = BOTTOM2_FIPS[0]['id'] + mock_update.assert_called_once_with( + t_ctx, fip_id2, {'floatingip': {'port_id': None}}) + mock_delete.assert_called_once_with(t_ctx, fip_id1) + mapping = db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, bridge_port_name, t_pod['pod_name'], constants.RT_PORT) + # check routing for bridge port in top pod is deleted + self.assertIsNone(mapping) + # check the association information is cleared self.assertIsNone(TOP_FLOATINGIPS[0]['fixed_port_id']) self.assertIsNone(TOP_FLOATINGIPS[0]['fixed_ip_address'])