diff mbox series

[ovs-dev,v2] python: ovsdb-idl: Add custom transaction operations.

Message ID 20240415162049.1073623-1-twilson@redhat.com
State Changes Requested
Delegated to: Ilya Maximets
Headers show
Series [ovs-dev,v2] python: ovsdb-idl: Add custom transaction operations. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/intel-ovs-compilation success test: success

Commit Message

Terry Wilson April 15, 2024, 4:20 p.m. UTC
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(-)

Comments

Ilya Maximets June 27, 2024, 9:48 p.m. UTC | #1
On 4/15/24 18:20, Terry Wilson wrote:
> 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>

Thanks, Terry.  I wish there were a better way to deal with this issue,
but this solution seems fine as well.  See some comments below.

> ---
>  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(-)
> 
> diff --git a/NEWS b/NEWS
> index b92cec532..f30b859fd 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -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
> diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
> index a80da84e7..1220e4cc6 100644
> --- a/python/ovs/db/idl.py
> +++ b/python/ovs/db/idl.py
> @@ -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

I think, we should add them before the abort, so the execution gets
to these even with dry run.

>          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.

The docs generally shouldn't talk to the reader, but describe the
behavior instead.  How about 'E.g. bulk deleting rows from the server
without downloading them into a local cache.' ?

Also, we need to document that these operations are always added to
the end of transaction, i.e. after all other operations.

> +
> +        :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 fb568dd82..487545a13 100644
> --- a/tests/ovsdb-idl.at
> +++ b/tests/ovsdb-idl.at
> @@ -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])
> diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
> index 48f8ee2d7..58829f520 100644
> --- a/tests/test-ovsdb.py
> +++ b/tests/test-ovsdb.py
> @@ -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
> +
> +

AFAIU, we need to escape the commas, so the commands.split(',') doesn't
split the JSON in parts, right?

In that case, maybe we can do something simpler, like replacing all the
commas with '|' and recovering them later?  It's a test-only application
so we don't have to handle every single case, we may just assume that
tests do not have '|' character.

Something like:

>>> inside = 0
>>> result = ''
>>> for i, s in enumerate(commands):
...     if s == '{':
...         inside = inside + 1
...     if s == '}':
...         inside = inside - 1
...     if s == ',' and inside > 0:
...         result = result + '|'
...     else:
...         result = result + s

And then just replace back once we get the words[1:].  What do you think?
For me it's much easier to understand what's going on this way.

(I just hacked together the code above, it's probably can be written better,
e.g. it doesn't need an enumeration, I guess.)

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index b92cec532..f30b859fd 100644
--- a/NEWS
+++ b/NEWS
@@ -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
diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
index a80da84e7..1220e4cc6 100644
--- a/python/ovs/db/idl.py
+++ b/python/ovs/db/idl.py
@@ -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
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index fb568dd82..487545a13 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -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])
diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
index 48f8ee2d7..58829f520 100644
--- a/tests/test-ovsdb.py
+++ b/tests/test-ovsdb.py
@@ -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')