Replace chassis and remote_vtep with port binding

Remote VTEP and port bound to a chassis are mutually exclusive, plus,
chassis ID was used for storing VTEP IP. This change proposes wrapping
both fields into a single key on lport.

Partial-Bug: #1690775
Change-Id: Iefd1fdc5a0a7ac62f812a8ca0e67c2acc3842350
This commit is contained in:
Dima Kuznetsov 2017-07-19 08:18:47 +03:00
parent bb09d75ff7
commit dc80063919
17 changed files with 174 additions and 128 deletions

View File

@ -130,7 +130,7 @@ class ChassisSNATApp(df_base_app.DFlowApp, snat_mixin.SNATApp_mixin):
if self.external_host_mac is not None:
# install flows only when compute port is added
if self.is_data_port(lport):
self.chassis = lport.chassis
self.chassis = lport.binding.chassis
self.install_lport_based_flows(lport)
else:

View File

@ -72,7 +72,8 @@ class MigrationApp(df_base_app.DFlowApp):
if not remote_chassis:
# chassis has not been online yet.
return
lport.peer_vtep_address = remote_chassis.ip
old_chassis = lport.binding.chassis
lport.binding.chassis = dest_chassis
LOG.info("src process migration event port = %(port)s"
"original_port = %(original_port)s"
@ -82,7 +83,7 @@ class MigrationApp(df_base_app.DFlowApp):
'chassis': dest_chassis})
# source node and other related nodes
if original_lport and lport.chassis.id != chassis_name:
if original_lport and old_chassis.id != chassis_name:
original_lport.emit_remote_deleted()
lport.emit_remote_created()

View File

