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
