From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 153861FF183 for ; Wed, 2 Jul 2025 22:13:57 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A1F52362BF; Wed, 2 Jul 2025 22:14:34 +0200 (CEST) Message-ID: <6016e664-5c81-4a32-a749-db807d4e56b0@proxmox.com> Date: Wed, 2 Jul 2025 22:14:29 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Beta To: Proxmox VE development discussion , Max Carrara References: <20250416124735.320256-1-m.carrara@proxmox.com> <20250416124735.320256-2-m.carrara@proxmox.com> Content-Language: de-AT, en-US From: Thomas Lamprecht Autocrypt: addr=t.lamprecht@proxmox.com; keydata= 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 DrEx In-Reply-To: <20250416124735.320256-2-m.carrara@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL -0.151 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.232 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [sshfsplugin.pm, proxmox.com] Subject: Re: [pve-devel] [PATCH v1 pve-storage 1/2] example: sshfs plugin: add custom storage plugin for SSHFS X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Am 16.04.25 um 14:47 schrieb Max Carrara: > This commit adds an example implementation of a custom storage plugin > that uses SSHFS [0] as the underlying filesystem. > > The implementation is very similar to that of the NFS plugin; as a > prerequisite, it is currently necessary to use pubkey auth and have > the host's root user's public key deployed to the remote host like so: > > ssh-copy-id -i ~/.ssh/id_my_private_key \ > -o UserKnownHostsFile=/etc/pve/priv/known_hosts [USER]@[HOST] > > Then, the storage can be added as follows: > > pvesm add sshfs [STOREID] \ > --username [USER] \ > --server [HOST] \ > --sshfs-remote-path [ABS PATH ON REMOTE] \ > --path /mnt/path/to/storage \ > --sshfs-private-key ~/.ssh/id_my_private_key > > If the host is part of a cluster, other nodes may connect to the > remote without any additional setup required. This is because we copy > the private key to `/etc/pve/priv/storage/$KEYNAME.key` and use the > cluster-wide `/etc/pve/priv/known_hosts` file. Also mark each SSHFS > storage as `shared` by default in order to make use of this. > > Note: Because there's currently no way to officially and permanently > mark a storage as shared (like some built-in plugins [1]) set > `$scfg->{shared} = 1;` in `on_add_hook`. This has almost the same > effect as modifying `@PVE::Storage::Plugin::SHARED_STORAGE` directly, > except that the `shared` is written to `/etc/pve/storage.cfg`. > > [0]: https://github.com/libfuse/sshfs > [1]: https://git.proxmox.com/?p=pve-storage.git;a=blob;f=src/PVE/Storage/Plugin.pm;h=4e16420f667f196e8eb99ae7c9f3f1d3e13791fb;hb=refs/heads/master#l37 not a fully complete review but some comments inline, and there is now also a new repo that would be a great fit for hosting this example: https://git.proxmox.com/?p=pve-storage-plugin-examples.git;a=summary > > Signed-off-by: Max Carrara > --- > Changes rfc-v1 --> v1: > * rework most of the plugin > * cease to call methods of DirPlugin (plugins should be isolated; > we don't want to encourage third-party devs to do that) > * handle SSH private key as sensitive property and place it on pmxcfs > * make storage shared > * manually implement attribute handling ("notes", "protected" for > backups) > > .../lib/PVE/Storage/Custom/SSHFSPlugin.pm | 398 ++++++++++++++++++ > 1 file changed, 398 insertions(+) > create mode 100644 example/sshfs-plugin/lib/PVE/Storage/Custom/SSHFSPlugin.pm > > diff --git a/example/sshfs-plugin/lib/PVE/Storage/Custom/SSHFSPlugin.pm b/example/sshfs-plugin/lib/PVE/Storage/Custom/SSHFSPlugin.pm > new file mode 100644 > index 0000000..75b29c1 > --- /dev/null > +++ b/example/sshfs-plugin/lib/PVE/Storage/Custom/SSHFSPlugin.pm > @@ -0,0 +1,398 @@ > +package PVE::Storage::Custom::SSHFSPlugin; > + > +use strict; > +use warnings; > + > +use feature 'signatures'; FYI: you can replace above four lines with: use v5.36; As that version implies strict, warnings and enables the signatures feature. > + > +use Cwd qw(); > +use Encode qw(decode encode); > +use File::Path qw(make_path); > +use File::Basename qw(dirname); > +use IO::File; > +use POSIX; The POSIX module exports a lot of stuff by default, even overriding some existing perl symbols IIRC, while most of the time one just needs some error constants or format time printing or the like, so explicitly importing what one uses is almost always the safer choice. There are some export groups for sets of exports, so that one can e.g. import qw(:errno_h) instead of every error number constant explicitly. > + > +use PVE::ProcFSTools; > +use PVE::Tools qw( > + file_copy > + file_get_contents > + file_set_contents > + run_command > +); > + > +use base qw(PVE::Storage::Plugin); > + > +my $CLUSTER_KNOWN_HOSTS = "/etc/pve/priv/known_hosts"; For intra-cluster ssh this shared known_host file is being deprecated in favor of using a per-node file, i.e. /etc/pve/nodes/NODENAME/ssh_known_hosts, it might be better to not re-use that here and rather use a dedicated file for this storage or directly encode it in the config entry (either passed by the user or TOFU on storage addition). > + > +# Plugin Definition > + > +sub properties { > + return { > + 'sshfs-remote-path' => { > + description => "Path on the remote filesystem used for SSHFS. Must be absolute.", > + type => 'string', > + format => 'pve-storage-path', > + }, > + 'sshfs-private-key' => { > + description => "Path to the private key to use for SSHFS.", > + type => 'string', > + format => 'pve-storage-path', > + } We normally do not add the storage type as prefix for properties. FWIW, we could treat the private-key like a password and manage it ourselves > + }; > +} > + > +sub options { > + return { > + disable => { optional => 1 }, > + path => { fixed => 1 }, > + 'create-base-path' => { optional => 1 }, > + content => { optional => 1 }, > + 'create-subdirs' => { optional => 1 }, > + 'content-dirs' => { optional => 1 }, > + 'prune-backups' => { optional => 1 }, > + 'max-protected-backups' => { optional => 1 }, > + format => { optional => 1 }, > + bwlimit => { optional => 1 }, > + preallocation => { optional => 1 }, > + nodes => { optional => 1 }, > + shared => { optional => 1 }, > + > + # SSHFS Options > + username => {}, > + server => {}, > + 'sshfs-remote-path' => {}, > + port => { optional => 1 }, > + 'sshfs-private-key' => { optional => 1 }, > + }; > +} > + > +# SSHFS Helpers > + > +my sub sshfs_remote_from_config: prototype($) ($scfg) { > + my ($user, $host, $remote_path) = $scfg->@{qw(username server sshfs-remote-path)}; > + return "${user}\@${host}:${remote_path}"; > +} > + > +my sub sshfs_private_key_path: prototype($) ($storeid) { > + return "/etc/pve/priv/storage/$storeid.key"; > +} > + > +my sub sshfs_common_ssh_opts: prototype($) ($storeid) { > + my $private_key_path = sshfs_private_key_path($storeid); > + > + my @common_opts = ( > + '-o', "UserKnownHostsFile=${CLUSTER_KNOWN_HOSTS}", > + '-o', 'GlobalKnownHostsFile=none', > + '-o', "IdentityFile=${private_key_path}", > + ); > + > + return @common_opts; > +} > + > +my sub sshfs_set_private_key: prototype($$) ($storeid, $src_key_path) { > + die "path of private key file not specified" if !defined($src_key_path); > + die "path of private key file does not exist" if ! -e $src_key_path; > + die "path of private key file does not point to a file" if ! -f $src_key_path; I mean, fine as this is more for better UX, but not really safe as there is a TOCTOU race here. If we really want to make it safe(r) we probably should not allow copying arbitrary files from the system and allow either passing a existing key to use as value, not the best UX, but it's not _that_ worse then the status quo IMO. > + > + my $dest_key_path = sshfs_private_key_path($storeid); > + > + my $dest_key_parent_dir = dirname($dest_key_path); > + if (! -e $dest_key_parent_dir) { > + make_path($dest_key_parent_dir, { chmod => 0700 }); chmod is useless for pmxcfs, as it controls those file attributes itself. While as is it shouldn't hurt, it might be confusing for devs basing off this plugin and wanting some other mode (on another path) and then wonder why this suddenly errors out; so maybe dropping the chmod and adding a comment that the file will reside below /etc/pve/priv, of which pmxcfs will always set the mode to 0700 for. btw., does make_path even dies explicitly, or would you need to check the return value? > + } else { > + die "'$dest_key_path' already exists" if -e $dest_key_path; Being able to override this on add might be nice though? And FWIW, there is some code deduplication with writing this file in the on_update_hook that might be avoidable I think. > + } > + > + file_copy($src_key_path, $dest_key_path, undef, 600); > + > + return undef; > +} > + > +my sub sshfs_remove_private_key: prototype($) ($storeid) { > + my $key_path = sshfs_private_key_path($storeid); > + unlink($key_path) or $! == ENOENT or die "failed to remove private key '$key_path' - $!\n"; > + > + return undef; > +} > + > +my sub sshfs_is_mounted: prototype($) ($scfg) { > + my $remote = sshfs_remote_from_config($scfg); > + > + my $mountpoint = Cwd::realpath($scfg->{path}); # Resolve symlinks > + return 0 if !defined($mountpoint); > + > + my $mountdata = PVE::ProcFSTools::parse_proc_mounts(); > + > + my $has_found_mountpoint = grep { > + $_->[0] =~ m|^\Q${remote}\E$| > + && $_->[1] eq $mountpoint > + && $_->[2] eq 'fuse.sshfs' > + } $mountdata->@*; > + > + return $has_found_mountpoint != 0; > +} > + > +my sub sshfs_mount: prototype($$) ($scfg, $storeid) { > + my $remote = sshfs_remote_from_config($scfg); > + my ($port, $mountpoint) = $scfg->@{qw(port path)}; > + > + my @common_opts = sshfs_common_ssh_opts($storeid); > + my $cmd = [ > + '/usr/bin/sshfs', @common_opts, > + '-o', 'noatime', > + ]; > + > + push($cmd->@*, '-p', $port) if $port; > + push($cmd->@*, $remote, $mountpoint); > + > + eval { > + run_command( > + $cmd, > + timeout => 10, > + errfunc => sub { warn "$_[0]\n"; }, > + ); > + }; > + if (my $err = $@) { > + die "failed to mount SSHFS storage '$remote' at '$mountpoint': $@\n"; > + } > + > + die "SSHFS storage '$remote' not mounted at '$mountpoint' despite reported success\n" > + if ! sshfs_is_mounted($scfg); > + > + return; > +} > + > +my sub sshfs_umount: prototype($) ($scfg) { > + my $mountpoint = $scfg->{path}; > + > + my $cmd = ['/usr/bin/umount', $mountpoint]; > + > + eval { > + run_command( > + $cmd, > + timeout => 10, > + errfunc => sub { warn "$_[0]\n"; }, > + ); > + }; > + if (my $err = $@) { > + die "failed to unmount SSHFS at '$mountpoint': $err\n"; > + } > + > + return; > +} > + > +# Storage Implementation > + > +sub on_add_hook ($class, $storeid, $scfg, %sensitive) { > + $scfg->{shared} = 1; # mark SSHFS storages as shared by default > + > + eval { > + my $src_key_path = $sensitive{'sshfs-private-key'}; > + sshfs_set_private_key($storeid, $src_key_path); > + }; > + die "error while adding SSHFS storage '${storeid}': $@\n" if $@; > + > + return undef; > +} > + > +sub on_update_hook ($class, $storeid, $scfg, %sensitive) { > + return undef if !exists($sensitive{'sshfs-private-key'}); > + > + my $src_key_path = $sensitive{'sshfs-private-key'}; > + > + if (!defined($src_key_path)) { > + warn "removing private key for SSHFS storage '${storeid}'"; > + warn "the storage might not be mountable without a private key!"; > + > + eval { sshfs_remove_private_key($storeid); }; > + die $@ if $@; > + > + return undef; > + } > + > + my $dest_key_path = sshfs_private_key_path($storeid); > + my $dest_key_path_tmp = "${dest_key_path}.old"; > + > + file_copy($dest_key_path, $dest_key_path_tmp) if -e $dest_key_path; > + > + eval { file_copy($src_key_path, $dest_key_path, undef, 600); }; > + > + if (my $err = $@) { > + if (-e $dest_key_path_tmp) { > + warn "attempting to restore previous private key for storage '${storeid}'\n"; > + eval { file_copy($dest_key_path_tmp, $dest_key_path, undef, 600); }; > + warn "$@\n" if $@; > + > + unlink $dest_key_path_tmp; > + } > + > + die "failed to set private key for SSHFS storage '${storeid}': $err\n"; > + } > + > + unlink $dest_key_path_tmp; > + > + return undef; > +} > + > +sub on_delete_hook ($class, $storeid, $scfg) { > + eval { sshfs_remove_private_key($storeid); }; > + warn $@ if $@; > + > + eval { sshfs_umount($scfg) if sshfs_is_mounted($scfg); }; > + warn $@ if $@; > + > + return undef; > +} > + > +sub check_connection ($class, $storeid, $scfg) { > + my ($user, $host, $port) = $scfg->@{qw(username server port)}; > + > + my @common_opts = sshfs_common_ssh_opts($storeid); > + my $cmd = [ > + '/usr/bin/ssh', > + '-T', > + @common_opts, > + '-o', 'BatchMode=yes', > + '-o', 'ConnectTimeout=5', > + ]; > + > + push($cmd->@*, "-p", $port) if $port; > + push($cmd->@*, "${user}\@${host}", 'exit 0'); > + > + eval { > + run_command( > + $cmd, > + timeout => 10, > + errfunc => sub { warn "$_[0]\n"; }, > + ); > + }; > + if (my $err = $@) { > + warn "$err"; > + return 0; > + } > + > + return 1; > +} > + > +sub activate_storage ($class, $storeid, $scfg, $cache) { > + my $mountpoint = $scfg->{path}; > + > + if (!sshfs_is_mounted($scfg)) { > + if ($scfg->{'create-base-path'} // 1) { > + make_path($mountpoint); > + } > + > + die "unable to activate storage '$storeid' - directory '$mountpoint' does not exist\n" > + if ! -d $mountpoint; > + > + sshfs_mount($scfg, $storeid); > + } > + > + $class->SUPER::activate_storage($storeid, $scfg, $cache); > + return; > +} > + > +sub get_volume_attribute ($class, $scfg, $storeid, $volname, $attribute) { > + my ($vtype) = $class->parse_volname($volname); > + return if $vtype ne 'backup'; > + > + my $volume_path = PVE::Storage::Plugin->filesystem_path($scfg, $volname); > + > + if ($attribute eq 'notes') { > + my $notes_path = $volume_path . $class->SUPER::NOTES_EXT; > + > + if (-f $notes_path) { > + my $notes = file_get_contents($notes_path); > + return eval { decode('UTF-8', $notes, 1) } // $notes; > + } > + > + return ""; > + } > + > + if ($attribute eq 'protected') { > + return -e PVE::Storage::protection_file_path($volume_path) ? 1 : 0; > + } > + > + return; > +} > + > +sub update_volume_attribute ($class, $scfg, $storeid, $volname, $attribute, $value) { > + my ($vtype, $name) = $class->parse_volname($volname); > + die "only backups support attribute '$attribute'\n" if $vtype ne 'backup'; > + > + my $volume_path = PVE::Storage::Plugin->filesystem_path($scfg, $volname); > + > + if ($attribute eq 'notes') { > + my $notes_path = $volume_path . $class->SUPER::NOTES_EXT; > + > + if (defined($value)) { > + my $encoded_notes = encode('UTF-8', $value); > + file_set_contents($notes_path, $encoded_notes); > + } else { > + unlink $notes_path or $! == ENOENT or die "could not delete notes - $!\n"; > + } > + > + return; > + } > + > + if ($attribute eq 'protected') { > + my $protection_path = PVE::Storage::protection_file_path($volume_path); > + > + # Protection already set or unset > + return if !((-e $protection_path) xor $value); > + > + if ($value) { > + my $fh = IO::File->new($protection_path, O_CREAT, 0644) > + or die "unable to create protection file '$protection_path' - $!\n"; > + close($fh); > + } else { > + unlink $protection_path or $! == ENOENT > + or die "could not delete protection file '$protection_path' - $!\n"; > + } > + > + return; > + } > + > + die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n"; > +} > + > +1; _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel