Browse Source

Check for missing port bindings each sync period

When two DVR bindings go to ACTIVE simultaneously, one of the
updates will hit a stale data exception when commit to the DB.
This will ultimately result in update_port_postcommit not getting
called when the second binding's status goes to ACTIVE on retry.

To handle this, we now check for missing port bindings once per
sync period.

Change-Id: I1efc20fa4c15c962fb8d5dc8842704b387ddde5b
changes/54/775054/1
Mitchell Jameson 6 months ago
parent
commit
1b36ea4d24
  1. 21
      networking_arista/common/db_lib.py
  2. 25
      networking_arista/ml2/arista_ml2.py
  3. 128
      networking_arista/tests/unit/ml2/test_arista_mechanism_driver.py

21
networking_arista/common/db_lib.py

@ -18,11 +18,13 @@ from neutron import context as nctx
import neutron.db.api as db
from neutron.db import db_base_plugin_v2
from neutron.db.models import segment as segment_models
from neutron.db import models_v2
from neutron.db import securitygroups_db as sec_db
from neutron.db import segments_db
from neutron.plugins.common import constants as p_const
from neutron.plugins.ml2 import driver_api
from neutron.plugins.ml2 import models as ml2_models
from sqlalchemy import and_
from networking_arista.common import db as db_models
@ -469,6 +471,25 @@ def get_network_segments_by_port_id(port_id):
return [segment[0] for segment in segments]
def get_unprovisioned_dvr_port_bindings():
"""Returns DVR port bindings that don't exist in arista_provisioned_vms"""
session = db.get_reader_session()
# Hack for pep8 E711 to allow == none
none = None
with session.begin():
arista_vms_model = db_models.AristaProvisionedVms
distributed_pb_model = ml2_models.DistributedPortBinding
port_bindings = (session.query(distributed_pb_model, models_v2.Port).
outerjoin(arista_vms_model,
and_(arista_vms_model.port_id ==
distributed_pb_model.port_id,
arista_vms_model.host_id ==
distributed_pb_model.host)).
join(models_v2.Port).
filter(arista_vms_model.port_id == none).all())
return port_bindings
class NeutronNets(db_base_plugin_v2.NeutronDbPluginV2,
sec_db.SecurityGroupDbMixin):
"""Access to Neutron DB.

25
networking_arista/ml2/arista_ml2.py

@ -2108,6 +2108,7 @@ class SyncService(object):
self._ndb = neutron_db
self._force_sync = True
self._region_updated_time = None
self._dvr_bindings_missing = False
def force_sync(self):
"""Sets the force_sync flag."""
@ -2131,6 +2132,8 @@ class SyncService(object):
self.force_sync()
return
self._update_dvr_port_bindings()
if not self._sync_required():
return
@ -2151,6 +2154,7 @@ class SyncService(object):
return
self._set_region_updated_time()
self._dvr_bindings_missing = False
def synchronize(self):
"""Sends data to EOS which differs from neutron DB."""
@ -2291,7 +2295,8 @@ class SyncService(object):
# Now update the VMs
for tenant in instances_to_update:
if not instances_to_update[tenant]:
if (not instances_to_update[tenant] and
not self._dvr_bindings_missing):
continue
try:
db_vms = db_lib.get_vms(tenant)
@ -2319,6 +2324,24 @@ class SyncService(object):
else:
return False
def _update_dvr_port_bindings(self):
"""Update arista_provisioned_vms with missing DVR port bindings
Check entries in the ml2_distributed_port_bindings that are missing
from the arista_provisioned_vms table. These may be due to a race that
causes update_port_postcommit not to be called for all. DVR bindings.
If any missing bindings exist, add them to the arista table and
initiate a sync
"""
unprovisioned_port_bindings = (
db_lib.get_unprovisioned_dvr_port_bindings())
if len(unprovisioned_port_bindings):
for pb, port in unprovisioned_port_bindings:
db_lib.remember_vm(pb.router_id, pb.host, pb.port_id,
port.network_id, port.tenant_id)
self._dvr_bindings_missing = True
self.force_sync()
def _sync_required(self):
""""Check whether the sync is required."""
try:

128
networking_arista/tests/unit/ml2/test_arista_mechanism_driver.py

