From 6a2b0aac8be655524ea223e32cac0395fcc9f975 Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Fri, 15 Apr 2022 12:08:37 +0100 Subject: [PATCH] ssh: Allow the remote file to be created This adds new parameters, create=(true|false), create-size=SIZE and create-mode=MODE to create and truncate the remote file. Reviewed-by: Laszlo Ersek (cherry picked from commit 0793f30b1071753532362b2ebf9cb8156a88c3c3) --- plugins/ssh/nbdkit-ssh-plugin.pod | 34 ++++++++- plugins/ssh/ssh.c | 112 +++++++++++++++++++++++++++--- tests/test-ssh.sh | 13 +++- 3 files changed, 146 insertions(+), 13 deletions(-) diff --git a/plugins/ssh/nbdkit-ssh-plugin.pod b/plugins/ssh/nbdkit-ssh-plugin.pod index 3f401c15..2bc2c4a7 100644 --- a/plugins/ssh/nbdkit-ssh-plugin.pod +++ b/plugins/ssh/nbdkit-ssh-plugin.pod @@ -5,8 +5,10 @@ nbdkit-ssh-plugin - access disk images over the SSH protocol =head1 SYNOPSIS nbdkit ssh host=HOST [path=]PATH - [compression=true] [config=CONFIG_FILE] [identity=FILENAME] - [known-hosts=FILENAME] [password=PASSWORD|-|+FILENAME] + [compression=true] [config=CONFIG_FILE] + [create=true] [create-mode=MODE] [create-size=SIZE] + [identity=FILENAME] [known-hosts=FILENAME] + [password=PASSWORD|-|+FILENAME] [port=PORT] [timeout=SECS] [user=USER] [verify-remote-host=false] @@ -62,6 +64,34 @@ The C parameter is optional. If it is I specified at all then F<~/.ssh/config> and F are both read. Missing or unreadable files are ignored. +=item B + +(nbdkit E 1.32) + +If set, the remote file will be created. The remote file is created +on the first NBD connection to nbdkit, not when nbdkit starts up. If +the file already exists, it will be replaced and any existing content +lost. + +If using this option, you must use C. C can +be used to control the permissions of the new file. + +=item BMODE + +(nbdkit E 1.32) + +If using C specify the default permissions of the new +remote file. You can use octal modes like C or +C. The default is C<0600>, ie. only readable and +writable by the remote user. + +=item BSIZE + +(nbdkit E 1.32) + +If using C, specify the virtual size of the new disk. +C can use modifiers like C<100M> etc. + =item BHOST Specify the name or IP address of the remote host. diff --git a/plugins/ssh/ssh.c b/plugins/ssh/ssh.c index 39d77e44..5e314cd7 100644 --- a/plugins/ssh/ssh.c +++ b/plugins/ssh/ssh.c @@ -44,6 +44,8 @@ #include #include +#include + #include #include #include @@ -51,6 +53,7 @@ #include #include "array-size.h" +#include "cleanup.h" #include "const-string-vector.h" #include "minmax.h" @@ -64,6 +67,9 @@ static const char *known_hosts = NULL; static const_string_vector identities = empty_vector; static uint32_t timeout = 0; static bool compression = false; +static bool create = false; +static int64_t create_size = -1; +static unsigned create_mode = S_IRUSR | S_IWUSR /* 0600 */; /* config can be: * NULL => parse options from default file @@ -167,6 +173,27 @@ ssh_config (const char *key, const char *value) return -1; compression = r; } + else if (strcmp (key, "create") == 0) { + r = nbdkit_parse_bool (value); + if (r == -1) + return -1; + create = r; + } + else if (strcmp (key, "create-size") == 0) { + create_size = nbdkit_parse_size (value); + if (create_size == -1) + return -1; + } + else if (strcmp (key, "create-mode") == 0) { + r = nbdkit_parse_unsigned (key, value, &create_mode); + if (r == -1) + return -1; + /* OpenSSH checks this too. */ + if (create_mode > 0777) { + nbdkit_error ("create-mode must be <= 0777"); + return -1; + } + } else { nbdkit_error ("unknown parameter '%s'", key); @@ -186,6 +213,13 @@ ssh_config_complete (void) return -1; } + /* If create=true, create-size must be supplied. */ + if (create && create_size == -1) { + nbdkit_error ("if using create=true, you must specify the size " + "of the new remote file using create-size=SIZE"); + return -1; + } + return 0; } @@ -200,7 +234,10 @@ ssh_config_complete (void) "identity= Prepend private key (identity) file.\n" \ "timeout=SECS Set SSH connection timeout.\n" \ "verify-remote-host=false Ignore known_hosts.\n" \ - "compression=true Enable compression." + "compression=true Enable compression.\n" \ + "create=true Create the remote file.\n" \ + "create-mode=MODE Set the permissions of the remote file.\n" \ + "create-size=SIZE Set the size of the remote file." /* Since we must simulate atomic pread and pwrite using seek + * read/write, calls on each handle must be serialized. @@ -329,6 +366,65 @@ authenticate (struct ssh_handle *h) return -1; } +/* This function opens or creates the remote file (depending on + * create=false|true). Parallel connections might call this function + * at the same time, and so we must hold a lock to ensure that the + * file is created at most once. + */ +static pthread_mutex_t create_lock = PTHREAD_MUTEX_INITIALIZER; + +static sftp_file +open_or_create_path (ssh_session session, sftp_session sftp, int readonly) +{ + ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&create_lock); + int access_type; + int r; + sftp_file file; + + access_type = readonly ? O_RDONLY : O_RDWR; + if (create) access_type |= O_CREAT | O_TRUNC; + + file = sftp_open (sftp, path, access_type, S_IRWXU); + if (!file) { + nbdkit_error ("cannot %s file for %s: %s", + create ? "create" : "open", + readonly ? "reading" : "writing", + ssh_get_error (session)); + return NULL; + } + + if (create) { + /* There's no sftp_truncate call. However OpenSSH lets you call + * SSH_FXP_SETSTAT + SSH_FILEXFER_ATTR_SIZE which invokes + * truncate(2) on the server. Libssh doesn't provide a binding + * for SSH_FXP_FSETSTAT so we have to pass the session + path. + */ + struct sftp_attributes_struct attrs = { + .flags = SSH_FILEXFER_ATTR_SIZE | + SSH_FILEXFER_ATTR_PERMISSIONS, + .size = create_size, + .permissions = create_mode, + }; + + r = sftp_setstat (sftp, path, &attrs); + if (r != SSH_OK) { + nbdkit_error ("setstat failed: %s", ssh_get_error (session)); + + /* Best-effort attempt to delete the remote file on failure. */ + r = sftp_unlink (sftp, path); + if (r != SSH_OK) + nbdkit_debug ("unlink failed: %s", ssh_get_error (session)); + + return NULL; + } + } + + /* On the next connection, don't create or truncate the file. */ + create = false; + + return file; +} + /* Create the per-connection handle. */ static void * ssh_open (int readonly) @@ -337,7 +433,6 @@ ssh_open (int readonly) const int set = 1; size_t i; int r; - int access_type; h = calloc (1, sizeof *h); if (h == NULL) { @@ -471,7 +566,7 @@ ssh_open (int readonly) if (authenticate (h) == -1) goto err; - /* Open the SFTP connection and file. */ + /* Open the SFTP connection. */ h->sftp = sftp_new (h->session); if (!h->sftp) { nbdkit_error ("failed to allocate sftp session: %s", @@ -484,14 +579,11 @@ ssh_open (int readonly) ssh_get_error (h->session)); goto err; } - access_type = readonly ? O_RDONLY : O_RDWR; - h->file = sftp_open (h->sftp, path, access_type, S_IRWXU); - if (!h->file) { - nbdkit_error ("cannot open file for %s: %s", - readonly ? "reading" : "writing", - ssh_get_error (h->session)); + + /* Open or create the remote file. */ + h->file = open_or_create_path (h->session, h->sftp, readonly); + if (!h->file) goto err; - } nbdkit_debug ("opened libssh handle"); diff --git a/tests/test-ssh.sh b/tests/test-ssh.sh index 6c0ce410..f04b4488 100755 --- a/tests/test-ssh.sh +++ b/tests/test-ssh.sh @@ -36,6 +36,7 @@ set -x requires test -f disk requires nbdcopy --version +requires stat --version # Check that ssh to localhost will work without any passwords or phrases. # @@ -48,7 +49,7 @@ then exit 77 fi -files="ssh.img" +files="ssh.img ssh2.img" rm -f $files cleanup_fn rm -f $files @@ -59,3 +60,13 @@ nbdkit -v -D ssh.log=2 -U - \ # The output should be identical. cmp disk ssh.img + +# Copy local file 'ssh.img' to newly created "remote" 'ssh2.img' +size="$(stat -c %s disk)" +nbdkit -v -D ssh.log=2 -U - \ + ssh host=localhost $PWD/ssh2.img \ + create=true create-size=$size \ + --run 'nbdcopy ssh.img "$uri"' + +# The output should be identical. +cmp disk ssh2.img -- 2.31.1