diff mbox series

[net-next,v1,2/2] tools/net/ynl: Add multi message support to ynl

Message ID 20240327181700.77940-3-donald.hunter@gmail.com (mailing list archive)
State Changes Requested
Delegated to: Netdev Maintainers
Headers show
Series netlink: Add nftables spec w/ multi messages | expand

Checks

Context Check Description
netdev/series_format success Posting correctly formatted
netdev/tree_selection success Clearly marked for net-next
netdev/ynl success Generated files up to date; no warnings/errors; no diff in generated;
netdev/fixes_present success Fixes tag not required for -next series
netdev/header_inline success No static functions without inline keyword in header files
netdev/build_32bit success Errors and warnings before: 8 this patch: 8
netdev/build_tools success Errors and warnings before: 0 this patch: 0
netdev/cc_maintainers success CCed 5 of 5 maintainers
netdev/build_clang success Errors and warnings before: 8 this patch: 8
netdev/verify_signedoff success Signed-off-by tag matches author and committer
netdev/deprecated_api success None detected
netdev/check_selftest success No net selftest shell script
netdev/verify_fixes success No Fixes tag
netdev/build_allmodconfig_warn success Errors and warnings before: 8 this patch: 8
netdev/checkpatch success total: 0 errors, 0 warnings, 0 checks, 140 lines checked
netdev/build_clang_rust success No Rust files in patch. Skipping build
netdev/kdoc success Errors and warnings before: 0 this patch: 0
netdev/source_inline success Was 0 now: 0
netdev/contest success net-next-2024-03-29--06-00 (tests: 951)

Commit Message

Donald Hunter March 27, 2024, 6:17 p.m. UTC
Add a "--multi <op> <json>" command line to ynl that makes it possible
to add several operations to a single netlink request payload. The
--multi command line option is repeated for each operation.

This is used by the nftables family for transaction batches. For
example:

./tools/net/ynl/cli.py \
 --spec Documentation/netlink/specs/nftables.yaml \
 --multi batch-begin '{"res-id": 10}' \
 --multi newtable '{"name": "test", "nfgen-family": 1}' \
 --multi newchain '{"name": "chain", "table": "test", "nfgen-family": 1}' \
 --multi batch-end '{"res-id": 10}'

Signed-off-by: Donald Hunter <donald.hunter@gmail.com>
---
 tools/net/ynl/cli.py     | 22 ++++++++++++++++---
 tools/net/ynl/lib/ynl.py | 47 +++++++++++++++++++++++++++-------------
 2 files changed, 51 insertions(+), 18 deletions(-)

Comments