@ -148,7 +148,7 @@ class TunnelingApp(df_base_app.DFlowApp):
self.ofproto.OFPFC_MODIFY)
def _add_egress_dispatch_flow(self, lport, segmentation_id):
remote_ip = lport.peer_vtep_address
remote_ip = lport.binding.ip
ofport = self._get_lport_tunnel_ofport(lport)
LOG.debug("set egress dispatch flow %(seg)s peer %(remote_ip)s",
{'seg': segmentation_id,
@ -227,7 +227,7 @@ class TunnelingApp(df_base_app.DFlowApp):
lport = self.db_store.get_one(l2.LogicalPort(id=port_id))
if not lport:
continue
peer_ip = lport.peer_vtep_address
peer_ip = lport.binding.ip
if peer_ip in peer_ip_list:
continue
peer_ip_list.add(peer_ip)

View File

@ -142,6 +142,17 @@ class DfLocalController(object):
self.nb_api.subscriber.unregister_topic(topic)
self._sync.remove_topic(topic)
def _get_ports_by_chassis(self, chassis):
return self.db_store.get_all(
l2.LogicalPort(
binding=l2.PortBinding(
type=l2.BINDING_CHASSIS,
chassis=chassis.id,
),
),
index=l2.LogicalPort.get_index('chassis_id'),
)
def update_chassis(self, chassis):
self.db_store.update(chassis)
remote_chassis_name = chassis.id
@ -149,20 +160,14 @@ class DfLocalController(object):
return
# Notify about remote port update
index = l2.LogicalPort.get_index('chassis_id')
remote_ports = self.db_store.get_all(l2.LogicalPort(chassis=chassis),
index=index)
for port in remote_ports:
for port in self._get_ports_by_chassis(chassis):
self._logical_port_process(port)
def delete_chassis(self, chassis):
LOG.info("Deleting remote ports in remote chassis %s", chassis.id)
# Chassis is deleted, there is no reason to keep the remote port
# in it.
index = l2.LogicalPort.get_index('chassis_id')
remote_ports = self.db_store.get_all(l2.LogicalPort(chassis=chassis),
index=index)
for port in remote_ports:
for port in self._get_ports_by_chassis(chassis):
self._delete_lport_instance(port)
self.db_store.delete(chassis)
@ -182,8 +187,7 @@ class DfLocalController(object):
lport.id)
return
chassis = lport.chassis
is_local = (chassis.id == self.chassis_name)
is_local = lport.binding.is_local
lport.is_local = is_local
if is_local:
if not lport.ofport:
@ -191,9 +195,6 @@ class DfLocalController(object):
if not lport.ofport:
# Not attached to the switch. Maybe it's a subport?
lport.ofport = self._get_trunk_subport_ofport(lport)
else:
lport.peer_vtep_address = (chassis.id if lport.remote_vtep else
chassis.ip)
if not lport.ofport and lport.is_local:
# The tunnel port online event will update the remote logical
@ -224,9 +225,11 @@ class DfLocalController(object):
pass
def update_lport(self, lport):
chassis = lport.chassis
if (not lport.remote_vtep and
not self._is_physical_chassis(chassis)):
if (
lport.binding is None or
(lport.binding.type == l2.BINDING_CHASSIS and
not self._is_physical_chassis(lport.binding.chassis))
):
LOG.debug(("Port %s has not been bound or it is a vPort"),
lport.id)
return

View File

@ -200,7 +200,7 @@ class Topology(object):
self.ovs_to_lport_mapping[ovs_port.id] = OvsLportMapping(
lport_id=lport_id, topic=topic)
chassis = lport.chassis
chassis = lport.binding.chassis
# check if migration occurs
if chassis.id != self.chassis_name:
device_owner = lport.device_owner

View File

@ -9,11 +9,13 @@
# 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 copy
from jsonmodels import fields
from jsonmodels import models
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_log import log
import dragonflow.db.field_types as df_fields
@ -83,6 +85,38 @@ class DhcpParams(models.Base):
siaddr = df_fields.IpAddressField()
BINDING_CHASSIS = 'chassis'
BINDING_VTEP = 'vtep'
class PortBinding(models.Base):
type = df_fields.EnumField((BINDING_CHASSIS, BINDING_VTEP), required=True)
chassis = df_fields.ReferenceField(core.Chassis)
vtep_address = df_fields.IpAddressField()
@property
def ip(self):
if self.type == BINDING_CHASSIS:
return self.chassis.ip
elif self.type == BINDING_VTEP:
return self.vtep_address
return None
@property
def is_local(self):
if self.type == BINDING_CHASSIS:
return self.chassis.id == cfg.CONF.host
return False
def __deepcopy__(self, memo):
return PortBinding(
type=self.type,
chassis=copy.deepcopy(self.chassis, memo),
vtep_address=self.vtep_address,
)
# LogicalPort events
EVENT_LOCAL_CREATED = 'local_created'
EVENT_REMOTE_CREATED = 'remote_created'
@ -98,7 +132,7 @@ EVENT_REMOTE_DELETED = 'remote_deleted'
EVENT_LOCAL_UPDATED, EVENT_REMOTE_UPDATED,
EVENT_LOCAL_DELETED, EVENT_REMOTE_DELETED,
}, indexes={
'chassis_id': 'chassis.id',
'chassis_id': 'binding.chassis.id',
'lswitch_id': 'lswitch.id',
'ip,lswitch': ('ips', 'lswitch.id'),
})
@ -109,7 +143,7 @@ class LogicalPort(mf.ModelBase, mixins.Name, mixins.Version, mixins.Topic,
subnets = df_fields.ReferenceListField(Subnet)
macs = df_fields.ListOfField(df_fields.MacAddressField())
enabled = fields.BoolField()
chassis = df_fields.ReferenceField(core.Chassis)
binding = fields.EmbeddedField(PortBinding)
lswitch = df_fields.ReferenceField(LogicalSwitch)
security_groups = df_fields.ReferenceListField(secgroups.SecurityGroup)
allowed_address_pairs = fields.ListField(AddressPair)
@ -117,16 +151,13 @@ class LogicalPort(mf.ModelBase, mixins.Name, mixins.Version, mixins.Topic,
device_owner = fields.StringField()
device_id = fields.StringField()
qos_policy = df_fields.ReferenceField(qos.QosPolicy)
remote_vtep = fields.BoolField()
dhcp_params = fields.EmbeddedField(DhcpParams)
binding_vnic_type = df_fields.EnumField(portbindings.VNIC_TYPES)
def __init__(self, ofport=None, is_local=None,
peer_vtep_address=None, **kwargs):
def __init__(self, ofport=None, is_local=None, **kwargs):
super(LogicalPort, self).__init__(**kwargs)
self.ofport = ofport
self.is_local = is_local
self.peer_vtep_address = peer_vtep_address
@property
def ip(self):

View File

@ -88,23 +88,40 @@ def _build_dhcp_params(port):
return ret
def _build_port_binding(port):
profile = port.get(portbindings.PROFILE)
if profile:
port_key = profile.get(df_const.DF_BINDING_PROFILE_PORT_KEY)
if port_key == df_const.DF_REMOTE_PORT_TYPE:
return l2.PortBinding(
type=l2.BINDING_VTEP,
vtep_address=profile.get(df_const.DF_BINDING_PROFILE_HOST_IP)
)
chassis = port.get(portbindings.HOST_ID)
if chassis:
return l2.PortBinding(type=l2.BINDING_CHASSIS, chassis=chassis)
def logical_port_from_neutron_port(port):
return l2.LogicalPort(
id=port['id'],
lswitch=port['network_id'],
topic=port['tenant_id'],
macs=[port['mac_address']],
ips=[ip['ip_address'] for ip in port.get('fixed_ips', [])],
subnets=[ip['subnet_id'] for ip in port.get('fixed_ips', [])],
name=port.get('name', df_const.DF_PORT_DEFAULT_NAME),
enabled=port.get('admin_state_up', False),
version=port['revision_number'],
device_owner=port.get('device_owner'),
device_id=port.get('device_id'),
security_groups=port.get('security_groups', []),
port_security_enabled=port.get(psec.PORTSECURITY, False),
allowed_address_pairs=_validate_ip_prefix_allowed_address_pairs(
port.get(addr_pair.ADDRESS_PAIRS, [])),
binding_vnic_type=port.get(portbindings.VNIC_TYPE),
qos_policy=port.get('qos_policy_id'),
dhcp_params=_build_dhcp_params(port))
id=port['id'],
lswitch=port['network_id'],
topic=port['tenant_id'],
macs=[port['mac_address']],
ips=[ip['ip_address'] for ip in port.get('fixed_ips', [])],
subnets=[ip['subnet_id'] for ip in port.get('fixed_ips', [])],
name=port.get('name', df_const.DF_PORT_DEFAULT_NAME),
enabled=port.get('admin_state_up', False),
version=port['revision_number'],
device_owner=port.get('device_owner'),
device_id=port.get('device_id'),
security_groups=port.get('security_groups', []),
port_security_enabled=port.get(psec.PORTSECURITY, False),
allowed_address_pairs=_validate_ip_prefix_allowed_address_pairs(
port.get(addr_pair.ADDRESS_PAIRS, [])),
binding_vnic_type=port.get(portbindings.VNIC_TYPE),
qos_policy=port.get('qos_policy_id'),
dhcp_params=_build_dhcp_params(port),
binding=_build_port_binding(port),
)

View File

@ -450,32 +450,11 @@ class DFMechDriver(api.MechanismDriver):
LOG.info("DFMechDriver: delete subnet %s", subnet_id)
def _get_chassis_and_remote_vtep(self, port):
# Router GW ports are not needed by dragonflow controller and
# they currently cause error as they couldn't be mapped to
# a valid ofport (or location)
if port.get('device_owner') == n_const.DEVICE_OWNER_ROUTER_GW:
chassis = None
else:
chassis = port.get('binding:host_id') or None
binding_profile = port.get('binding:profile')
remote_vtep = False
if binding_profile and binding_profile.get(
df_const.DF_BINDING_PROFILE_PORT_KEY) ==\
df_const.DF_REMOTE_PORT_TYPE:
chassis = binding_profile.get(df_const.DF_BINDING_PROFILE_HOST_IP)
remote_vtep = True
return chassis, remote_vtep
@lock_db.wrap_db_lock(lock_db.RESOURCE_ML2_NETWORK_OR_PORT)
def create_port_postcommit(self, context):
port = context.current
chassis, remote_vtep = self._get_chassis_and_remote_vtep(port)
lport = neutron_l2.logical_port_from_neutron_port(port)
lport.chassis = chassis
lport.remote_vtep = remote_vtep
self.nb_api.create(lport)
LOG.info("DFMechDriver: create port %s", port['id'])
@ -528,11 +507,7 @@ class DFMechDriver(api.MechanismDriver):
updated_port)
return None
chassis, remote_vtep = self._get_chassis_and_remote_vtep(updated_port)
lport = neutron_l2.logical_port_from_neutron_port(updated_port)
lport.chassis = chassis
lport.remote_vtep = remote_vtep
self.nb_api.update(lport)
LOG.info("DFMechDriver: update port %s", updated_port['id'])

