Disallow duplicated policy name in DB

Bug: DB allows two PE instances to insert two policies with the same
name (different ID), causing irrecoverable error in all following
policy sync attempts.

This patch fixes the bug by locking database table and checking for
duplicates before adding new policy.

Manually tested under SQLite and MySQL.

Change-Id: I0de1692a895fbf47703f668b44e2c9300f518f76
This commit is contained in:
Eric K 2016-09-28 01:07:06 -07:00
parent dcf097b9cd
commit a0f1c1a17d
3 changed files with 39 additions and 7 deletions

View File

@ -102,9 +102,7 @@ class PolicyModel(base.APIModel):
'kind': item.get('kind'),
'desc': item.get('description')})
except exception.CongressException as e:
(num, desc) = error_codes.get('failed_to_create_policy')
raise webservice.DataModelException(
num, desc + ": " + str(e))
raise webservice.DataModelException.create(e)
return (policy_metadata['id'], policy_metadata)

View File

@ -18,6 +18,7 @@ from __future__ import division
from __future__ import absolute_import
from oslo_config import cfg
import sqlalchemy as sa
from sqlalchemy.orm import exc as db_exc
@ -59,10 +60,40 @@ class Policy(model_base.BASE, model_base.HasId, model_base.HasAudit):
def add_policy(id_, name, abbreviation, description, owner, kind,
deleted=False, session=None):
session = session or db.get_session()
with session.begin(subtransactions=True):
policy = Policy(id_, name, abbreviation, description, owner,
kind, deleted)
session.add(policy)
mysql = (cfg.CONF.database.connection is not None and
(cfg.CONF.database.connection.split(':/')[0] == 'mysql' or
cfg.CONF.database.connection.split('+')[0] == 'mysql'))
postgres = (cfg.CONF.database.connection is not None and
(cfg.CONF.database.connection.split(':/')[0] == 'postgresql' or
cfg.CONF.database.connection.split('+')[0] == 'postgresql'))
try:
with session.begin(subtransactions=True):
# lock policies table to prevent duplicate named policy being added
# after duplicate check but before transaction closes.
# supported DBs are SQLite and MySQL and Postgres
# TODO(ekcs): table locking is special to underlying DB
# change DB schema to prevent duplicate generically without locking
if mysql: # Explicitly LOCK TABLES for MySQL
session.execute('LOCK TABLES policies WRITE')
if postgres: # Explicitly LOCK TABLE for Postgres
session.execute('LOCK TABLE policies IN EXCLUSIVE MODE')
# Do nothing for SQLite; DB auto locked for transaction
# add if no policy of duplicate name exists
unique = (session.query(Policy).
filter(Policy.name == name).
filter(Policy.deleted == is_soft_deleted(name, deleted)).
count() == 0)
if unique:
policy = Policy(id_, name, abbreviation, description, owner,
kind, deleted)
session.add(policy)
finally: # always release table lock
if mysql:
session.execute('UNLOCK TABLES')
# postgres automatically releases lock after transaction completes
if not unique:
raise KeyError("Policy with name %s already exists" % name)
return policy

View File

@ -376,6 +376,9 @@ class Runtime (object):
obj['description'],
obj['owner_id'],
obj['kind'])
except KeyError:
raise exception.Conflict(
"Policy with name %s already exists" % name)
except Exception:
policy_name = policy_obj.name
msg = "Error thrown while adding policy %s into DB." % policy_name