Make ML2 ensure_dvr_port_binding more robust

There is a remote chance that this operation may
be prone to DB integrity errors, in case the binding
is attempted on the same port twice.

Ideally getter methods should not create, but this
is a common Neutron (anti)-pattern that would be
difficult to eradicate (at least in a single patch);
so for now let's make this code more defensive.

Related-bug: #1269131
Related-bug: #1335226

Change-Id: Ie6c57fd46f0752839814dbac5b14fae2364f973d
This commit is contained in:
armando-migliaccio 2014-07-03 17:00:44 -07:00
parent ab1f98bb0d
commit 5cc68dacad
2 changed files with 34 additions and 11 deletions

View File

@ -15,6 +15,8 @@
from sqlalchemy.orm import exc from sqlalchemy.orm import exc
from oslo.db import exception as db_exc
from neutron.common import constants as n_const from neutron.common import constants as n_const
from neutron.db import api as db_api from neutron.db import api as db_api
from neutron.db import models_v2 from neutron.db import models_v2
@ -90,16 +92,13 @@ def get_locked_port_and_binding(session, port_id):
def ensure_dvr_port_binding(session, port_id, host, router_id=None): def ensure_dvr_port_binding(session, port_id, host, router_id=None):
# FIXME(armando-migliaccio): take care of LP #1335226 record = (session.query(models.DVRPortBinding).
# DVR ports are slightly different from the others in filter_by(port_id=port_id, host=host).first())
# that binding happens at a later stage via L3 agent if record:
# therefore we need to keep this logic of creation on return record
# missing binding.
with session.begin(subtransactions=True): try:
try: with session.begin(subtransactions=True):
record = (session.query(models.DVRPortBinding).
filter_by(port_id=port_id, host=host).one())
except exc.NoResultFound:
record = models.DVRPortBinding( record = models.DVRPortBinding(
port_id=port_id, port_id=port_id,
host=host, host=host,
@ -109,7 +108,11 @@ def ensure_dvr_port_binding(session, port_id, host, router_id=None):
cap_port_filter=False, cap_port_filter=False,
status=n_const.PORT_STATUS_DOWN) status=n_const.PORT_STATUS_DOWN)
session.add(record) session.add(record)
return record return record
except db_exc.DBDuplicateEntry:
LOG.debug("DVR Port %s already bound", port_id)
return (session.query(models.DVRPortBinding).
filter_by(port_id=port_id, host=host).one())
def delete_dvr_port_binding(session, port_id, host): def delete_dvr_port_binding(session, port_id, host):

View File

@ -13,6 +13,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock
from sqlalchemy.orm import query
from neutron import context from neutron import context
from neutron.db import api as db_api from neutron.db import api as db_api
from neutron.db import l3_db from neutron.db import l3_db
@ -66,6 +70,22 @@ class Ml2DBTestCase(base.BaseTestCase):
self.ctx.session.add(record) self.ctx.session.add(record)
return record return record
def test_ensure_dvr_port_binding_deals_with_db_duplicate(self):
network_id = 'foo_network_id'
port_id = 'foo_port_id'
router_id = 'foo_router_id'
host_id = 'foo_host_id'
self._setup_neutron_network(network_id, [port_id])
self._setup_dvr_binding(network_id, port_id, router_id, host_id)
with mock.patch.object(query.Query, 'first') as query_first:
query_first.return_value = []
with mock.patch.object(ml2_db.LOG, 'debug') as log_trace:
binding = ml2_db.ensure_dvr_port_binding(
self.ctx.session, port_id, host_id, router_id)
self.assertTrue(query_first.called)
self.assertTrue(log_trace.called)
self.assertEqual(port_id, binding.port_id)
def test_ensure_dvr_port_binding(self): def test_ensure_dvr_port_binding(self):
network_id = 'foo_network_id' network_id = 'foo_network_id'
port_id = 'foo_port_id' port_id = 'foo_port_id'