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>
222 lines
5.9 KiB
Perl
222 lines
5.9 KiB
Perl
package PVE::Storage::DirPlugin;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Cwd;
|
|
use File::Path;
|
|
use IO::File;
|
|
use POSIX;
|
|
|
|
use PVE::Storage::Plugin;
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
|
|
use base qw(PVE::Storage::Plugin);
|
|
|
|
# Configuration
|
|
|
|
sub type {
|
|
return 'dir';
|
|
}
|
|
|
|
sub plugindata {
|
|
return {
|
|
content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1, none => 1 },
|
|
{ images => 1, rootdir => 1 }],
|
|
format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 } , 'raw' ],
|
|
};
|
|
}
|
|
|
|
sub properties {
|
|
return {
|
|
path => {
|
|
description => "File system path.",
|
|
type => 'string', format => 'pve-storage-path',
|
|
},
|
|
mkdir => {
|
|
description => "Create the directory if it doesn't exist.",
|
|
type => 'boolean',
|
|
default => 'yes',
|
|
},
|
|
is_mountpoint => {
|
|
description =>
|
|
"Assume the given path is an externally managed mountpoint " .
|
|
"and consider the storage offline if it is not mounted. ".
|
|
"Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.",
|
|
type => 'string',
|
|
default => 'no',
|
|
},
|
|
bwlimit => get_standard_option('bwlimit'),
|
|
};
|
|
}
|
|
|
|
sub options {
|
|
return {
|
|
path => { fixed => 1 },
|
|
nodes => { optional => 1 },
|
|
shared => { optional => 1 },
|
|
disable => { optional => 1 },
|
|
maxfiles => { optional => 1 },
|
|
'prune-backups' => { optional => 1 },
|
|
'max-protected-backups' => { optional => 1 },
|
|
content => { optional => 1 },
|
|
format => { optional => 1 },
|
|
mkdir => { optional => 1 },
|
|
is_mountpoint => { optional => 1 },
|
|
bwlimit => { optional => 1 },
|
|
preallocation => { optional => 1 },
|
|
};
|
|
}
|
|
|
|
# Storage implementation
|
|
#
|
|
|
|
# NOTE: should ProcFSTools::is_mounted accept an optional cache like this?
|
|
sub path_is_mounted {
|
|
my ($mountpoint, $mountdata) = @_;
|
|
|
|
$mountpoint = Cwd::realpath($mountpoint); # symlinks
|
|
return 0 if !defined($mountpoint); # path does not exist
|
|
|
|
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
|
|
return 1 if grep { $_->[1] eq $mountpoint } @$mountdata;
|
|
return undef;
|
|
}
|
|
|
|
sub parse_is_mountpoint {
|
|
my ($scfg) = @_;
|
|
my $is_mp = $scfg->{is_mountpoint};
|
|
return undef if !defined $is_mp;
|
|
if (defined(my $bool = PVE::JSONSchema::parse_boolean($is_mp))) {
|
|
return $bool ? $scfg->{path} : undef;
|
|
}
|
|
return $is_mp; # contains a path
|
|
}
|
|
|
|
# FIXME remove on the next APIAGE reset.
|
|
# Deprecated, use get_volume_attribute instead.
|
|
sub get_volume_notes {
|
|
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
|
|
|
|
my ($vtype) = $class->parse_volname($volname);
|
|
return if $vtype ne 'backup';
|
|
|
|
my $path = $class->filesystem_path($scfg, $volname);
|
|
$path .= $class->SUPER::NOTES_EXT;
|
|
|
|
return PVE::Tools::file_get_contents($path) if -f $path;
|
|
|
|
return '';
|
|
}
|
|
|
|
# FIXME remove on the next APIAGE reset.
|
|
# Deprecated, use update_volume_attribute instead.
|
|
sub update_volume_notes {
|
|
my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
|
|
|
|
my ($vtype) = $class->parse_volname($volname);
|
|
die "only backups can have notes\n" if $vtype ne 'backup';
|
|
|
|
my $path = $class->filesystem_path($scfg, $volname);
|
|
$path .= $class->SUPER::NOTES_EXT;
|
|
|
|
if (defined($notes) && $notes ne '') {
|
|
PVE::Tools::file_set_contents($path, $notes);
|
|
} else {
|
|
unlink $path or $! == ENOENT or die "could not delete notes - $!\n";
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub get_volume_attribute {
|
|
my ($class, $scfg, $storeid, $volname, $attribute) = @_;
|
|
|
|
if ($attribute eq 'notes') {
|
|
return $class->get_volume_notes($scfg, $storeid, $volname);
|
|
}
|
|
|
|
my ($vtype) = $class->parse_volname($volname);
|
|
return if $vtype ne 'backup';
|
|
|
|
if ($attribute eq 'protected') {
|
|
my $path = $class->filesystem_path($scfg, $volname);
|
|
return -e PVE::Storage::protection_file_path($path) ? 1 : 0;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub update_volume_attribute {
|
|
my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
|
|
|
|
if ($attribute eq 'notes') {
|
|
return $class->update_volume_notes($scfg, $storeid, $volname, $value);
|
|
}
|
|
|
|
my ($vtype) = $class->parse_volname($volname);
|
|
die "only backups support attribute '$attribute'\n" if $vtype ne 'backup';
|
|
|
|
if ($attribute eq 'protected') {
|
|
my $path = $class->filesystem_path($scfg, $volname);
|
|
my $protection_path = PVE::Storage::protection_file_path($path);
|
|
|
|
return if !((-e $protection_path) xor $value); # protection status already correct
|
|
|
|
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";
|
|
}
|
|
|
|
sub status {
|
|
my ($class, $storeid, $scfg, $cache) = @_;
|
|
|
|
if (defined(my $mp = parse_is_mountpoint($scfg))) {
|
|
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
|
|
if !$cache->{mountdata};
|
|
|
|
return undef if !path_is_mounted($mp, $cache->{mountdata});
|
|
}
|
|
|
|
return $class->SUPER::status($storeid, $scfg, $cache);
|
|
}
|
|
|
|
|
|
sub activate_storage {
|
|
my ($class, $storeid, $scfg, $cache) = @_;
|
|
|
|
my $path = $scfg->{path};
|
|
if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
|
|
mkpath $path;
|
|
}
|
|
|
|
my $mp = parse_is_mountpoint($scfg);
|
|
if (defined($mp) && !path_is_mounted($mp, $cache->{mountdata})) {
|
|
die "unable to activate storage '$storeid' - " .
|
|
"directory is expected to be a mount point but is not mounted: '$mp'\n";
|
|
}
|
|
|
|
$class->SUPER::activate_storage($storeid, $scfg, $cache);
|
|
}
|
|
|
|
sub check_config {
|
|
my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
|
|
my $opts = PVE::SectionConfig::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
|
|
return $opts if !$create;
|
|
if ($opts->{path} !~ m@^/[-/a-zA-Z0-9_.]+$@) {
|
|
die "illegal path for directory storage: $opts->{path}\n";
|
|
}
|
|
return $opts;
|
|
}
|
|
|
|
1;
|