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