Eric Blake
2022-Oct-26  21:15 UTC
[Libguestfs] [libnbd PATCH 0/3] Improve nbdsh -u handling
I was trying to add a TLS test to nbdkit, and got annoyed that I couldn't just write --run 'nbdsh -u "$uri" -c ...'. This fixes the libnbd side of the problem (a URI with ?tls-psk-file=... is rejected unless we tell libnbd that parsing local files is okay); but I will also be working on patches to nbdkit ($uri is more useful if it includes 'alice@' and '?tls-psk-file=...' in the first place). Eric Blake (3): nbdsh: Refactor handling of -u and friends nbdsh: Improve --help and initial banner contents. nbdsh: Allow -u interleaved with -c python/nbdsh.py | 142 +++++++++++++++++++++++++++------------------ sh/test-context.sh | 26 ++++----- sh/test-error.sh | 37 +++++++----- 3 files changed, 119 insertions(+), 86 deletions(-) -- 2.37.3
Eric Blake
2022-Oct-26  21:15 UTC
[Libguestfs] [libnbd PATCH 1/3] nbdsh: Refactor handling of -u and friends
Slightly rearrange the code so that we can reuse the -c framework for
executing the code snippets for -u, --opt-mode, and --base-allocation.
Slightly changes the error message when a URI is invalid (which
requires revamping test-error.sh a bit to match), but otherwise no
semantic change intended.
---
 python/nbdsh.py  | 67 ++++++++++++++++++++++--------------------------
 sh/test-error.sh | 37 ++++++++++++++------------
 2 files changed, 52 insertions(+), 52 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index a98a9113..84ac84e2 100644
--- a/python/nbdsh.py
+++ b/python/nbdsh.py
@@ -1,5 +1,5 @@
 # NBD client library in userspace
-# Copyright (C) 2013-2021 Red Hat Inc.
+# Copyright (C) 2013-2022 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -35,12 +35,13 @@ def shell():
                                      epilog=epilog)
     short_options = []
     long_options = []
