Merge "Add indexed column support to ovsdbapp"
This commit is contained in:
commit
7736bac7dc
|
@ -24,33 +24,97 @@ _NO_DEFAULT = object()
|
||||||
|
|
||||||
class Backend(object):
|
class Backend(object):
|
||||||
lookup_table = {}
|
lookup_table = {}
|
||||||
ovsdb_connection = None
|
_ovsdb_connection = None
|
||||||
|
|
||||||
def __init__(self, connection, start=True, **kwargs):
|
def __init__(self, connection, start=True, auto_index=True, **kwargs):
|
||||||
super(Backend, self).__init__(**kwargs)
|
super(Backend, self).__init__(**kwargs)
|
||||||
|
self.ovsdb_connection = connection
|
||||||
|
if auto_index:
|
||||||
|
self.autocreate_indices()
|
||||||
if start:
|
if start:
|
||||||
self.start_connection(connection)
|
self.start_connection(connection)
|
||||||
|
|
||||||
@classmethod
|
@property
|
||||||
def start_connection(cls, connection):
|
def ovsdb_connection(self):
|
||||||
|
return self.__class__._ovsdb_connection
|
||||||
|
|
||||||
|
@ovsdb_connection.setter
|
||||||
|
def ovsdb_connection(self, connection):
|
||||||
|
if self.__class__._ovsdb_connection is None:
|
||||||
|
self.__class__._ovsdb_connection = connection
|
||||||
|
|
||||||
|
def create_index(self, table, *columns):
|
||||||
|
"""Create a multi-column index on a table
|
||||||
|
|
||||||
|
:param table: The table on which to create an index
|
||||||
|
:type table: string
|
||||||
|
:param columns: The columns in the index
|
||||||
|
:type columns: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
index_name = idlutils.index_name(*columns)
|
||||||
|
idx = self.tables[table].rows.index_create(index_name)
|
||||||
|
idx.add_columns(*columns)
|
||||||
|
|
||||||
|
def autocreate_indices(self):
|
||||||
|
"""Create simple one-column indexes
|
||||||
|
|
||||||
|
This creates indexes for all lookup_table entries and for all defined
|
||||||
|
indexes columns in the OVSDB schema, as long as they are simple
|
||||||
|
one-column indexes (e.g. 'name') fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tables = set(self.idl.tables.keys())
|
||||||
|
# lookup table indices
|
||||||
|
for table, (lt, col, uuid_col) in self.lookup_table.items():
|
||||||
|
if table != lt or not col or uuid_col or table not in tables:
|
||||||
|
# Just handle simple cases where we are looking up a single
|
||||||
|
# column on a single table
|
||||||
|
continue
|
||||||
|
index_name = idlutils.index_name(col)
|
||||||
|
try:
|
||||||
|
idx = self.idl.tables[table].rows.index_create(index_name)
|
||||||
|
except ValueError:
|
||||||
|
# index already exists
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
idx.add_column(col)
|
||||||
|
LOG.debug("Created index %s", index_name)
|
||||||
|
tables.remove(table)
|
||||||
|
|
||||||
|
# Simple ovsdb-schema indices
|
||||||
|
for table in self.idl.tables.values():
|
||||||
|
if table.name not in tables:
|
||||||
|
continue
|
||||||
|
col = idlutils.get_index_column(table)
|
||||||
|
if not col:
|
||||||
|
continue
|
||||||
|
index_name = idlutils.index_name(col)
|
||||||
|
try:
|
||||||
|
idx = table.rows.index_create(index_name)
|
||||||
|
except ValueError:
|
||||||
|
pass # index already exists
|
||||||
|
else:
|
||||||
|
idx.add_column(col)
|
||||||
|
LOG.debug("Created index %s", index_name)
|
||||||
|
tables.remove(table.name)
|
||||||
|
|
||||||
|
def start_connection(self, connection):
|
||||||
try:
|
try:
|
||||||
if cls.ovsdb_connection is None:
|
self.ovsdb_connection.start()
|
||||||
cls.ovsdb_connection = connection
|
|
||||||
cls.ovsdb_connection.start()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
connection_exception = exceptions.OvsdbConnectionUnavailable(
|
connection_exception = exceptions.OvsdbConnectionUnavailable(
|
||||||
db_schema=cls.schema, error=e)
|
db_schema=self.schema, error=e)
|
||||||
LOG.exception(connection_exception)
|
LOG.exception(connection_exception)
|
||||||
raise connection_exception
|
raise connection_exception
|
||||||
|
|
||||||
@classmethod
|
def restart_connection(self):
|
||||||
def restart_connection(cls):
|
self.ovsdb_connection.stop()
|
||||||
cls.ovsdb_connection.stop()
|
self.ovsdb_connection.start()
|
||||||
cls.ovsdb_connection.start()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def idl(self):
|
def idl(self):
|
||||||
return self.__class__.ovsdb_connection.idl
|
return self.ovsdb_connection.idl
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tables(self):
|
def tables(self):
|
||||||
|
@ -60,8 +124,8 @@ class Backend(object):
|
||||||
|
|
||||||
def create_transaction(self, check_error=False, log_errors=True, **kwargs):
|
def create_transaction(self, check_error=False, log_errors=True, **kwargs):
|
||||||
return transaction.Transaction(
|
return transaction.Transaction(
|
||||||
self, self.__class__.ovsdb_connection,
|
self, self.ovsdb_connection,
|
||||||
self.__class__.ovsdb_connection.timeout,
|
self.ovsdb_connection.timeout,
|
||||||
check_error, log_errors)
|
check_error, log_errors)
|
||||||
|
|
||||||
def db_create(self, table, **col_values):
|
def db_create(self, table, **col_values):
|
||||||
|
@ -119,15 +183,18 @@ class Backend(object):
|
||||||
return record.result
|
return record.result
|
||||||
|
|
||||||
t = self.tables[table]
|
t = self.tables[table]
|
||||||
try:
|
if isinstance(record, uuid.UUID):
|
||||||
if isinstance(record, uuid.UUID):
|
|
||||||
return t.rows[record]
|
|
||||||
try:
|
try:
|
||||||
uuid_ = uuid.UUID(record)
|
return t.rows[record]
|
||||||
return t.rows[uuid_]
|
except KeyError:
|
||||||
except ValueError:
|
raise idlutils.RowNotFound(table=table, col='uuid',
|
||||||
# Not a UUID string, continue lookup by other means
|
match=record)
|
||||||
pass
|
try:
|
||||||
|
uuid_ = uuid.UUID(record)
|
||||||
|
return t.rows[uuid_]
|
||||||
|
except ValueError:
|
||||||
|
# Not a UUID string, continue lookup by other means
|
||||||
|
pass
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# If record isn't found by UUID , go ahead and look up by the table
|
# If record isn't found by UUID , go ahead and look up by the table
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -54,12 +54,42 @@ class RowNotFound(exceptions.OvsdbAppException):
|
||||||
message = "Cannot find %(table)s with %(col)s=%(match)s"
|
message = "Cannot find %(table)s with %(col)s=%(match)s"
|
||||||
|
|
||||||
|
|
||||||
|
def index_name(*columns):
|
||||||
|
assert columns
|
||||||
|
return "_".join(sorted(columns))
|
||||||
|
|
||||||
|
|
||||||
|
def index_lookup(table, **matches):
|
||||||
|
"""Find a value in Table by index
|
||||||
|
|
||||||
|
:param table: The table to search in
|
||||||
|
:type table: ovs.db.schema.TableSchema
|
||||||
|
:param matches: The column/value pairs of the index to search
|
||||||
|
:type matches: The types of the columns being matched
|
||||||
|
:returns: A Row object
|
||||||
|
"""
|
||||||
|
idx = table.rows.indexes[index_name(*matches.keys())]
|
||||||
|
search = table.rows.IndexEntry(**matches)
|
||||||
|
return next(idx.irange(search, search))
|
||||||
|
|
||||||
|
|
||||||
|
def table_lookup(table, column, match):
|
||||||
|
return next(r for r in table.rows.values() if getattr(r, column) == match)
|
||||||
|
|
||||||
|
|
||||||
def row_by_value(idl_, table, column, match, default=_NO_DEFAULT):
|
def row_by_value(idl_, table, column, match, default=_NO_DEFAULT):
|
||||||
"""Lookup an IDL row in a table by column/value"""
|
"""Lookup an IDL row in a table by column/value"""
|
||||||
tab = idl_.tables[table]
|
tab = idl_.tables[table]
|
||||||
for r in tab.rows.values():
|
try:
|
||||||
if getattr(r, column) == match:
|
return index_lookup(tab, **{column: match})
|
||||||
return r
|
except KeyError: # no index column
|
||||||
|
try:
|
||||||
|
return table_lookup(tab, column, match)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
except StopIteration: # match not found via index
|
||||||
|
pass
|
||||||
|
|
||||||
if default is not _NO_DEFAULT:
|
if default is not _NO_DEFAULT:
|
||||||
return default
|
return default
|
||||||
raise RowNotFound(table=table, col=column, match=match)
|
raise RowNotFound(table=table, col=column, match=match)
|
||||||
|
|
|
@ -22,9 +22,6 @@ class OvnSbApiIdlImpl(ovs_idl.Backend, api.API):
|
||||||
'Chassis': idlutils.RowLookup('Chassis', 'name', None),
|
'Chassis': idlutils.RowLookup('Chassis', 'name', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, connection):
|
|
||||||
super(OvnSbApiIdlImpl, self).__init__(connection)
|
|
||||||
|
|
||||||
def chassis_add(self, chassis, encap_types, encap_ip, may_exist=False,
|
def chassis_add(self, chassis, encap_types, encap_ip, may_exist=False,
|
||||||
**columns):
|
**columns):
|
||||||
return cmd.ChassisAddCommand(self, chassis, encap_types, encap_ip,
|
return cmd.ChassisAddCommand(self, chassis, encap_types, encap_ip,
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from ovsdbapp.backend.ovs_idl import idlutils
|
||||||
|
from ovsdbapp.schema.ovn_northbound import impl_idl
|
||||||
|
from ovsdbapp.tests.functional import base
|
||||||
|
from ovsdbapp.tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestOvnNbIndex(base.FunctionalTestCase):
|
||||||
|
schemas = ['OVN_Northbound']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestOvnNbIndex, self).setUp()
|
||||||
|
self.api = impl_idl.OvnNbApiIdlImpl(self.connection)
|
||||||
|
|
||||||
|
def test_find(self):
|
||||||
|
# This test will easily time out if indexing isn't used
|
||||||
|
length = 2000
|
||||||
|
basename = utils.get_rand_device_name('testswitch')
|
||||||
|
with self.api.transaction(check_error=True) as txn:
|
||||||
|
for i in range(length):
|
||||||
|
txn.add(self.api.ls_add("%s%d" % (basename, i)))
|
||||||
|
match = "%s%d" % (basename, length / 2)
|
||||||
|
sw = self.api.lookup('Logical_Switch', match)
|
||||||
|
self.assertEqual(sw.name, match)
|
||||||
|
|
||||||
|
def test_default_indices(self):
|
||||||
|
self.assertTrue(self.api.lookup_table)
|
||||||
|
for key, (table, col, _) in self.api.lookup_table.items():
|
||||||
|
idl_table = self.api.tables[table]
|
||||||
|
self.assertIn(col, idl_table.rows.indexes)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOvnNbWithoutIndex(base.FunctionalTestCase):
|
||||||
|
schemas = ['OVN_Northbound']
|
||||||
|
|
||||||
|
# Due to ovsdbapp by default creating singleton connections, it's possible
|
||||||
|
# that a test is run where we already have a connection/idl set up that
|
||||||
|
# already has indexing set up on it.
|
||||||
|
class NewNbApiIdlImpl(impl_idl.OvnNbApiIdlImpl):
|
||||||
|
@property
|
||||||
|
def ovsdb_connection(self):
|
||||||
|
return self._ovsdb_connection
|
||||||
|
|
||||||
|
@ovsdb_connection.setter
|
||||||
|
def ovsdb_connection(self, connection):
|
||||||
|
self._ovsdb_connection = connection
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestOvnNbWithoutIndex, self).setUp()
|
||||||
|
self.api = self.NewNbApiIdlImpl(self.connection, start=False,
|
||||||
|
auto_index=False)
|
||||||
|
|
||||||
|
@mock.patch.object(idlutils, 'table_lookup')
|
||||||
|
def test_create_index(self, table_lookup):
|
||||||
|
self.assertFalse(self.api.tables['Logical_Switch'].rows.indexes)
|
||||||
|
self.api.create_index("Logical_Switch", "name")
|
||||||
|
self.api.start_connection(self.connection)
|
||||||
|
name = utils.get_rand_device_name("testswitch")
|
||||||
|
self.api.ls_add(name).execute(check_error=True)
|
||||||
|
sw = self.api.lookup('Logical_Switch', name)
|
||||||
|
self.assertEqual(sw.name, name)
|
||||||
|
self.assertRaises(idlutils.RowNotFound, self.api.lookup,
|
||||||
|
'Logical_Switch', 'nothere')
|
||||||
|
table_lookup.assert_not_called()
|
|
@ -169,3 +169,15 @@ class TestIdlUtils(base.TestCase):
|
||||||
mock.sentinel.table_name,
|
mock.sentinel.table_name,
|
||||||
FAKE_RECORD_GUID)
|
FAKE_RECORD_GUID)
|
||||||
self.assertEqual(mock.sentinel.row_value, res)
|
self.assertEqual(mock.sentinel.row_value, res)
|
||||||
|
|
||||||
|
def test_index_name(self):
|
||||||
|
expected = {
|
||||||
|
('one',): 'one',
|
||||||
|
('abc', 'def'): 'abc_def',
|
||||||
|
('def', 'abc'): 'abc_def',
|
||||||
|
('one', 'two', 'three'): 'one_three_two',
|
||||||
|
}
|
||||||
|
for args, result in expected.items():
|
||||||
|
self.assertEqual(result, idlutils.index_name(*args))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, idlutils.index_name)
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from ovsdbapp.backend import ovs_idl
|
from ovsdbapp.backend import ovs_idl
|
||||||
|
@ -23,7 +24,9 @@ class FakeRow(object):
|
||||||
|
|
||||||
|
|
||||||
class FakeTable(object):
|
class FakeTable(object):
|
||||||
rows = {'fake-id-1': FakeRow(uuid='fake-id-1', name='Fake1')}
|
rows = collections.UserDict({'fake-id-1':
|
||||||
|
FakeRow(uuid='fake-id-1', name='Fake1')})
|
||||||
|
rows.indexes = {}
|
||||||
indexes = []
|
indexes = []
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +38,9 @@ class FakeBackend(ovs_idl.Backend):
|
||||||
def start_connection(self, connection):
|
def start_connection(self, connection):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def autocreate_indices(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestBackendOvsIdl(base.TestCase):
|
class TestBackendOvsIdl(base.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -48,5 +48,5 @@ class TestOvsdbIdl(base.TestCase):
|
||||||
|
|
||||||
def test_init_session(self):
|
def test_init_session(self):
|
||||||
conn = mock.MagicMock()
|
conn = mock.MagicMock()
|
||||||
backend = impl_idl.OvsdbIdl(conn, start=False)
|
impl_idl.OvsdbIdl(conn, start=False)
|
||||||
self.assertIsNone(backend.ovsdb_connection)
|
conn.start_connection.assert_not_called()
|
||||||
|
|
Loading…
Reference in New Issue