# Copyright (c) 2017 Red Hat Inc. # # 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. import collections import logging import six from ovsdbapp import api from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.backend.ovs_idl import rowview LOG = logging.getLogger(__name__) class BaseCommand(api.Command): READ_ONLY = False def __init__(self, api): self.api = api self.result = None def execute(self, check_error=False, log_errors=True, **kwargs): try: if self.READ_ONLY: self.run_idl(None) return self.result with self.api.transaction(check_error, log_errors, **kwargs) as t: t.add(self) return self.result except Exception: if log_errors: LOG.exception("Error executing command") if check_error: raise @classmethod def set_column(cls, row, col, val): setattr(row, col, idlutils.db_replace_record(val)) @classmethod def set_columns(cls, row, **columns): for col, val in columns.items(): cls.set_column(row, col, val) def post_commit(self, txn): pass def __str__(self): command_info = self.__dict__ return "%s(%s)" % ( self.__class__.__name__, ", ".join("%s=%s" % (k, v) for k, v in command_info.items() if k not in ['api', 'result'])) class ReadOnlyCommand(BaseCommand): READ_ONLY = True class AddCommand(BaseCommand): table_name = [] # unhashable, won't be looked up def post_commit(self, txn): # If get_insert_uuid fails, self.result was not a result of a # recent insert. Most likely we are post_commit after a lookup() real_uuid = txn.get_insert_uuid(self.result) or self.result row = self.api.tables[self.table_name].rows[real_uuid] self.result = rowview.RowView(row) class DbCreateCommand(BaseCommand): def __init__(self, api, table, **columns): super(DbCreateCommand, self).__init__(api) self.table = table self.columns = columns def run_idl(self, txn): row = txn.insert(self.api._tables[self.table]) self.set_columns(row, **self.columns) # This is a temporary row to be used within the transaction self.result = row def post_commit(self, txn): # Replace the temporary row with the post-commit UUID to match vsctl self.result = txn.get_insert_uuid(self.result.uuid) class DbDestroyCommand(BaseCommand): def __init__(self, api, table, record): super(DbDestroyCommand, self).__init__(api) self.table = table self.record = record def run_idl(self, txn): record = self.api.lookup(self.table, self.record) record.delete() class DbSetCommand(BaseCommand): def __init__(self, api, table, record, *col_values): super(DbSetCommand, self).__init__(api) self.table = table self.record = record self.col_values = col_values def run_idl(self, txn): record = self.api.lookup(self.table, self.record) for col, val in self.col_values: # TODO(twilson) Ugh, the OVS library doesn't like OrderedDict # We're only using it to make a unit test work, so we should fix # this soon. if isinstance(val, collections.OrderedDict): val = dict(val) if isinstance(val, dict): # NOTE(twilson) OVS 2.6's Python IDL has mutate methods that # would make this cleaner, but it's too early to rely on them. existing = getattr(record, col, {}) existing.update(val) val = existing self.set_column(record, col, val) class DbAddCommand(BaseCommand): def __init__(self, api, table, record, column, *values): super(DbAddCommand, self).__init__(api) self.table = table self.record = record self.column = column self.values = values def run_idl(self, txn): record = self.api.lookup(self.table, self.record) for value in self.values: if isinstance(value, collections.Mapping): # We should be doing an add on a 'map' column. If the key is # already set, do nothing, otherwise set the key to the value # Since this operation depends on the previous value, verify() # must be called. field = getattr(record, self.column, {}) for k, v in six.iteritems(value): if k in field: continue field[k] = v else: # We should be appending to a 'set' column. try: record.addvalue(self.column, idlutils.db_replace_record(value)) continue except AttributeError: # OVS < 2.6 field = getattr(record, self.column, []) field.append(value) record.verify(self.column) self.set_column(record, self.column, field) class DbClearCommand(BaseCommand): def __init__(self, api, table, record, column): super(DbClearCommand, self).__init__(api) self.table = table self.record = record self.column = column def run_idl(self, txn): record = self.api.lookup(self.table, self.record) # Create an empty value of the column type value = type(getattr(record, self.column))() setattr(record, self.column, value) class DbGetCommand(ReadOnlyCommand): def __init__(self, api, table, record, column): super(DbGetCommand, self).__init__(api) self.table = table self.record = record self.column = column def run_idl(self, txn): record = self.api.lookup(self.table, self.record) # TODO(twilson) This feels wrong, but ovs-vsctl returns single results # on set types without the list. The IDL is returning them as lists, # even if the set has the maximum number of items set to 1. Might be # able to inspect the Schema and just do this conversion for that case. result = idlutils.get_column_value(record, self.column) if isinstance(result, list) and len(result) == 1: self.result = result[0] else: self.result = result class DbListCommand(ReadOnlyCommand): def __init__(self, api, table, records, columns, if_exists, row=False): super(DbListCommand, self).__init__(api) self.table = table self.columns = columns self.if_exists = if_exists self.records = records self.row = row def run_idl(self, txn): table_schema = self.api._tables[self.table] idx = idlutils.get_index_column(table_schema) columns = self.columns or list(table_schema.columns.keys()) + ['_uuid'] # If there's an index for this table, we'll fetch all columns and # remove the unwanted ones based on self.records. Otherwise, let's try # to get the uuid of the wanted ones which is an O(n^2) operation. if not idx and self.records: rows = [] for record in self.records: try: rows.append(self.api.idl.lookup(self.table, record)) except idlutils.RowNotFound: if self.if_exists: continue raise else: rows = table_schema.rows.values() def _match(row): elem = getattr(row, idx) found = elem in self.records if found: records_found.remove(elem) return found records_found = [] if idx and self.records: if self.if_exists: match = lambda row: getattr(row, idx) in self.records else: # If we're using the approach of removing the unwanted # elements, we'll use a helper list to remove elements as we # find them in the DB contents. This will help us identify # quickly if there's some record missing to raise a RowNotFound # exception later. records_found = list(self.records) match = _match else: match = lambda row: True self.result = [ rowview.RowView(row) if self.row else { c: idlutils.get_column_value(row, c) for c in columns } for row in rows if match(row) ] if records_found: raise idlutils.RowNotFound(table=self.table, col=idx, match=records_found[0]) class DbFindCommand(ReadOnlyCommand): def __init__(self, api, table, *conditions, **kwargs): super(DbFindCommand, self).__init__(api) self.table = self.api._tables[table] self.conditions = conditions self.row = kwargs.get('row', False) self.columns = (kwargs.get('columns') or list(self.table.columns.keys()) + ['_uuid']) def run_idl(self, txn): self.result = [ rowview.RowView(r) if self.row else { c: idlutils.get_column_value(r, c) for c in self.columns } for r in self.table.rows.values() if idlutils.row_match(r, self.conditions) ] class BaseGetRowCommand(ReadOnlyCommand): def __init__(self, api, record): super(BaseGetRowCommand, self).__init__(api) self.record = record def run_idl(self, txn): self.result = self.api.lookup(self.table, self.record) class DbRemoveCommand(BaseCommand): def __init__(self, api, table, record, column, *values, **keyvalues): super(DbRemoveCommand, self).__init__(api) self.table = table self.record = record self.column = column self.values = values self.keyvalues = keyvalues self.if_exists = keyvalues.pop('if_exists', False) def run_idl(self, txn): try: record = self.api.lookup(self.table, self.record) if isinstance(getattr(record, self.column), dict): for value in self.values: record.delkey(self.column, value) for key, value in self.keyvalues.items(): record.delkey(self.column, key, value) elif isinstance(getattr(record, self.column), list): for value in self.values: record.delvalue(self.column, value) else: value = type(getattr(record, self.column))() setattr(record, self.column, value) except idlutils.RowNotFound: if self.if_exists: return else: raise