The ability to mark backups as protected broke the implicit assumption in vzdump that remove=1 and current number of backups being the limit (i.e. sum of all keep options) will result in a backup being removed. Introduce a new storage property 'max-protected-backups' to limit the number of protected backups per guest. Use 5 as a default value, as it should cover most use cases, while still not having too big of a potential overhead in many scenarios. For external plugins that do not return the backup subtype in list_volumes, all protected backups with the same ID will count towards the limit. An alternative would be to count the protected backups when pruning. While that would avoid the need for a new property, it would break the current semantics of protected backups being ignored for pruning. It also would be less flexible, e.g. for PBS, it can make sense to have both keep-all=1 and a limit for the number of protected snapshots on the PVE side. Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
267 lines
7.0 KiB
Perl
267 lines
7.0 KiB
Perl
package PVE::Storage::CephFSPlugin;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use IO::File;
|
|
use Net::IP;
|
|
use File::Path;
|
|
|
|
use PVE::CephConfig;
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
use PVE::ProcFSTools;
|
|
use PVE::Storage::Plugin;
|
|
use PVE::Systemd;
|
|
use PVE::Tools qw(run_command file_set_contents);
|
|
|
|
use base qw(PVE::Storage::Plugin);
|
|
|
|
sub cephfs_is_mounted {
|
|
my ($scfg, $storeid, $mountdata) = @_;
|
|
|
|
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
|
|
my $configfile = $cmd_option->{ceph_conf};
|
|
|
|
my $subdir = $scfg->{subdir} // '/';
|
|
my $mountpoint = $scfg->{path};
|
|
|
|
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
|
|
return $mountpoint if grep {
|
|
$_->[2] =~ m#^ceph|fuse\.ceph-fuse# &&
|
|
$_->[0] =~ m#\Q:$subdir\E$|^ceph-fuse$# &&
|
|
$_->[1] eq $mountpoint
|
|
} @$mountdata;
|
|
|
|
warn "A filesystem is already mounted on $mountpoint\n"
|
|
if grep { $_->[1] eq $mountpoint } @$mountdata;
|
|
|
|
return undef;
|
|
}
|
|
|
|
# FIXME: remove once it's possible to specify _netdev for fuse.ceph mounts
|
|
sub systemd_netmount {
|
|
my ($where, $type, $what, $opts) = @_;
|
|
|
|
# don't do default deps, systemd v241 generator produces ordering deps on both
|
|
# local-fs(-pre) and remote-fs(-pre) targets if we use the required _netdev
|
|
# option. Over three corners this gets us an ordering cycle on shutdown, which
|
|
# may make shutdown hang if the random cycle breaking hits the "wrong" unit to
|
|
# delete.
|
|
my $unit = <<"EOF";
|
|
[Unit]
|
|
Description=${where}
|
|
DefaultDependencies=no
|
|
Requires=system.slice
|
|
Wants=network-online.target
|
|
Before=umount.target remote-fs.target
|
|
After=systemd-journald.socket system.slice network.target -.mount remote-fs-pre.target network-online.target
|
|
Conflicts=umount.target
|
|
|
|
[Mount]
|
|
Where=${where}
|
|
What=${what}
|
|
Type=${type}
|
|
Options=${opts}
|
|
EOF
|
|
|
|
my $unit_fn = PVE::Systemd::escape_unit($where, 1) . ".mount";
|
|
my $unit_path = "/run/systemd/system/$unit_fn";
|
|
my $daemon_needs_reload = -e $unit_path;
|
|
|
|
file_set_contents($unit_path, $unit);
|
|
|
|
run_command(['systemctl', 'daemon-reload'], errmsg => "daemon-reload error")
|
|
if $daemon_needs_reload;
|
|
run_command(['systemctl', 'start', $unit_fn], errmsg => "mount error");
|
|
|
|
}
|
|
|
|
sub cephfs_mount {
|
|
my ($scfg, $storeid) = @_;
|
|
|
|
my $mountpoint = $scfg->{path};
|
|
my $subdir = $scfg->{subdir} // '/';
|
|
|
|
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
|
|
my $configfile = $cmd_option->{ceph_conf};
|
|
my $secretfile = $cmd_option->{keyring};
|
|
my $server = $cmd_option->{mon_host} // PVE::CephConfig::get_monaddr_list($configfile);
|
|
my $type = 'ceph';
|
|
my $fs_name = $scfg->{'fs-name'};
|
|
|
|
my @opts = ();
|
|
if ($scfg->{fuse}) {
|
|
$type = 'fuse.ceph';
|
|
push @opts, "ceph.id=$cmd_option->{userid}";
|
|
push @opts, "ceph.keyfile=$secretfile" if defined($secretfile);
|
|
push @opts, "ceph.conf=$configfile" if defined($configfile);
|
|
push @opts, "ceph.client_fs=$fs_name" if defined($fs_name);
|
|
} else {
|
|
push @opts, "name=$cmd_option->{userid}";
|
|
push @opts, "secretfile=$secretfile" if defined($secretfile);
|
|
push @opts, "conf=$configfile" if defined($configfile);
|
|
push @opts, "fs=$fs_name" if defined($fs_name);
|
|
}
|
|
|
|
push @opts, $scfg->{options} if $scfg->{options};
|
|
|
|
systemd_netmount($mountpoint, $type, "$server:$subdir", join(',', @opts));
|
|
}
|
|
|
|
# Configuration
|
|
|
|
sub type {
|
|
return 'cephfs';
|
|
}
|
|
|
|
sub plugindata {
|
|
return {
|
|
content => [ { vztmpl => 1, iso => 1, backup => 1, snippets => 1},
|
|
{ backup => 1 }],
|
|
};
|
|
}
|
|
|
|
sub properties {
|
|
return {
|
|
fuse => {
|
|
description => "Mount CephFS through FUSE.",
|
|
type => 'boolean',
|
|
},
|
|
subdir => {
|
|
description => "Subdir to mount.",
|
|
type => 'string', format => 'pve-storage-path',
|
|
},
|
|
'fs-name' => {
|
|
description => "The Ceph filesystem name.",
|
|
type => 'string', format => 'pve-configid',
|
|
},
|
|
};
|
|
}
|
|
|
|
sub options {
|
|
return {
|
|
path => { fixed => 1 },
|
|
monhost => { optional => 1},
|
|
nodes => { optional => 1 },
|
|
subdir => { optional => 1 },
|
|
disable => { optional => 1 },
|
|
options => { optional => 1 },
|
|
username => { optional => 1 },
|
|
content => { optional => 1 },
|
|
format => { optional => 1 },
|
|
mkdir => { optional => 1 },
|
|
fuse => { optional => 1 },
|
|
bwlimit => { optional => 1 },
|
|
maxfiles => { optional => 1 },
|
|
keyring => { optional => 1 },
|
|
'prune-backups' => { optional => 1 },
|
|
'max-protected-backups' => { optional => 1 },
|
|
'fs-name' => { optional => 1 },
|
|
};
|
|
}
|
|
|
|
sub check_config {
|
|
my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
|
|
|
|
$config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
|
|
|
|
return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
|
|
}
|
|
|
|
# Storage implementation
|
|
|
|
sub on_add_hook {
|
|
my ($class, $storeid, $scfg, %param) = @_;
|
|
|
|
my $secret = $param{keyring} if defined $param{keyring} // undef;
|
|
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $secret);
|
|
|
|
return;
|
|
}
|
|
|
|
sub on_update_hook {
|
|
my ($class, $storeid, $scfg, %param) = @_;
|
|
|
|
if (exists($param{keyring})) {
|
|
if (defined($param{keyring})) {
|
|
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
|
|
} else {
|
|
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub on_delete_hook {
|
|
my ($class, $storeid, $scfg) = @_;
|
|
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
|
|
return;
|
|
}
|
|
|
|
sub status {
|
|
my ($class, $storeid, $scfg, $cache) = @_;
|
|
|
|
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
|
|
|
|
return undef if !cephfs_is_mounted($scfg, $storeid, $cache->{mountdata});
|
|
|
|
return $class->SUPER::status($storeid, $scfg, $cache);
|
|
}
|
|
|
|
sub activate_storage {
|
|
my ($class, $storeid, $scfg, $cache) = @_;
|
|
|
|
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
|
|
|
|
# NOTE: mkpath may hang if storage is mounted but not reachable
|
|
if (!cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
|
|
my $path = $scfg->{path};
|
|
|
|
mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
|
|
|
|
die "unable to activate storage '$storeid' - " .
|
|
"directory '$path' does not exist\n" if ! -d $path;
|
|
|
|
cephfs_mount($scfg, $storeid);
|
|
}
|
|
|
|
$class->SUPER::activate_storage($storeid, $scfg, $cache);
|
|
}
|
|
|
|
sub deactivate_storage {
|
|
my ($class, $storeid, $scfg, $cache) = @_;
|
|
|
|
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
|
|
|
|
my $path = $scfg->{path};
|
|
|
|
if (cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
|
|
run_command(['/bin/umount', $path], errmsg => 'umount error');
|
|
}
|
|
}
|
|
|
|
# FIXME remove on the next APIAGE reset.
|
|
# Deprecated, use get_volume_attribute instead.
|
|
sub get_volume_notes {
|
|
my $class = shift;
|
|
PVE::Storage::DirPlugin::get_volume_notes($class, @_);
|
|
}
|
|
|
|
# FIXME remove on the next APIAGE reset.
|
|
# Deprecated, use update_volume_attribute instead.
|
|
sub update_volume_notes {
|
|
my $class = shift;
|
|
PVE::Storage::DirPlugin::update_volume_notes($class, @_);
|
|
}
|
|
|
|
sub get_volume_attribute {
|
|
return PVE::Storage::DirPlugin::get_volume_attribute(@_);
|
|
}
|
|
|
|
sub update_volume_attribute {
|
|
return PVE::Storage::DirPlugin::update_volume_attribute(@_);
|
|
}
|
|
|
|
1;
|