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 <[email protected]>
---
 NEWS                 |  2 ++
 python/ovs/db/idl.py | 21 ++++++++++++-
 tests/ovsdb-idl.at   | 27 ++++++++++++++++
 tests/test-ovsdb.py  | 73 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 122 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index d05f2d0f8..fca983e5a 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,8 @@ Post-v3.3.0
      * Link status changes are now handled via interrupt mode if the DPDK
        driver supports it.  It is possible to revert to polling mode by setting
        per interface 'options:dpdk-lsc-interrupt' to 'false'.
+   - Python:
+     * Add custom transaction support to the Idl via add_op().
 
 
 v3.3.0 - 16 Feb 2024
diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
index b6d5ed697..c1caab7b2 100644
--- a/python/ovs/db/idl.py
+++ b/python/ovs/db/idl.py
@@ -1708,6 +1708,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
@@ -1843,7 +1845,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:
@@ -1978,6 +1980,8 @@ class Transaction(object):
             operations.append({"op": "comment",
                                "comment": "\n".join(self._comments)})
 
+        operations += self._operations
+
         # Dry run?
         if self.dry_run:
             operations.append({"op": "abort"})
@@ -1996,6 +2000,21 @@ 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.
+        bulk deleting rows from the server without downloading them into a
+        local cache.
+
+        All ops are applied after any other operations in the transaction
+
+        :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
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index b9dc0bdea..9070ea051 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -2863,6 +2863,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])
diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
index 67a45f044..91c5f4b4c 100644
--- a/tests/test-ovsdb.py
+++ b/tests/test-ovsdb.py
@@ -36,6 +36,66 @@ vlog.set_levels_from_string("console:dbg")
 vlog.init(None)
 
 
+def substitute_object_text(data, quotechar='"', obj_chars=("{}", "[]"),
+                           tag_format="_OBJECT_{}_"):
+    """Replace objects in strings with tags that can later be retrieved
+
+    Given data like:
+    'cmd1 1, cmd2 {"a": {"a": "b"}}, cmd3 1 2, cmd4 ["a", "b"]'
+
+    Return an output string:
+    'cmd1 1, cmd2 _OBJECT_0_, cmd3 1 2, cmd4 _OBJECT_1_ data'
+
+    and a dictionary of replaced text:
+    {'_OBJECT_0_': '{"a": {"a": "b"}}', '_OBJECT_1_': '["a", "b"]'}
+    """
+
+    obj_chars = dict(obj_chars)
+    in_quote = False
+    in_object = []  # stack of nested outer object opening characters
+    replaced_text = {}
+    output = ""
+    start = end = 0
+    for i, c in enumerate(data):
+        if not in_object:
+            if not in_quote and c in obj_chars:
+                # This is the start of a non-quoted outer object that will
+                # be replaced by a tag
+                in_object.append(c)
+                start = i
+            else:
+                # Regular output
+                output += c
+            if c == quotechar:
+                in_quote = not in_quote
+        elif not in_quote:  # unquoted object
+            if c == in_object[0]:
+                # Record on the stack that we are in a nested object of the
+                # same type as the outer object, this object will not be
+                # substituted with a tag
+                in_object.append(c)
+            elif c == obj_chars[in_object[0]]:
+                # This is the closing character to this potentially nested
+                # object's opening character, so pop it off the stack
+                in_object.pop()
+                if not in_object:
+                    # This is the outer object's closing character, so record
+                    # the substituted text and generate the tagged text
+                    end = i + 1
+                    tag = tag_format.format(len(replaced_text))
+                    replaced_text[tag] = data[start:end]
+                    output += tag
+    return output, replaced_text
+
+
+def recover_object_text_from_list(words, json):
+    if not json:
+        return words
+    # NOTE(twilson) This does not handle the case of having multiple replaced
+    # objects in the same word, e.g. two json adjacent json strings
+    return [json.get(word, word) for word in words]
+
+
 def unbox_json(json):
     if type(json) is list and len(json) == 1:
         return json[0]
@@ -389,8 +449,15 @@ def idl_set(idl, commands, step):
     increment = False
     fetch_cmds = []
     events = []
+    # `commands` is a comma-separated list of space-separated arguments. To
+    # handle commands that take arguments that may contain spaces or commas,
+    # e.g. JSON, it is necessary to process `commands` to extract those
+    # arguments before splitting by ',' or ' ' below, and then re-insert them
+    # after the arguments are split.
+    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:]
 
@@ -449,6 +516,12 @@ def idl_set(idl, commands, step):
             s = txn.insert(idl.tables["simple"], new_uuid=uuid.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')
-- 
2.34.3

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to