Eric Blake
2022-Nov-04 21:18 UTC
[Libguestfs] [libnbd PATCH v2 0/3] Improve nbdsh -u handling
v1 was here: https://listman.redhat.com/archives/libguestfs/2022-October/030216.html Since then, I've incorporated changes based on Rich's feedback: swap order of patches 2 and 3 less change in patch 1 (including no unsafe eval(%s) for --uri) in patch 2, include -c in list of snippets to store, and use dict of lambdas to map back to the desired action Eric Blake (3): nbdsh: Refactor handling of -c nbdsh: Allow -u interleaved with -c nbdsh: Improve --help and initial banner contents. python/nbdsh.py | 142 +++++++++++++++++++++++++++------------------ sh/test-context.sh | 26 ++++----- sh/test-error.sh | 37 +++++++----- 3 files changed, 119 insertions(+), 86 deletions(-) -- 2.38.1
Eric Blake
2022-Nov-04 21:18 UTC
[Libguestfs] [libnbd PATCH v2 1/3] nbdsh: Refactor handling of -c
By telling argparse to always supply a list for args.command, it
becomes easier to sink all logic related to going interactive to after
all command line options have been processed. This in turn will make
future patches that preserve command line ordering easier to manage.
---
python/nbdsh.py | 47 +++++++++++++++++++++++------------------------
1 file changed, 23 insertions(+), 24 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index a98a9113..1552ac73 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
@@ -33,6 +33,8 @@ def shell():
epilog = '''Please read the nbdsh(1) manual page for full
usage.'''
parser = argparse.ArgumentParser(prog='nbdsh',
description=description,
epilog=epilog)
+
+ parser.set_defaults(command=[])
short_options = []
long_options = []
@@ -98,10 +100,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"
@@ -126,26 +124,27 @@ def shell():
(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
+ # 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) == 0:
+ sys.ps1 = "nbd> "
+ code.interact(banner=make_banner(args), local=locals(),
exitmsg='')
def make_banner(args):
--
2.38.1
Eric Blake
2022-Nov-04 21:18 UTC
[Libguestfs] [libnbd PATCH v2 2/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, where the attempt to inject -c to match the hint from
the original error message is processed too late, even though it
occurred earlier in the command line. The immediate workaround is
thus to spell out the URI with a manual -c:
$ 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.
Instead of creating separate args.command and args.uri, we can use a
custom argparse Action subclass which will merge various command-line
options into a unified args.snippets. Then with a little bit of
lambda magic, dispatching the various snippets in order looks fairly
easy.
I did have to drop one use of args.uri in the help banner, but it will
be restored in improved form in the next patch.
test-error.sh and test-context.sh have 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 | 76 +++++++++++++++++++++++++++-------------------
sh/test-context.sh | 26 ++++++++--------
sh/test-error.sh | 37 ++++++++++++----------
3 files changed, 77 insertions(+), 62 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index 1552ac73..0919c9ec 100644
--- a/python/nbdsh.py
+++ b/python/nbdsh.py
@@ -34,15 +34,37 @@ def shell():
parser = argparse.ArgumentParser(prog='nbdsh',
description=description,
epilog=epilog)
- parser.set_defaults(command=[])
+ # Allow intermixing of various options for replay in command-line order:
+ # each option registered with this Action subclass will append a tuple
+ # to a single list of snippets
+ class SnippetAction(argparse.Action):
+ def __init__(self, option_strings, dest, nargs=None,
+ default=argparse.SUPPRESS, **kwargs):
+ if nargs not in [0, None]:
+ raise ValueError("nargs must be 0 or None")
+ super().__init__(option_strings, dest, nargs=nargs,
+ default=default, **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ dest = self.dest
+ if dest != 'command':
+ setattr(namespace, 'need_handle',
+ getattr(namespace, 'need_handle') + 1)
+ elif values == '-':
+ dest = 'stdin'
+ snippets = getattr(namespace, 'snippets')[:]
+ snippets.append((dest, values))
+ setattr(namespace, 'snippets', snippets)
+
+ parser.set_defaults(need_handle=0, snippets=[])
short_options = []
long_options = []
- parser.add_argument('--base-allocation',
action='store_true',
+ parser.add_argument('--base-allocation', action=SnippetAction,
nargs=0,
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=SnippetAction,
help="run a Python statement "
"(may be used multiple times)")
short_options.append("-c")
@@ -52,15 +74,17 @@ 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=SnippetAction, nargs=0,
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=SnippetAction,
+ 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', dest='uri',
action=SnippetAction,
+ help=argparse.SUPPRESS)
parser.add_argument('-v', '--verbose',
action='store_true')
short_options.append("-v")
@@ -79,9 +103,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.need_handle:
print("error: -n option cannot be used with " +
"--base-allocation, --opt-mode or --uri",
file=sys.stderr)
@@ -109,30 +131,20 @@ def shell():
h = nbd.NBD()
h.set_handle_name("nbdsh")
- # Set other attributes in the handle.
- if args.base_allocation:
- h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)
- if args.opt_mode:
- h.set_opt_mode(True)
-
- # 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)
-
- # Run all -c snippets
+ # Run all snippets
# https://stackoverflow.com/a/11754346
d = dict(locals(), **globals())
+ do_snippet = {
+ "command": lambda arg: exec(arg, d, d),
+ "stdin": lambda arg: exec(sys.stdin.read(), d, d),
+ "base_allocation": lambda arg: h.add_meta_context(
+ nbd.CONTEXT_BASE_ALLOCATION),
+ "opt_mode": lambda arg: h.set_opt_mode(True),
+ "uri": lambda arg: h.connect_uri(arg),
+ }
try:
- for c in args.command:
- if c != '-':
- exec(c, d, d)
- else:
- exec(sys.stdin.read(), d, d)
+ for (act, arg) in args.snippets:
+ do_snippet[act](arg)
except nbd.Error as ex:
if nbd.NBD().get_debug():
traceback.print_exc()
@@ -142,7 +154,7 @@ def shell():
sys.exit(1)
# If there are no explicit -c or --command parameters, go interactive.
- if len(args.command) == 0:
+ if len(args.snippets) - args.need_handle == 0:
sys.ps1 = "nbd> "
code.interact(banner=make_banner(args), local=locals(),
exitmsg='')
@@ -165,7 +177,7 @@ def example(ex, desc): line("%-34s # %s" % (ex,
desc))
line("The ???nbd??? module has already been imported.")
blank()
example("h = nbd.NBD()", "Create a new handle.")
- if args.uri is None:
+ if False: # args.uri is None:
example('h.connect_tcp("remote", "10809")',
"Connect to a remote server.")
example("h.get_size()", "Get size of the remote disk.")
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))"')
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.38.1
Eric Blake
2022-Nov-04 21:18 UTC
[Libguestfs] [libnbd PATCH v2 3/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, 28 insertions(+), 9 deletions(-)
diff --git a/python/nbdsh.py b/python/nbdsh.py
index 0919c9ec..872b94fc 100644
--- a/python/nbdsh.py
+++ b/python/nbdsh.py
@@ -86,11 +86,13 @@ def __call__(self, parser, namespace, values,
option_string=None):
parser.add_argument('--connect', dest='uri',
action=SnippetAction,
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")
@@ -127,7 +129,10 @@ def __call__(self, parser, namespace, values,
option_string=None):
os.environ["LIBNBD_DEBUG"] = "1"
# Create the handle.
- if not args.n:
+ if args.n:
+ pass
+ else:
+ global h
h = nbd.NBD()
h.set_handle_name("nbdsh")
@@ -165,21 +170,35 @@ 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 False: # args.uri is None:
+ connect_hint = True
+ else:
+ global h
+ 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.38.1
Maybe Matching Threads
- [libnbd PATCH] nbdsh: Add -b option to simplify h.block_status
- [libnbd PATCH] nbdsh: Support -u as synonym for --connect
- [libnbd PATCH v2] nbdsh: Prefer --uri over --connect
- [libnbd PATCH] nbdsh: Start adding unit tests
- [libnbd PATCH] nbdsh: Add --opt-mode command line option