View File

@ -120,13 +120,16 @@ class DFBgpPlugin(service_base.ServicePluginBase,
"""Get the accessible external ip of chassis where lport resides in"""
lport = self.nb_api.get(l2.LogicalPort(id=lport_id, topic=topic))
binding_host = lport.chassis
if not binding_host:
binding = lport.binding
if not binding:
LOG.warning(
'Logical port %s has not been bound to any host yet', lport_id)
return
return self._get_external_ip_by_host(binding_host.id)
if binding.type == l2.BINDING_VTEP:
return binding.vtep_address
elif binding.type == l2.BINDING_CHASSIS:
return self._get_external_ip_by_host(binding.chassis.id)
def _get_external_ip_by_host(self, host):
chassis = self.nb_api.get(core.Chassis(id=host))

View File

@ -104,7 +104,7 @@ class DragonflowDriver(base.DriverBase, mixins.LazyNbApiMixin):
)
self.nb_api.create(model)
self.nb_api.update(l2.LogicalPort(id=subport.port_id,
chassis=df_parent.chassis))
binding=df_parent.binding))
def _delete_subports_handler(self, *args, **kwargs):
"""Handle the event that subports were deleted"""
@ -134,4 +134,4 @@ class DragonflowDriver(base.DriverBase, mixins.LazyNbApiMixin):
)
self.nb_api.delete(model)
self.nb_api.update(l2.LogicalPort(id=subport.port_id,
chassis=None))
binding=None))

