'pbs',
);
+our $QCOW2_PREALLOCATION = {
+ off => 1,
+ metadata => 1,
+ falloc => 1,
+ full => 1,
+};
+
+our $RAW_PREALLOCATION = {
+ off => 1,
+ falloc => 1,
+ full => 1,
+};
+
our $MAX_VOLUMES_PER_GUEST = 1024;
cfs_register_file ('storage.cfg',
sub validate_prune_backups {
my ($prune_backups) = @_;
- my $keep_all = delete $prune_backups->{'keep-all'};
+ my $res = { $prune_backups->%* };
- if (!scalar(grep {$_ > 0} values %{$prune_backups})) {
- $prune_backups = { 'keep-all' => 1 };
+ my $keep_all = delete $res->{'keep-all'};
+
+ if (scalar(grep { $_ > 0 } values %{$res}) == 0) {
+ $res = { 'keep-all' => 1 };
} elsif ($keep_all) {
die "keep-all cannot be set together with other options.\n";
}
- return $prune_backups;
+ return $res;
}
register_standard_option('prune-backups', {
description => "The retention options with shorter intervals are processed first " .
optional => 1,
},
'prune-backups' => get_standard_option('prune-backups'),
+ 'max-protected-backups' => {
+ description => "Maximal number of protected backups per guest. Use '-1' for unlimited.",
+ type => 'integer',
+ minimum => -1,
+ optional => 1,
+ default => "Unlimited for users with Datastore.Allocate privilege, 5 for other users",
+ },
shared => {
description => "Mark storage as shared.",
type => 'boolean',
type => 'string', format => 'pve-storage-format',
optional => 1,
},
+ preallocation => {
+ description => "Preallocation mode for raw and qcow2 images. " .
+ "Using 'metadata' on raw images results in preallocation=off.",
+ type => 'string', enum => ['off', 'metadata', 'falloc', 'full'],
+ default => 'metadata',
+ optional => 1,
+ },
},
};
return $cfg;
}
+sub preallocation_cmd_option {
+ my ($scfg, $fmt) = @_;
+
+ my $prealloc = $scfg->{preallocation};
+
+ if ($fmt eq 'qcow2') {
+ $prealloc = $prealloc // 'metadata';
+
+ die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
+ if !$QCOW2_PREALLOCATION->{$prealloc};
+
+ return "preallocation=$prealloc";
+ } elsif ($fmt eq 'raw') {
+ $prealloc = $prealloc // 'off';
+ $prealloc = 'off' if $prealloc eq 'metadata';
+
+ die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
+ if !$RAW_PREALLOCATION->{$prealloc};
+
+ return "preallocation=$prealloc";
+ }
+
+ return;
+}
+
# Storage implementation
# called during addition of storage (before the new storage config got written)
my ($vmid, $name) = ($1, $2);
my (undef, $format, $isBase) = parse_name_dir($name);
return ('images', $name, $vmid, undef, undef, $isBase, $format);
- } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::iso_extension_re)$!) {
+ } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!) {
return ('iso', $1);
- } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::vztmpl_extension_re)$!) {
+ } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
return ('vztmpl', $1);
} elsif ($volname =~ m!^rootdir/(\d+)$!) {
return ('rootdir', $1, $1);
- } elsif ($volname =~ m!^backup/([^/]+(?:\.(?:tgz|(?:(?:tar|vma)(?:\.(?:${\COMPRESSOR_RE}))?))))$!) {
+ } elsif ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
my $fn = $1;
if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) {
return ('backup', $fn, $2);
} else {
my $cmd = ['/usr/bin/qemu-img', 'create'];
- push @$cmd, '-o', 'preallocation=metadata' if $fmt eq 'qcow2';
+ my $prealloc_opt = preallocation_cmd_option($scfg, $fmt);
+ push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
push @$cmd, '-f', $fmt, $path, "${size}K";
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
+ die "cannot remove protected volume '$volname' on '$storeid'\n"
+ if $class->get_volume_attribute($scfg, $storeid, $volname, 'protected');
+
my $path = $class->filesystem_path($scfg, $volname);
if ($isBase) {
my ($size, $format, $used, $parent) = $info->@{qw(virtual-size format actual-size backing-filename)};
($size) = ($size =~ /^(\d+)$/) or die "size '$size' not an integer\n"; # untaint
+ # coerce back from string
+ $size = int($size);
($used) = ($used =~ /^(\d+)$/) or die "used '$used' not an integer\n"; # untaint
+ # coerce back from string
+ $used = int($used);
($format) = ($format =~ /^(\S+)$/) or die "format '$format' includes whitespace\n"; # untaint
if (defined($parent)) {
($parent) = ($parent =~ /^(\S+)$/) or die "parent '$parent' includes whitespace\n"; # untaint
return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size;
}
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
die "volume notes are not supported for $class";
}
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
die "volume notes are not supported for $class";
}
+# Returns undef if the attribute is not supported for the volume.
+# Should die if there is an error fetching the attribute.
+# Possible attributes:
+# notes - user-provided comments/notes.
+# protected - not to be removed by free_image, and for backups, ignored when pruning.
+sub get_volume_attribute {
+ my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+
+ if ($attribute eq 'notes') {
+ my $notes = eval { $class->get_volume_notes($scfg, $storeid, $volname); };
+ if (my $err = $@) {
+ return if $err =~ m/^volume notes are not supported/;
+ die $err;
+ }
+ return $notes;
+ }
+
+ return;
+}
+
+# Dies if the attribute is not supported for the volume.
+sub update_volume_attribute {
+ my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+
+ if ($attribute eq 'notes') {
+ $class->update_volume_notes($scfg, $storeid, $volname, $value);
+ }
+
+ die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
+}
+
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my $path = $class->filesystem_path($scfg, $volname);
return undef;
}
+# Asserts that a rollback to $snap on $volname is possible.
+# If certain snapshots are preventing the rollback and $blockers is an array
+# reference, the snapshot names can be pushed onto $blockers prior to dying.
sub volume_rollback_is_possible {
- my ($class, $scfg, $storeid, $volname, $snap) = @_;
+ my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
return 1;
}
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
my $features = {
- snapshot => { current => { qcow2 => 1}, snap => { qcow2 => 1} },
- clone => { base => {qcow2 => 1, raw => 1, vmdk => 1} },
- template => { current => {qcow2 => 1, raw => 1, vmdk => 1, subvol => 1} },
- copy => { base => {qcow2 => 1, raw => 1, vmdk => 1},
- current => {qcow2 => 1, raw => 1, vmdk => 1},
- snap => {qcow2 => 1} },
- sparseinit => { base => {qcow2 => 1, raw => 1, vmdk => 1},
- current => {qcow2 => 1, raw => 1, vmdk => 1} },
+ snapshot => {
+ current => { qcow2 => 1 },
+ snap => { qcow2 => 1 },
+ },
+ clone => {
+ base => { qcow2 => 1, raw => 1, vmdk => 1 },
+ },
+ template => {
+ current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
+ },
+ copy => {
+ base => { qcow2 => 1, raw => 1, vmdk => 1 },
+ current => { qcow2 => 1, raw => 1, vmdk => 1 },
+ snap => { qcow2 => 1 },
+ },
+ sparseinit => {
+ base => { qcow2 => 1, raw => 1, vmdk => 1 },
+ current => { qcow2 => 1, raw => 1, vmdk => 1 },
+ },
+ rename => {
+ current => {qcow2 => 1, raw => 1, vmdk => 1},
+ },
};
- # clone_image creates a qcow2 volume
- return 0 if $feature eq 'clone' &&
- defined($opts->{valid_target_formats}) &&
- !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}});
+ if ($feature eq 'clone') {
+ if (
+ defined($opts->{valid_target_formats})
+ && !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}})
+ ) {
+ return 0; # clone_image creates a qcow2 volume
+ }
+ } elsif ($feature eq 'rename') {
+ return 0 if $class->can('api') && $class->api() < 10;
+ }
- my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
- $class->parse_volname($volname);
+
+ my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
my $key = undef;
if($snapname){
my $info;
if ($tt eq 'iso') {
- next if $fn !~ m!/([^/]+$PVE::Storage::iso_extension_re)$!i;
+ next if $fn !~ m!/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!i;
$info = { volid => "$sid:iso/$1", format => 'iso' };
} elsif ($tt eq 'vztmpl') {
- next if $fn !~ m!/([^/]+$PVE::Storage::vztmpl_extension_re)$!;
+ next if $fn !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
$info = { volid => "$sid:vztmpl/$1", format => "t$2" };
} elsif ($tt eq 'backup') {
- next if $fn !~ m!/([^/]+\.(tgz|(?:(?:tar|vma)(?:\.(${\COMPRESSOR_RE}))?)))$!;
+ next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
my $original = $fn;
my $format = $2;
$fn = $1;
my $archive_info = eval { PVE::Storage::archive_info($fn) } // {};
$info->{ctime} = $archive_info->{ctime} if defined($archive_info->{ctime});
+ $info->{subtype} = $archive_info->{type} // 'unknown';
if (defined($vmid) || $fn =~ m!\-([1-9][0-9]{2,8})\-[^/]+\.${format}$!) {
$info->{vmid} = $vmid // $1;
$info->{notes} = $notes if defined($notes);
}
+ $info->{protected} = 1 if -e PVE::Storage::protection_file_path($original);
} elsif ($tt eq 'snippets') {
$info = {
return $res;
};
+# If attributes are set on a volume, they should be included in the result.
+# See get_volume_attribute for a list of possible attributes.
sub list_volumes {
my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
return ($res->{total}, $res->{avail}, $res->{used}, 1);
}
-sub volume_snapshot_list {
+# Returns a hash with the snapshot names as keys and the following data:
+# id - Unique id to distinguish different snapshots even if the have the same name.
+# timestamp - Creation time of the snapshot (seconds since epoch).
+# Returns an empty hash if the volume does not exist.
+sub volume_snapshot_info {
my ($class, $scfg, $storeid, $volname) = @_;
- # implement in subclass
- die "Volume_snapshot_list is not implemented for $class";
-
- # return an empty array if dataset does not exist.
+ die "volume_snapshot_info is not implemented for $class";
}
sub activate_storage {
push @{$backup_groups->{$group}}, $prune_entry;
} else {
# ignore backups that don't use the standard naming scheme
- $prune_entry->{mark} = 'protected';
+ $prune_entry->{mark} = 'renamed';
}
+ $prune_entry->{mark} = 'protected' if $backup->{protected};
+
push @{$prune_list}, $prune_entry;
}
sysread($fh, my $size, 8);
$size = unpack('Q<', $size);
die "import: no size found in export header, aborting.\n" if !defined($size);
- die "import: got a bad size (not a multiple of 1K), aborting.\n" if ($size&1023);
# Size is in bytes!
return $size;
}
return ();
}
+sub rename_volume {
+ my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+ die "not implemented in storage plugin '$class'\n" if $class->can('api') && $class->api() < 10;
+ die "no path found\n" if !$scfg->{path};
+
+ my (
+ undef,
+ $source_image,
+ $source_vmid,
+ $base_name,
+ $base_vmid,
+ undef,
+ $format
+ ) = $class->parse_volname($source_volname);
+
+ $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
+ if !$target_volname;
+
+ my $basedir = $class->get_subdir($scfg, 'images');
+
+ mkpath "${basedir}/${target_vmid}";
+
+ my $old_path = "${basedir}/${source_vmid}/${source_image}";
+ my $new_path = "${basedir}/${target_vmid}/${target_volname}";
+
+ die "target volume '${target_volname}' already exists\n" if -e $new_path;
+
+ my $base = $base_name ? "${base_vmid}/${base_name}/" : '';
+
+ rename($old_path, $new_path) ||
+ die "rename '$old_path' to '$new_path' failed - $!\n";
+
+ return "${storeid}:${base}${target_vmid}/${target_volname}";
+}
+
1;