+    shortcuts = 0
     parser.add_argument('--base-allocation',
action='store_true',
                         help='request the "base:allocation" meta
context')
     long_options.append("--base-allocation")
-    parser.add_argument('-c', '--command',
action='append',
+    parser.add_argument('-c', '--command',
action='append', default=[],
                         help="run a Python statement "
                         "(may be used multiple times)")
     short_options.append("-c")
@@ -98,10 +99,6 @@ def shell():
         print("\n".join(long_options))
         exit(0)
-    # Create the banner and prompt.
-    banner = make_banner(args)
-    sys.ps1 = "nbd> "
-
     # If verbose, set LIBNBD_DEBUG=1
     if args.verbose:
         os.environ["LIBNBD_DEBUG"] = "1"
@@ -110,42 +107,40 @@ def shell():
     if not args.n:
         h = nbd.NBD()
         h.set_handle_name("nbdsh")
+        cmds = args.command
         # Set other attributes in the handle.
+        if args.uri is not None:
+            cmds.insert(0, 'h.connect_uri("%s")' % args.uri)
+            shortcuts += 1
         if args.base_allocation:
-            h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)
+            cmds.insert(0,
'h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)')
+            shortcuts += 1
         if args.opt_mode:
-            h.set_opt_mode(True)
+            cmds.insert(0, 'h.set_opt_mode(True)')
+            shortcuts += 1
-        # Parse the URI.
-        if args.uri is not None:
-            try:
-                h.connect_uri(args.uri)
-            except nbd.Error as ex:
-                print("nbdsh: unable to connect to uri '%s':
%s" %
-                      (args.uri, ex.string), file=sys.stderr)
-                sys.exit(1)
-
-    # If there are no -c or --command parameters, go interactive,
-    # otherwise we run the commands and exit.
-    if not args.command:
-        code.interact(banner=banner, local=locals(), exitmsg='')
-    else:
-        # https://stackoverflow.com/a/11754346
-        d = dict(locals(), **globals())
-        try:
-            for c in args.command:
-                if c != '-':
-                    exec(c, d, d)
-                else:
-                    exec(sys.stdin.read(), d, d)
-        except nbd.Error as ex:
-            if nbd.NBD().get_debug():
-                traceback.print_exc()
+    # Run all -c snippets, including any shortcuts we just added
+    # https://stackoverflow.com/a/11754346
+    d = dict(locals(), **globals())
+    try:
+        for c in args.command:
+            if c != '-':
+                exec(c, d, d)
             else:
-                print("nbdsh: command line script failed: %s" %
ex.string,
-                      file=sys.stderr)
-            sys.exit(1)
+                exec(sys.stdin.read(), d, d)
+    except nbd.Error as ex:
+        if nbd.NBD().get_debug():
+            traceback.print_exc()
+        else:
+            print("nbdsh: command line script failed: %s" %
ex.string,
+                  file=sys.stderr)
+        sys.exit(1)
+
+    # If there are no explicit -c or --command parameters, go interactive.
+    if len(args.command) - shortcuts == 0:
+        sys.ps1 = "nbd> "
+        code.interact(banner=make_banner(args), local=locals(),
exitmsg='')
 def make_banner(args):
diff --git a/sh/test-error.sh b/sh/test-error.sh
index a33ce475..7dbd7ae4 100755
--- a/sh/test-error.sh
+++ b/sh/test-error.sh
@@ -35,32 +35,37 @@ cat $err
 grep Traceback $err && fail=1
 grep '^nbdsh: .*unrecognized.*no-such' $err
-# Test behavior when -u fails.  No python trace should be present.
-nbdsh -u 'nbd+unix:///?socket=/nosuchsock' >$out 2>$err
&& fail=1
-test ! -s $out
-cat $err
-grep Traceback $err && fail=1
-grep '^nbdsh: unable to connect to uri.*nosuchsock' $err
-
 # Triggering nbd.Error non-interactively (via -c) prints the error. The
-# output includes the python trace when debugging is enabled (which is
-# the default for our testsuite, when using ./run).
-nbdsh -c 'h.is_read_only()' >$out 2>$err && fail=1
+# output includes the python trace when debugging is enabled (our default
+# when run under 'make check', but set explicitly here to make sure).
+LIBNBD_DEBUG=1 nbdsh -c 'h.is_read_only()' >$out 2>$err
&& fail=1
 test ! -s $out
 cat $err
 grep Traceback $err
 grep 'in is_read_only' $err
 grep '^nbd\.Error: nbd_is_read_only: ' $err
-# Override ./run's default to show that without debug, the error is
succinct.
-nbdsh -c '
-import os
-os.environ["LIBNBD_DEBUG"] = "0"
-h.is_read_only()
-' >$out 2>$err && fail=1
+# Without debugging, the error is succinct.
+LIBNBD_DEBUG=0 nbdsh -c 'h.is_read_only()' >$out 2>$err
&& fail=1
 test ! -s $out
 cat $err
 grep Traceback $err && fail=1
 grep '^nbdsh: command line script failed: nbd_is_read_only: ' $err
+# --verbose overrides environment to request debugging.
+LIBNBD_DEBUG=0 nbdsh --verbose -c 'h.is_read_only()' >$out 2>$err
&& fail=1
+test ! -s $out
+cat $err
+grep Traceback $err
+grep 'in is_read_only' $err
+grep '^nbd\.Error: nbd_is_read_only: ' $err
+
+# Test behavior when -u fails; since it triggers nbd.Error, it should
+# be succinct without debug.
+LIBNBD_DEBUG=0 nbdsh -u 'nbd+unix:///?socket=/nosuchsock' >$out
2>$err && fail=1
+test ! -s $out
+cat $err
+grep Traceback $err && fail=1
+grep '^nbdsh: .*nbd_connect_uri: ' $err
+
 exit $fail
-- 
2.37.3
Eric Blake
2022-Oct-26  21:16 UTC
[Libguestfs] [libnbd PATCH 2/3] nbdsh: Improve --help and initial banner contents.
Document all options in --help output.  If -n is not in use, then
enhance the banner to print the current state of h, and further tailor
the advice given on useful next steps to take to mention opt_go when
using --opt-mode.
---
 python/nbdsh.py | 37 +++++++++++++++++++++++++++----------
 1 file changed, 27 insertions(+), 10 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index 84ac84e2..f23d8bfb 100644
--- a/python/nbdsh.py
+++ b/python/nbdsh.py
@@ -61,11 +61,13 @@ def shell():
     # For back-compat, provide --connect as an undocumented synonym to --uri
     parser.add_argument('--connect', dest='uri',
help=argparse.SUPPRESS)
-    parser.add_argument('-v', '--verbose',
action='store_true')
+    parser.add_argument('-v', '--verbose',
action='store_true',
+                        help="enable verbose debugging")
     short_options.append("-v")
     long_options.append("--verbose")
-    parser.add_argument('-V', '--version',
action='store_true')
+    parser.add_argument('-V', '--version',
action='store_true',
+                        help="display version information")
     short_options.append("-V")
     long_options.append("--version")
@@ -140,30 +142,45 @@ def shell():
     # If there are no explicit -c or --command parameters, go interactive.
     if len(args.command) - shortcuts == 0:
         sys.ps1 = "nbd> "
-        code.interact(banner=make_banner(args), local=locals(),
exitmsg='')
+        if args.n:
+            h = None
+        code.interact(banner=make_banner(args, h), local=locals(),
exitmsg='')
-def make_banner(args):
+def make_banner(args, h):
     lines = []
     def line(x): lines.append(x)
     def blank(): line("")
     def example(ex, desc): line("%-34s # %s" % (ex, desc))
+    connect_hint = False
+    go_hint = False
     blank()
     line("Welcome to nbdsh, the shell for interacting with")
     line("Network Block Device (NBD) servers.")
     blank()
-    if not args.n:
-        line("The ???nbd??? module has already been imported and
there")
-        line("is an open NBD handle called ???h???.")
-        blank()
-    else:
+    if args.n:
         line("The ???nbd??? module has already been imported.")
         blank()
         example("h = nbd.NBD()", "Create a new handle.")
-    if args.uri is None:
+        connect_hint = True
+    else:
+        state = h.connection_state()
+        state = state[:state.find(':')]
+        line("The ???nbd??? module has already been imported and
there")
+        line("is an open NBD handle called ???h??? in state
'%s'." % state)
+        blank()
+        if h.aio_is_created():
+            connect_hint = True
+            if h.get_opt_mode():
+                go_hint = True
+        elif h.aio_is_negotiating():
+            go_hint = True
+    if connect_hint:
         example('h.connect_tcp("remote", "10809")',
                 "Connect to a remote server.")
+    if go_hint:
+        example("h.opt_go()", "Finish option negotiation")
     example("h.get_size()", "Get size of the remote disk.")
     example("buf = h.pread(512, 0)", "Read the first
sector.")
     example("exit() or Ctrl-D", "Quit the shell")
-- 
2.37.3
Eric Blake
2022-Oct-26  21:16 UTC
[Libguestfs] [libnbd PATCH 3/3] nbdsh: Allow -u interleaved with -c
Starting with a back-story: I wanted[1] to write:
$ nbdkit --tls=require --tls-psk=$dir/tests/keys.psk null --run \
 'nbdsh -u "nbds://alice at
localhost/?tls-psk-file='"$dir"'/tests/keys.psk" \
   -c "h.get_size()"'
but it fails with the latest libnbd release, due to:
nbdsh: unable to connect to uri 'nbds://alice at
localhost/?tls-psk-file=/home/eblake/libnbd/tests/keys.psk':
nbd_connect_uri: local file access (tls-psk-file) is not allowed, call
nbd_set_uri_allow_local_file to enable this: Operation not permitted
But without this patch, this obvious attempt at a fix doesn't work:
$ nbdkit --tls=require --tls-psk=$dir/tests/keys.psk null --run \
 'nbdsh -c "h.set_uri_allow_local_file(True)" \
   -u "nbds://alice at
localhost/?tls-psk-file='"$dir"'/tests/keys.psk" \
   -c "h.get_size()"'
because nbdsh was performing h.connect_uri() prior to running any of
the -c strings (seen more obviously two patches ago when refactoring
shortcuts to reuse the -c framework), where the attempt to inject -c
to obey the error message is processed too late, even though it
occurred earlier in the command line.  The immediate workaround is
thus:
$ nbdkit --tls=require --tls-psk=$dir/tests/keys.psk null --run \
 'nbdsh -c "h.set_uri_allow_local_file(True)" \
   -c "h.connect_uri(\"nbds://alice at
localhost/?tls-psk-file='"$dir"'/tests/keys.psk\")"
\
   -c "h.get_size()"'
which gets awkward because of the multiple layers of quoting required
(--run, -c, and Python each strip a layer).  But we can do better.  We
can create args.command in command-line order by introducing the use
of a custom Action subclass of argparse.  test-context.sh has to be
updated to match our saner processing order.  nbdkit's testsuite still
passes even when using this update to nbdsh, so I don't think programs
in the wild were relying on insane command-line rearranging.
[1] Actually, I wanted to use -U- and write -u "$uri" instead of -u
"nbds+unix...", but that involves teaching nbdkit how to display a
more-useful URI when TLS is in use.  Not this patch's problem.
---
 python/nbdsh.py    | 60 ++++++++++++++++++++++++++++++----------------
 sh/test-context.sh | 26 ++++++++++----------
 2 files changed, 51 insertions(+), 35 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index f23d8bfb..370f3fd2 100644
--- a/python/nbdsh.py
+++ b/python/nbdsh.py
@@ -33,11 +33,37 @@ def shell():
     epilog = '''Please read the nbdsh(1) manual page for full
usage.'''
     parser = argparse.ArgumentParser(prog='nbdsh',
description=description,
                                      epilog=epilog)
+
+    # We want to treat some options as shortcuts for longer '-c code'
+    # snippets while preserving relative ordering of the overall command
+    # line; use of the shortcuts does not prevent interactive mode.
+    class ShortcutAction(argparse.Action):
+        def __init__(self, option_strings, dest, nargs=None, const=None,
+                     default=argparse.SUPPRESS, **kwargs):
+            if nargs not in [0, 1]:
+                raise ValueError("nargs must be 0 or 1")
+            if const is None:
+                raise ValueError("missing const string for use as
shortcut")
+            super().__init__(option_strings, dest, nargs=nargs, const=const,
+                             default=default, **kwargs)
+
+        def __call__(self, parser, namespace, values, option_string=None):
+            setattr(namespace, 'shortcuts',
+                    getattr(namespace, 'shortcuts') + 1)
+            commands = getattr(namespace, 'command')[:]
+            if self.nargs == 0:
+                commands.append(self.const)
+            else:
+                commands.append(self.const % values[0])
+            setattr(namespace, 'command', commands)
+
+    parser.set_defaults(shortcuts=0)
     short_options = []
     long_options = []
-    shortcuts = 0
-    parser.add_argument('--base-allocation',
action='store_true',
+    parser.add_argument('--base-allocation', action=ShortcutAction,
nargs=0,
+                        const='h.add_meta_context' +
+                        '(nbd.CONTEXT_BASE_ALLOCATION)',
                         help='request the "base:allocation" meta
context')
     long_options.append("--base-allocation")
@@ -51,15 +77,20 @@ def shell():
                         help="do not create the implicit handle
'h'")
     short_options.append("-n")
-    parser.add_argument('--opt-mode', action='store_true',
+    parser.add_argument('--opt-mode', action=ShortcutAction, nargs=0,
+                        const='h.set_opt_mode(True)',
                         help='request opt mode during connection')
     long_options.append("--opt-mode")
-    parser.add_argument('-u', '--uri', help="connect to
NBD URI")
+    parser.add_argument('-u', '--uri', action=ShortcutAction,
nargs=1,
+                        const='h.connect_uri("%s")',
+                        help="connect to NBD URI")
     short_options.append("-u")
     long_options.append("--uri")
     # For back-compat, provide --connect as an undocumented synonym to --uri
-    parser.add_argument('--connect', dest='uri',
help=argparse.SUPPRESS)
+    parser.add_argument('--connect', action=ShortcutAction, nargs=1,
+                        const='h.connect_uri("%s")',
+                        dest='uri', help=argparse.SUPPRESS)
     parser.add_argument('-v', '--verbose',
action='store_true',
                         help="enable verbose debugging")
@@ -80,9 +111,7 @@ def shell():
     args = parser.parse_args()
     # It's an error if -n is passed with certain other options.
-    if args.n and (args.base_allocation or
-                   args.opt_mode or
-                   args.uri is not None):
+    if args.n and args.shortcuts > 0:
         print("error: -n option cannot be used with " +
               "--base-allocation, --opt-mode or --uri",
               file=sys.stderr)
@@ -111,18 +140,7 @@ def shell():
         h.set_handle_name("nbdsh")
         cmds = args.command
-        # Set other attributes in the handle.
-        if args.uri is not None:
-            cmds.insert(0, 'h.connect_uri("%s")' % args.uri)
-            shortcuts += 1
-        if args.base_allocation:
-            cmds.insert(0,
'h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)')
-            shortcuts += 1
-        if args.opt_mode:
-            cmds.insert(0, 'h.set_opt_mode(True)')
-            shortcuts += 1
-
-    # Run all -c snippets, including any shortcuts we just added
+    # Run all -c snippets, including shortcuts
     # https://stackoverflow.com/a/11754346
     d = dict(locals(), **globals())
     try:
@@ -140,7 +158,7 @@ def shell():
         sys.exit(1)
     # If there are no explicit -c or --command parameters, go interactive.
-    if len(args.command) - shortcuts == 0:
+    if len(args.command) - args.shortcuts == 0:
         sys.ps1 = "nbd> "
         if args.n:
             h = None
diff --git a/sh/test-context.sh b/sh/test-context.sh
index 168fe487..8f759075 100755
--- a/sh/test-context.sh
+++ b/sh/test-context.sh
@@ -31,15 +31,13 @@ if test "x$output" != xFalse; then
     fail=1
 fi
-# Use of -c to request context is too late with -u
-output=$(nbdkit -U - null --run 'nbdsh -c "
-try:
-    h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)
-    assert False
-except nbd.Error:
-    print(\"okay\")
-" -u "nbd+unix://?socket=$unixsocket"')
-if test "x$output" != xokay; then
+# Can also use manual -c to request context before -u
+output=$(nbdkit -U - null --run 'nbdsh \
+-c "h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)" \
+-u "nbd+unix://?socket=$unixsocket" \
+-c "print(h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION))"
+')
+if test "x$output" != xTrue; then
     echo "$0: unexpected output: $output"
     fail=1
 fi
@@ -53,9 +51,9 @@ if test "x$output" != xTrue; then
     fail=1
 fi
-# Again, but with --b after -u, and with abbreviated option names
+# Again, but with abbreviated option names
 output=$(nbdkit -U - null --run 'nbdsh \
-    -u "nbd+unix://?socket=$unixsocket" --b \
+    --b -u "nbd+unix://?socket=$unixsocket" \
     -c "print(h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION))"')
 if test "x$output" != xTrue; then
     echo "$0: unexpected output: $output"
@@ -65,7 +63,7 @@ fi
 if [[ $(nbdkit --help) =~ --no-sr ]]; then
     # meta context depends on server cooperation
     output=$(nbdkit -U - --no-sr null --run 'nbdsh \
-      -u "nbd+unix://?socket=$unixsocket" --base-allocation \
+      --base-allocation -u "nbd+unix://?socket=$unixsocket" \
       -c
"print(h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION))"')
     if test "x$output" != xFalse; then
         echo "$0: unexpected output: $output"
@@ -77,7 +75,7 @@ fi
 # Test interaction with opt mode
 output=$(nbdkit -U - null --run 'nbdsh \
-    -u "nbd+unix://?socket=$unixsocket" --opt-mode --base-allocation
\
+    --opt-mode --base-allocation -u "nbd+unix://?socket=$unixsocket"
\
     -c "
 try:
     h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION)
@@ -94,7 +92,7 @@ fi
 # And with --opt-mode, we can get away without --base-allocation
 output=$(nbdkit -U - null --run 'nbdsh \
-    -u "nbd+unix://?socket=$unixsocket" --opt-mode \
+    --opt-mode -u "nbd+unix://?socket=$unixsocket" \
     -c "h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)" \
     -c "h.opt_go()" \
     -c "print(h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION))"')
-- 
2.37.3