@ -14,6 +14,7 @@
# limitations under the License.
import functools
import netaddr
import operator
import requests
import socket
@ -23,10 +24,15 @@ from mock import patch
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_utils import uuidutils
import six
from neutron import context
import neutron.db.api as db
from neutron.db import models_v2
from neutron.objects import ports as port_obj
from neutron.plugins.ml2 import driver_api as api
from neutron.plugins.ml2 import models as ml2_models
from neutron.tests import base
from neutron.tests.unit import testlib_api
@ -2225,6 +2231,7 @@ class SyncServiceTest(testlib_api.SqlTestCase):
ndb = db_lib.NeutronNets()
self.sync_service = arista_ml2.SyncService(self.rpc, ndb)
self.sync_service._force_sync = False
self.setup_coreplugin('ml2')
def test_region_in_sync(self):
"""Tests whether the region_in_sync() behaves as expected."""
@ -2678,3 +2685,124 @@ class SyncServiceTest(testlib_api.SqlTestCase):
]
self.rpc.assert_has_calls(expected_calls)
def test_synchronize_missing_dvr_bindings(self):
"""Tests whether missing DVR bindings get synced"""
region_updated_time = {
'regionName': 'RegionOne',
'regionTimestamp': '12345'
}
self.rpc.get_region_updated_time.return_value = region_updated_time
self.sync_service._region_updated_time = region_updated_time
tenant_id = uuidutils.generate_uuid()
router_id = uuidutils.generate_uuid()
port_id = uuidutils.generate_uuid()
network_id = uuidutils.generate_uuid()
host1 = 'host1'
host2 = 'host2'
# Add two DVR port bindings to neutron db
ctx = context.get_admin_context()
with ctx.session.begin(subtransactions=True):
ctx.session.add(models_v2.Network(id=network_id))
port_obj.Port(ctx,
id=port_id,
project_id=tenant_id,
network_id=network_id,
mac_address=netaddr.EUI('00-00-00-00-00-00'),
admin_state_up=True,
status='ACTIVE',
device_id='',
device_owner='').create()
with ctx.session.begin(subtransactions=True):
for host in (host1, host2):
pb = ml2_models.DistributedPortBinding(port_id=port_id,
host=host,
router_id=router_id,
vif_type='ovs',
vif_details='',
vnic_type='normal',
profile='',
status='ACTIVE')
ctx.session.add(pb)
# Just add one of them to arista_provisioned_vms
db_lib.remember_tenant(tenant_id)
db_lib.remember_vm(router_id, host1, port_id, network_id, tenant_id)
region_tenants = {
tenant_id: {
'tenantVmInstances': {},
'tenantBaremetalInstances': {},
'tenantRouterInstances': {
router_id: {
'routerPorts': {
port_id: {
'networkId': network_id,
'portId': port_id,
'hosts': [host1],
},
},
},
},
'tenantNetworks': {},
}
}
self.rpc.get_tenants.return_value = region_tenants
self.rpc.sync_start.return_value = True
self.rpc.sync_end.return_value = True
self.rpc.check_cvx_availability.return_value = True
self.rpc._baremetal_supported.return_value = False
self.rpc.get_all_baremetal_hosts.return_value = {}
# Check that sync triggers a send of the new binding
self.sync_service.do_synchronize()
expected_calls = [
mock.call.perform_sync_of_sg(),
mock.call.check_cvx_availability(),
mock.call.sync_start(),
mock.call.register_with_eos(sync=True),
mock.call.check_supported_features(),
mock.call.get_tenants(),
mock.call.create_instance_bulk(
tenant_id,
{port_id: {
'id': port_id,
'device_owner': '',
'name': None,
'network_id': network_id,
'tenant_id': tenant_id,
'device_id': ''}},
{router_id: {'vmId': router_id,
'ports': [{'networkId': network_id,
'portId': port_id,
'deviceId': router_id,
'hosts': [host1, host2]}],
'baremetal_instance': False}},
{},
sync=True),
mock.call.sync_end(),
mock.call.get_region_updated_time()
]
self.assertTrue(self.rpc.mock_calls == expected_calls,
"Seen: %s\nExpected: %s" % (
self.rpc.mock_calls,
expected_calls,
)
)
# Check that sync doesn't create DVR ports again
self.sync_service.do_synchronize()
expected_calls.extend([
mock.call.perform_sync_of_sg(),
mock.call.check_cvx_availability(),
mock.call.get_region_updated_time()
])
self.assertTrue(self.rpc.mock_calls == expected_calls,
"Seen: %s\nExpected: %s" % (
self.rpc.mock_calls,
expected_calls,
)
)
Loading…
Cancel
Save