@@ -7,6 +7,8 @@ Post-v3.3.0
- The primary development branch has been renamed from 'master' to 'main'.
The OVS tree remains hosted on GitHub.
https://github.com/openvswitch/ovs.git
+ - Python:
+ * Add custom transaction support to the Idl via add_op().
v3.3.0 - 16 Feb 2024
@@ -1703,6 +1703,8 @@ class Transaction(object):
self._inserted_rows = {} # Map from UUID to _InsertedRow
+ self._operations = []
+
def add_comment(self, comment):
"""Appends 'comment' to the comments that will be passed to the OVSDB
server when this transaction is committed. (The comment will be
@@ -1838,7 +1840,7 @@ class Transaction(object):
"rows": [rows]})
# Add updates.
- any_updates = False
+ any_updates = bool(self._operations)
for row in self._txn_rows.values():
if row._changes is None:
if row._table.is_root:
@@ -1977,6 +1979,7 @@ class Transaction(object):
if self.dry_run:
operations.append({"op": "abort"})
+ operations += self._operations
if not any_updates:
self._status = Transaction.UNCHANGED
else:
@@ -1991,6 +1994,19 @@ class Transaction(object):
self.__disassemble()
return self._status
+ def add_op(self, op):
+ """Add a raw OVSDB operation to the transaction
+
+ This can be useful for re-using the existing Idl connection to take
+ actions that are difficult or expensive to do with the Idl itself. e.g.
+ deleting a bunch of rows on the server that you don't want to store
+ in memory.
+
+ :param op: An "op" for an OVSDB "transact" request (rfc 7047 Sec 5.2)
+ :type op: dict
+ """
+ self._operations.append(op)
+
def commit_block(self):
"""Attempts to commit this transaction, blocking until the commit
either succeeds or fails. Returns the final commit status, which may
@@ -2758,6 +2758,33 @@ OVSDB_CHECK_IDL_PERS_UUID_INSERT([simple idl, persistent uuid insert],
[['This UUID would duplicate a UUID already present within the table or deleted within the same transaction']])
+OVSDB_CHECK_IDL_PY([simple idl, python, add_op],
+ [],
+ [['insert 1, insert 2, insert 3, insert 1' \
+ 'add_op {"op": "delete", "table": "simple", "where": [["i", "==", 1]]}' \
+ 'add_op {"op": "insert", "table": "simple", "row": {"i": 2}}, delete 3' \
+ 'insert 2, add_op {"op": "update", "table": "simple", "row": {"i": 1}, "where": [["i", "==", 2]]}'
+ ]],
+ [[000: empty
+001: commit, status=success
+002: table simple: i=1 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<1>
+002: table simple: i=1 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<2>
+002: table simple: i=2 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+002: table simple: i=3 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<4>
+003: commit, status=success
+004: table simple: i=2 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+004: table simple: i=3 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<4>
+005: commit, status=success
+006: table simple: i=2 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+006: table simple: i=2 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<5>
+007: commit, status=success
+008: table simple: i=1 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+008: table simple: i=1 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<5>
+008: table simple: i=1 r=0 b=false s= u=<0> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<6>
+009: done
+]],[],sort)
+
+
m4_define([OVSDB_CHECK_IDL_CHANGE_AWARE],
[AT_SETUP([simple idl, database change aware, online conversion - $1])
AT_KEYWORDS([ovsdb server idl db_change_aware conversion $1])
@@ -36,6 +36,53 @@ vlog.set_levels_from_string("console:dbg")
vlog.init(None)
+OBJ_STR = "_OBJECT_{}_"
+
+
+def substitute_object_text(data, quotechar='"', obj_chars=("{}", "[]")):
+ obj_chars = dict(obj_chars)
+ in_quote = False
+ in_object = []
+ removed_text = []
+ output = ""
+ start = end = 0
+ for i, s in enumerate(data):
+ if not in_object:
+ if not in_quote and s in obj_chars:
+ in_object.append(s)
+ start = i
+ else:
+ output += s
+ if s == quotechar:
+ in_quote = not in_quote
+ elif not in_quote: # unquoted object
+ if s == in_object[0]:
+ in_object.append(s)
+ elif s == obj_chars[in_object[0]]:
+ in_object.pop()
+ if not in_object:
+ end = i + 1
+ removed_text.append(data[start:end])
+ output += OBJ_STR.format(len(removed_text) - 1)
+ return output, removed_text
+
+
+def recover_object_text_from_list(words, json):
+ if not json:
+ return words
+ results = []
+ i = 0
+ for word in words:
+ while json:
+ if OBJ_STR.format(i) not in word:
+ results.append(word)
+ break
+ replacement = json.pop(0)
+ results.append(word.replace(OBJ_STR.format(i), replacement))
+ i += 1
+ return results
+
+
def unbox_json(json):
if type(json) is list and len(json) == 1:
return json[0]
@@ -377,8 +424,10 @@ def idl_set(idl, commands, step):
increment = False
fetch_cmds = []
events = []
+ commands, data = substitute_object_text(commands)
for command in commands.split(','):
words = command.split()
+ words = recover_object_text_from_list(words, data)
name = words[0]
args = words[1:]
@@ -437,6 +486,12 @@ def idl_set(idl, commands, step):
s = txn.insert(idl.tables["simple"], new_uuid=args[0],
persist_uuid=True)
s.i = int(args[1])
+ elif name == "add_op":
+ if len(args) != 1:
+ sys.stderr.write('"add_op" command requires 1 argument\n')
+ sys.stderr.write(f"args={args}\n")
+ sys.exit(1)
+ txn.add_op(ovs.json.from_string(args[0]))
elif name == "delete":
if len(args) != 1:
sys.stderr.write('"delete" command requires 1 argument\n')
It can be useful to be able to send raw transaction operations through the Idl's connection. For example, to clean up MAC_Binding entries for floating IPs without having to monitor the MAC_Binding table which can be quite large. Signed-off-by: Terry Wilson <twilson@redhat.com> --- NEWS | 2 ++ python/ovs/db/idl.py | 18 ++++++++++++++- tests/ovsdb-idl.at | 27 ++++++++++++++++++++++ tests/test-ovsdb.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-)