View File

@ -245,11 +245,19 @@ def with_nb_objects(*objs):
res = [o for o in res if o.topic == topic]
return res
def _get(obj):
objs = _get_all(type(obj))
for o in objs:
if obj.id == o.id:
return o
def decorator(func):
@functools.wraps(func)
def wrapper(obj, *args, **kwargs):
with mock.patch.object(
obj.nb_api, 'get_all', side_effect=_get_all
), mock.patch.object(
obj.nb_api, 'get', side_effect=_get,
):
return func(obj, *args, **kwargs)
return wrapper

View File

@ -33,11 +33,15 @@ from dragonflow.db.models import secgroups
from dragonflow.tests import base as tests_base
_DEFAULT = object()
class DFAppTestBase(tests_base.BaseTestCase):
apps_list = []
def setUp(self, enable_selective_topo_dist=False):
cfg.CONF.set_override('apps_list', self.apps_list, group='df')
cfg.CONF.set_override('host', fake_chassis1.id)
super(DFAppTestBase, self).setUp()
mock.patch('ryu.base.app_manager.AppManager.get_instance').start()
mock.patch('dragonflow.db.api_nb.NbApi.get_instance').start()
@ -52,8 +56,8 @@ class DFAppTestBase(tests_base.BaseTestCase):
db_store._instance = None
self.nb_api = api_nb.NbApi.get_instance(False)
self.controller = df_local_controller.DfLocalController('fake_host',
self.nb_api)
self.controller = df_local_controller.DfLocalController(
fake_chassis1.id, self.nb_api)
self.vswitch_api = self.controller.vswitch_api = mock.MagicMock()
kwargs = dict(
nb_api=self.controller.nb_api,
@ -141,6 +145,27 @@ fake_external_switch1 = l2.LogicalSwitch(
id='fake_external_switch1')
fake_chassis1 = core.Chassis(
id='fakehost',
ip='172.24.4.50',
tunnel_types=('vxlan',),
)
fake_chassis2 = core.Chassis(
id='fake_host2',
ip='172.24.4.51',
tunnel_types=('vxlan',),
)
def chassis_binding(chassis):
return l2.PortBinding(
type=l2.BINDING_CHASSIS,
chassis=chassis,
)
def make_fake_port(id=None,
subnets=None,
is_local=None,
@ -151,16 +176,20 @@ def make_fake_port(id=None,
enabled=True,
topic='fake_tenant1',
device_owner='compute:None',
chassis='fake_host',
binding=_DEFAULT,
version=2,
unique_key=2,
port_security_enabled=True,
allowed_address_pairs=None,
binding_vnic_type='normal',
security_groups=['fake_security_group_id1'],
device_id='fake_device_id',
ofport=1,
dhcp_params=None):
if binding == _DEFAULT:
binding = chassis_binding(fake_chassis1.id)
fake_port = l2.LogicalPort(
id="%s_%s" % (name, ofport) if not id else id,
topic=topic,
@ -170,10 +199,10 @@ def make_fake_port(id=None,
ips=ips,
subnets=subnets,
macs=macs,
chassis=chassis,
binding=binding,
lswitch=lswitch,
security_groups=security_groups,
allowed_address_pairs=[],
allowed_address_pairs=allowed_address_pairs or [],
port_security_enabled=port_security_enabled,
device_owner=device_owner,
device_id=device_id,
@ -250,26 +279,12 @@ fake_remote_port1 = make_fake_remote_port(
macs=['fa:16:3e:8c:2e:af'],
name='fake_remote_port',
ips=['10.0.0.8'],
chassis='fake_host2',
binding=chassis_binding('fake_host2'),
unique_key=5,
ofport=1,
subnets=['fake_subnet1'])
fake_chassis1 = core.Chassis(
id='fake_host',
ip='172.24.4.50',
tunnel_types=('vxlan',),
)
fake_chassis2 = core.Chassis(
id='fake_host2',
ip='172.24.4.51',
tunnel_types=('vxlan',),
)
fake_floatingip1 = l3.FloatingIp(
id='fake_floatingip_id1',
topic='fake_tenant1',

View File

@ -20,6 +20,7 @@ from dragonflow.db import model_framework
from dragonflow.db import model_proxy
from dragonflow.db.models import core
from dragonflow.db.models import mixins
from dragonflow.tests.common import utils
from dragonflow.tests.unit import test_app_base
@ -63,6 +64,7 @@ class DfLocalControllerTestCase(test_app_base.DFAppTestBase):
mock_delete_lport.assert_called_once_with(lport)
mock_db_store_delete.assert_called_once_with(chassis)
@utils.with_nb_objects(test_app_base.fake_chassis1)
def test_register_chassis(self):
cfg.CONF.set_override('external_host_ip',
'172.24.4.100',

View File

@ -398,9 +398,9 @@ class TestDFMechDriver(DFMechanismDriverTestCase):
self.nb_api.create.assert_called_once()
lport = self.nb_api.create.call_args_list[0][0][0]
self.assertIsInstance(lport, l2.LogicalPort)
self.assertTrue(lport.remote_vtep)
self.assertEqual(l2.BINDING_VTEP, lport.binding.type)
# lport.chassis is a proxy, and we don't have a real database
self.assertEqual("20.0.0.2", lport.chassis.id)
self.assertEqual("20.0.0.2", str(lport.binding.ip))
self.nb_api.update.reset_mock()
profile['host_ip'] = "20.0.0.20"
@ -410,8 +410,8 @@ class TestDFMechDriver(DFMechanismDriverTestCase):
self.nb_api.update.assert_called_once()
lport = self.nb_api.update.call_args_list[0][0][0]
self.assertIsInstance(lport, l2.LogicalPort)
self.assertTrue(lport.remote_vtep)
self.assertEqual("20.0.0.20", lport.chassis.id)
self.assertEqual(l2.BINDING_VTEP, lport.binding.type)
self.assertEqual("20.0.0.20", str(lport.binding.ip))
def test_delete_port(self):
with self.port() as p:

View File

@ -54,7 +54,7 @@ class TestSGApp(test_app_base.DFAppTestBase):
return n_const.IPv4
def _get_another_local_lport(self):
fake_local_port = l2.LogicalPort(
fake_local_port = test_app_base.make_fake_local_port(
id='fake_port2',
topic='fake_tenant1',
name='',
@ -64,7 +64,6 @@ class TestSGApp(test_app_base.DFAppTestBase):
netaddr.IPAddress('2222:2222::2')],
subnets=['fake_subnet1'],
macs=[netaddr.EUI('fa:16:3e:8c:2e:12')],
chassis='fake_host',
lswitch='fake_switch1',
security_groups=['fake_security_group_id1'],
allowed_address_pairs=[],
@ -76,8 +75,6 @@ class TestSGApp(test_app_base.DFAppTestBase):
# 'binding_profile': {},
# 'binding_vnic_type': 'normal',
)
fake_local_port.is_local = True
fake_local_port.ofport = 20
return fake_local_port
def _get_another_security_group(self, is_ipv6=False):

View File

@ -19,6 +19,7 @@ import mock
from oslo_config import cfg
from dragonflow.controller import topology
from dragonflow.tests.common import utils
from dragonflow.tests.unit import test_app_base
@ -64,15 +65,12 @@ class TestTopology(test_app_base.DFAppTestBase):
test_app_base.fake_ovs_port1)
self.controller._register_models()
@utils.with_nb_objects(
test_app_base.fake_chassis1,
test_app_base.fake_local_port1,
test_app_base.fake_logic_switch1,
)
def test_vm_port_online_offline(self):
self.nb_api.get_all.side_effect = nb_api_get_all_func(
test_app_base.fake_logic_switch1,
test_app_base.fake_local_port1,
test_app_base.fake_chassis1,
)
self.nb_api.get.return_value = (
test_app_base.fake_local_port1)
original_update = self.controller.update
self.controller.update = mock.Mock()
self.controller.update.side_effect = original_update
@ -116,25 +114,22 @@ class TestTopology(test_app_base.DFAppTestBase):
def test_vm_online_after_topology_pulled(self):
self.nb_api.get_all.side_effect = nb_api_get_all_func(
test_app_base.fake_logic_switch1,
test_app_base.fake_local_port1)
def _get_logical_port(lport):
lport_id = lport.id
if lport_id == test_app_base.fake_local_port1.id:
return test_app_base.fake_local_port1
if lport_id == test_app_base.fake_local_port2.id:
return test_app_base.fake_local_port2
self.nb_api.get.side_effect = _get_logical_port
test_app_base.fake_local_port1,
test_app_base.fake_chassis1,
)
self.nb_api.get.return_value = test_app_base.fake_local_port1
# Pull topology by first ovs port online
self.topology.ovs_port_updated(test_app_base.fake_ovs_port1)
# Another port online
self.nb_api.get_all.side_effect = nb_api_get_all_func(
test_app_base.fake_logic_switch1,
test_app_base.fake_local_port1,
test_app_base.fake_local_port2)
test_app_base.fake_local_port2,
test_app_base.fake_chassis1,
)
self.controller.update = mock.Mock()
self.nb_api.get.return_value = test_app_base.fake_local_port2
self.topology.ovs_port_updated(test_app_base.fake_ovs_port2)
self.controller.update.assert_called_once_with(
test_app_base.fake_local_port2)
@ -171,7 +166,6 @@ class TestTopology(test_app_base.DFAppTestBase):
mock.call(test_app_base.fake_local_port2)]
self.controller.update.assert_has_calls(
calls, any_order=True)
print(self.controller.update.call_args_list)
self.assertEqual(4, self.controller.update.call_count)
self.nb_api.subscriber.register_topic.assert_called_once()

View File

@ -71,9 +71,9 @@ class TestTunnelingApp(test_app_base.DFAppTestBase):
def test_multicast_flow_for_remote_port(self):
fake_remote_gre_port1 = make_fake_remote_port(
lswitch='fake_gre_switch1',
chassis='fake_host2',
binding=test_app_base.chassis_binding('fake_host2'),
name='fake_remote_gre_port1')
remote_ip = fake_remote_gre_port1.peer_vtep_address
remote_ip = fake_remote_gre_port1.binding.ip
ofport = fake_remote_gre_port1.ofport
match = self.app._make_bum_match(metadata=21)
actions = [
@ -98,7 +98,7 @@ class TestTunnelingApp(test_app_base.DFAppTestBase):
fake_remote_gre_port2 = make_fake_remote_port(
lswitch='fake_gre_switch1',
chassis='fake_host2',
binding=test_app_base.chassis_binding('fake_host2'),
name='fake_remote_gre_port2')
self.controller.update(fake_remote_gre_port2)
self.app.parser.OFPInstructionActions.assert_called_with(