Jakub Kicinski March 29, 2024, 12:57 a.m. UTC | #1
On Wed, 27 Mar 2024 18:17:00 +0000 Donald Hunter wrote:
> -    parser = argparse.ArgumentParser(description='YNL CLI sample')
> +    description = """
> +    YNL CLI utility - a general purpose netlink utility that uses YNL specs

YNL specs is intentional or should have been YAML? :)

> +    to drive protocol encoding and decoding.
> +    """
> +    epilog = """
> +    The --multi option can be repeated to include several operations
> +    in the same netlink payload.
> +    """
> +
> +    parser = argparse.ArgumentParser(description=description,
> +                                     epilog=epilog)
>      parser.add_argument('--spec', dest='spec', type=str, required=True)
>      parser.add_argument('--schema', dest='schema', type=str)
>      parser.add_argument('--no-schema', action='store_true')
>      parser.add_argument('--json', dest='json_text', type=str)
> -    parser.add_argument('--do', dest='do', type=str)
> -    parser.add_argument('--dump', dest='dump', type=str)
> +    parser.add_argument('--do', dest='do', metavar='OPERATION', type=str)
> +    parser.add_argument('--dump', dest='dump', metavar='OPERATION', type=str)
>      parser.add_argument('--sleep', dest='sleep', type=int)
>      parser.add_argument('--subscribe', dest='ntf', type=str)
>      parser.add_argument('--replace', dest='flags', action='append_const',
> @@ -40,6 +50,8 @@ def main():
>      parser.add_argument('--output-json', action='store_true')
>      parser.add_argument('--dbg-small-recv', default=0, const=4000,
>                          action='store', nargs='?', type=int)
> +    parser.add_argument('--multi', dest='multi', nargs=2, action='append',
> +                        metavar=('OPERATION', 'JSON_TEXT'), type=str)

We'd only support multiple "do" requests, I wonder if we should somehow
call this out. Is --multi-do unnecessary extra typing?

Code itself looks pretty good!
Donald Hunter March 29, 2024, 1:37 p.m. UTC | #2
Jakub Kicinski <kuba@kernel.org> writes:

> On Wed, 27 Mar 2024 18:17:00 +0000 Donald Hunter wrote:
>> -    parser = argparse.ArgumentParser(description='YNL CLI sample')
>> +    description = """
>> +    YNL CLI utility - a general purpose netlink utility that uses YNL specs
>
> YNL specs is intentional or should have been YAML? :)

I'm not sure it was intentional, but YAML is definitely better :-)

>> +    to drive protocol encoding and decoding.
>> +    """
>> +    epilog = """
>> +    The --multi option can be repeated to include several operations
>> +    in the same netlink payload.
>> +    """
>> +
>> +    parser = argparse.ArgumentParser(description=description,
>> +                                     epilog=epilog)
>>      parser.add_argument('--spec', dest='spec', type=str, required=True)
>>      parser.add_argument('--schema', dest='schema', type=str)
>>      parser.add_argument('--no-schema', action='store_true')
>>      parser.add_argument('--json', dest='json_text', type=str)
>> -    parser.add_argument('--do', dest='do', type=str)
>> -    parser.add_argument('--dump', dest='dump', type=str)
>> +    parser.add_argument('--do', dest='do', metavar='OPERATION', type=str)
>> +    parser.add_argument('--dump', dest='dump', metavar='OPERATION', type=str)
>>      parser.add_argument('--sleep', dest='sleep', type=int)
>>      parser.add_argument('--subscribe', dest='ntf', type=str)
>>      parser.add_argument('--replace', dest='flags', action='append_const',
>> @@ -40,6 +50,8 @@ def main():
>>      parser.add_argument('--output-json', action='store_true')
>>      parser.add_argument('--dbg-small-recv', default=0, const=4000,
>>                          action='store', nargs='?', type=int)
>> +    parser.add_argument('--multi', dest='multi', nargs=2, action='append',
>> +                        metavar=('OPERATION', 'JSON_TEXT'), type=str)
>
> We'd only support multiple "do" requests, I wonder if we should somehow
> call this out. Is --multi-do unnecessary extra typing?

I prefer --multi but will update the help text to say "DO-OPERATIION"
and "... several do operations".

> Code itself looks pretty good!
Jakub Kicinski March 29, 2024, 3:43 p.m. UTC | #3
On Fri, 29 Mar 2024 13:37:31 +0000 Donald Hunter wrote:
> > We'd only support multiple "do" requests, I wonder if we should somehow
> > call this out. Is --multi-do unnecessary extra typing?  
> 
> I prefer --multi but will update the help text to say "DO-OPERATIION"
> and "... several do operations".

Alright, technically doing multi-dump should also work, but maybe
there's less of a benefit there, so we can keep the multi focused
on do for now.

Looking at the code again, are you sure we'll process all the responses
not just the first one?

Shouldn't this:

+                    del reqs_by_seq[nl_msg.nl_seq]
                     done = True

be something like:

		del reqs_by_seq[nl_msg.nl_seq]
		done = len(reqs_by_seq) == 0

?

Would be good to add an example of multi executing some get operations.

My other concern is the formatting of the response. For mutli we should
probably retain the indexes, e.g. 3 dos should produce an array with a
length of 3, some of the entries may be None if the command only acked.
Would that make sense?
Donald Hunter March 29, 2024, 6:57 p.m. UTC | #4
Jakub Kicinski <kuba@kernel.org> writes:

> On Fri, 29 Mar 2024 13:37:31 +0000 Donald Hunter wrote:
>> > We'd only support multiple "do" requests, I wonder if we should somehow
>> > call this out. Is --multi-do unnecessary extra typing?  
>> 
>> I prefer --multi but will update the help text to say "DO-OPERATIION"
>> and "... several do operations".
>
> Alright, technically doing multi-dump should also work, but maybe
> there's less of a benefit there, so we can keep the multi focused
> on do for now.
>
> Looking at the code again, are you sure we'll process all the responses
> not just the first one?
>
> Shouldn't this:
>
> +                    del reqs_by_seq[nl_msg.nl_seq]
>                      done = True
>
> be something like:
>
> 		del reqs_by_seq[nl_msg.nl_seq]
> 		done = len(reqs_by_seq) == 0
>

Hmm yes, that's a good catch. I need to check the DONE semantics for
these nftables batch operations.

> Would be good to add an example of multi executing some get operations.

I think this was a blind spot on my part because nftables doesn't
support batch for get operations:

https://elixir.bootlin.com/linux/latest/source/net/netfilter/nf_tables_api.c#L9092

I'll need to try using multi for gets without any batch messages and see how
everything behaves.

> My other concern is the formatting of the response. For mutli we should
> probably retain the indexes, e.g. 3 dos should produce an array with a
> length of 3, some of the entries may be None if the command only acked.
> Would that make sense?

As I said, a blind spot on my part - I didn't really think there was a
need to do anything for None responses but if get can work then an array
of responses will be needed.
Donald Hunter March 29, 2024, 9:01 p.m. UTC | #5
On Fri, 29 Mar 2024 at 18:58, Donald Hunter <donald.hunter@gmail.com> wrote:
>
> Jakub Kicinski <kuba@kernel.org> writes:
>
> > Looking at the code again, are you sure we'll process all the responses
> > not just the first one?
> >
> > Shouldn't this:
> >
> > +                    del reqs_by_seq[nl_msg.nl_seq]
> >                      done = True
> >
> > be something like:
> >
> >               del reqs_by_seq[nl_msg.nl_seq]
> >               done = len(reqs_by_seq) == 0
> >
>
> Hmm yes, that's a good catch. I need to check the DONE semantics for
> these nftables batch operations.

Well that's a problem:

./tools/net/ynl/cli.py \
     --spec Documentation/netlink/specs/nftables.yaml \
     --multi batch-begin '{"res-id": 10}' \
     --multi newtable '{"name": "test", "nfgen-family": 1}' \
     --multi newchain '{"name": "chain", "table": "test", "nfgen-family": 1}' \
     --multi batch-end '{"res-id": 10}'
Adding: 20778
Adding: 20779
Adding: 20780
Adding: 20781
Done: 20779
Done: 20780

There's no response for 'batch-begin' or 'batch-end'. We may need a
per op spec property to tell us if a request will be acknowledged.

> > Would be good to add an example of multi executing some get operations.
>
> I think this was a blind spot on my part because nftables doesn't
> support batch for get operations:
>
> https://elixir.bootlin.com/linux/latest/source/net/netfilter/nf_tables_api.c#L9092
>
> I'll need to try using multi for gets without any batch messages and see how
> everything behaves.

Okay, so it can be made to work. Will incorporate into the next revision:

./tools/net/ynl/cli.py \
     --spec Documentation/netlink/specs/nftables.yaml \
     --multi gettable '{"name": "test", "nfgen-family": 1}' \
     --multi getchain '{"name": "chain", "table": "test", "nfgen-family": 1}'
[{'flags': set(),
  'handle': 10,
  'name': 'test',
  'nfgen-family': 1,
  'res-id': 200,
  'use': 1,
  'version': 0},
 {'handle': 1,
  'name': 'chain',
  'nfgen-family': 1,
  'res-id': 200,
  'table': 'test',
  'use': 0,
  'version': 0}]
Jakub Kicinski March 29, 2024, 9:46 p.m. UTC | #6
On Fri, 29 Mar 2024 21:01:09 +0000 Donald Hunter wrote:
> There's no response for 'batch-begin' or 'batch-end'. We may need a
> per op spec property to tell us if a request will be acknowledged.

:(

Pablo, could we possibly start processing the ACK flags on those
messages? Maybe the existing user space doesn't set ACK so nobody
would notice?

I don't think the messages are otherwise marked as special from 
the "netlink layer" perspective.

> > I think this was a blind spot on my part because nftables doesn't
> > support batch for get operations:
> >
> > https://elixir.bootlin.com/linux/latest/source/net/netfilter/nf_tables_api.c#L9092
> >
> > I'll need to try using multi for gets without any batch messages and see how
> > everything behaves.  
> 
> Okay, so it can be made to work. Will incorporate into the next revision:

Great!
Pablo Neira Ayuso March 29, 2024, 10:12 p.m. UTC | #7
On Fri, Mar 29, 2024 at 02:46:39PM -0700, Jakub Kicinski wrote:
> On Fri, 29 Mar 2024 21:01:09 +0000 Donald Hunter wrote:
> > There's no response for 'batch-begin' or 'batch-end'. We may need a
> > per op spec property to tell us if a request will be acknowledged.
> 
> :(
> 
> Pablo, could we possibly start processing the ACK flags on those
> messages? Maybe the existing user space doesn't set ACK so nobody
> would notice?
> 
> I don't think the messages are otherwise marked as special from 
> the "netlink layer" perspective.

It is possible to explore this. I don't have a use-case for NLM_F_ACK
and the begin marker message at this stage.

Thanks.
diff mbox series

Patch

diff --git a/tools/net/ynl/cli.py b/tools/net/ynl/cli.py
index f131e33ac3ee..1b8f87b472ba 100755
--- a/tools/net/ynl/cli.py
+++ b/tools/net/ynl/cli.py
@@ -19,13 +19,23 @@  class YnlEncoder(json.JSONEncoder):
 
 
 def main():
-    parser = argparse.ArgumentParser(description='YNL CLI sample')
+    description = """
+    YNL CLI utility - a general purpose netlink utility that uses YNL specs
+    to drive protocol encoding and decoding.
+    """
+    epilog = """
+    The --multi option can be repeated to include several operations
+    in the same netlink payload.
+    """
+
+    parser = argparse.ArgumentParser(description=description,
+                                     epilog=epilog)
     parser.add_argument('--spec', dest='spec', type=str, required=True)
     parser.add_argument('--schema', dest='schema', type=str)
     parser.add_argument('--no-schema', action='store_true')
     parser.add_argument('--json', dest='json_text', type=str)
-    parser.add_argument('--do', dest='do', type=str)
-    parser.add_argument('--dump', dest='dump', type=str)
+    parser.add_argument('--do', dest='do', metavar='OPERATION', type=str)
+    parser.add_argument('--dump', dest='dump', metavar='OPERATION', type=str)
     parser.add_argument('--sleep', dest='sleep', type=int)
     parser.add_argument('--subscribe', dest='ntf', type=str)
     parser.add_argument('--replace', dest='flags', action='append_const',
@@ -40,6 +50,8 @@  def main():
     parser.add_argument('--output-json', action='store_true')
     parser.add_argument('--dbg-small-recv', default=0, const=4000,
                         action='store', nargs='?', type=int)
+    parser.add_argument('--multi', dest='multi', nargs=2, action='append',
+                        metavar=('OPERATION', 'JSON_TEXT'), type=str)
     args = parser.parse_args()
 
     def output(msg):
@@ -73,6 +85,10 @@  def main():
         if args.dump:
             reply = ynl.dump(args.dump, attrs)
             output(reply)
+        if args.multi:
+            ops = [ (item[0], json.loads(item[1]), args.flags) for item in args.multi ]
+            reply = ynl.do_multi(ops)
+            output(reply)
     except NlError as e:
         print(e)
         exit(1)
diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py
index 557ef5a22b7d..cecd89db7d58 100644
--- a/tools/net/ynl/lib/ynl.py
+++ b/tools/net/ynl/lib/ynl.py
@@ -927,16 +927,11 @@  class YnlFamily(SpecFamily):
 
       return op['do']['request']['attributes'].copy()
 
-    def _op(self, method, vals, flags=None, dump=False):
-        op = self.ops[method]
-
+    def _encode_message(self, op, vals, flags, req_seq):
         nl_flags = Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK
         for flag in flags or []:
             nl_flags |= flag
-        if dump:
-            nl_flags |= Netlink.NLM_F_DUMP
 
-        req_seq = random.randint(1024, 65535)
         msg = self.nlproto.message(nl_flags, op.req_value, 1, req_seq)
         if op.fixed_header:
             msg += self._encode_struct(op.fixed_header, vals)
@@ -944,8 +939,20 @@  class YnlFamily(SpecFamily):
         for name, value in vals.items():
             msg += self._add_attr(op.attr_set.name, name, value, search_attrs)
         msg = _genl_msg_finalize(msg)
+        return msg
 
-        self.sock.send(msg, 0)
+    def _ops(self, ops):
+        reqs_by_seq = {}
+        req_seq = random.randint(1024, 65535)
+        payload = b''
+        for (method, vals, flags) in ops:
+            op = self.ops[method]
+            msg = self._encode_message(op, vals, flags, req_seq)
+            reqs_by_seq[req_seq] = (op, msg)
+            payload += msg
+            req_seq += 1
+
+        self.sock.send(payload, 0)
 
         done = False
         rsp = []
@@ -954,8 +961,9 @@  class YnlFamily(SpecFamily):
             nms = NlMsgs(reply, attr_space=op.attr_set)
             self._recv_dbg_print(reply, nms)
             for nl_msg in nms:
-                if nl_msg.extack:
-                    self._decode_extack(msg, op, nl_msg.extack)
+                if nl_msg.extack and nl_msg.nl_seq in reqs_by_seq:
+                    (req_op, req_msg) = reqs_by_seq[nl_msg.nl_seq]
+                    self._decode_extack(req_msg, req_op, nl_msg.extack)
 
                 if nl_msg.error:
                     raise NlError(nl_msg)
@@ -963,13 +971,15 @@  class YnlFamily(SpecFamily):
                     if nl_msg.extack:
                         print("Netlink warning:")
                         print(nl_msg)
+                    del reqs_by_seq[nl_msg.nl_seq]
                     done = True
                     break
 
                 decoded = self.nlproto.decode(self, nl_msg)
+                rsp_op = self.rsp_by_value[decoded.cmd()]
 
                 # Check if this is a reply to our request
-                if nl_msg.nl_seq != req_seq or decoded.cmd() != op.rsp_value:
+                if nl_msg.nl_seq not in reqs_by_seq or decoded.cmd() != rsp_op.rsp_value:
                     if decoded.cmd() in self.async_msg_ids:
                         self.handle_ntf(decoded)
                         continue
@@ -977,19 +987,26 @@  class YnlFamily(SpecFamily):
                         print('Unexpected message: ' + repr(decoded))
                         continue
 
-                rsp_msg = self._decode(decoded.raw_attrs, op.attr_set.name)
+                rsp_msg = self._decode(decoded.raw_attrs, rsp_op.attr_set.name)
                 if op.fixed_header:
-                    rsp_msg.update(self._decode_struct(decoded.raw, op.fixed_header))
+                    rsp_msg.update(self._decode_struct(decoded.raw, rsp_op.fixed_header))
                 rsp.append(rsp_msg)
 
         if not rsp:
             return None
-        if not dump and len(rsp) == 1:
+        if not Netlink.NLM_F_DUMP in flags and len(rsp) == 1:
             return rsp[0]
         return rsp
 
+    def _op(self, method, vals, flags):
+        ops = [(method, vals, flags)]
+        return self._ops(ops)
+
     def do(self, method, vals, flags=None):
-        return self._op(method, vals, flags)
+        return self._op(method, vals, flags or [])
 
     def dump(self, method, vals):
-        return self._op(method, vals, [], dump=True)
+        return self._op(method, vals, [Netlink.NLM_F_DUMP])
+
+    def do_multi(self, ops):
+        return self._ops(ops)