Files
pve-storage/PVE/Storage/LvmThinPlugin.pm
Fabian Ebner 6fbffac0a3 lvm thin: don't assume that a thin pool and its volumes are active
There are cases where autoactivation can fail, as reported in the
community forum [0]. And it could also be that a volume was
deactivated by something outside of our control.

It doesn't seem strictly necessary to activate the thin pool itself
(creating/removing/activating LVs within the pool still works if it's
not active), but it does not report usage information as long as
neither the pool nor any of its LVs are active. Activate the pool for
that, for being able to use the flag in status(), and it should also
serve as a good indicator that there's a problem with the pool if it
can't be activated.

Before activating, check the (cached) lv_state from lvm_list_volumes.
It's necessary to update the cache in activate_storage, because the
flag is re-used in status(). Also update it for other (de)activations
to be more future-proof.

[0]: https://forum.proxmox.com/threads/local-lvm-not-available-after-kernel-update-on-pve-7.97406

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2021-11-10 14:23:54 +01:00

394 lines
9.9 KiB
Perl

package PVE::Storage::LvmThinPlugin;
use strict;
use warnings;
use IO::File;
use PVE::Tools qw(run_command trim);
use PVE::Storage::Plugin;
use PVE::Storage::LVMPlugin;
use PVE::JSONSchema qw(get_standard_option);
# see: man lvmthin
# lvcreate -n ThinDataLV -L LargeSize VG
# lvconvert --type thin-pool VG/ThinDataLV
# lvcreate -n pvepool -L 20G pve
# lvconvert --type thin-pool pve/pvepool
# NOTE: volumes which were created as linked clones of another base volume
# are currently not tracking this relationship in their volume IDs. this is
# generally not a problem, as LVM thin allows deletion of such base volumes
# without affecting the linked clones. this leads to increased disk usage
# when migrating LVM-thin volumes, which is normally prevented for linked clones.
use base qw(PVE::Storage::LVMPlugin);
sub type {
return 'lvmthin';
}
sub plugindata {
return {
content => [ {images => 1, rootdir => 1}, { images => 1, rootdir => 1}],
};
}
sub properties {
return {
thinpool => {
description => "LVM thin pool LV name.",
type => 'string', format => 'pve-storage-vgname',
},
};
}
sub options {
return {
thinpool => { fixed => 1 },
vgname => { fixed => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
};
}
# NOTE: the fourth and fifth element of the returned array are always
# undef, even if the volume is a linked clone of another volume. see note
# at beginning of file.
sub parse_volname {
my ($class, $volname) = @_;
PVE::Storage::Plugin::parse_lvm_name($volname);
if ($volname =~ m/^((vm|base)-(\d+)-\S+)$/) {
return ('images', $1, $3, undef, undef, $2 eq 'base', 'raw');
}
die "unable to parse lvm volume name '$volname'\n";
}
sub filesystem_path {
my ($class, $scfg, $volname, $snapname) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $vg = $scfg->{vgname};
my $path = defined($snapname) ? "/dev/$vg/snap_${name}_$snapname": "/dev/$vg/$name";
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "unsupported format '$fmt'" if $fmt ne 'raw';
die "illegal name '$name' - should be 'vm-$vmid-*'\n"
if $name && $name !~ m/^vm-$vmid-/;
my $vgs = PVE::Storage::LVMPlugin::lvm_vgs();
my $vg = $scfg->{vgname};
die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
$name = $class->find_free_diskname($storeid, $scfg, $vmid)
if !$name;
my $cmd = ['/sbin/lvcreate', '-aly', '-V', "${size}k", '--name', $name,
'--thinpool', "$vg/$scfg->{thinpool}" ];
run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
return $name;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my $vg = $scfg->{vgname};
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
if (my $dat = $lvs->{$scfg->{vgname}}) {
# remove all volume snapshots first
foreach my $lv (keys %$dat) {
next if $lv !~ m/^snap_${volname}_${PVE::JSONSchema::CONFIGID_RE}$/;
my $cmd = ['/sbin/lvremove', '-f', "$vg/$lv"];
run_command($cmd, errmsg => "lvremove snapshot '$vg/$lv' error");
}
# finally remove original (if exists)
if ($dat->{$volname}) {
my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
}
}
return undef;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $vgname = $scfg->{vgname};
$cache->{lvs} = PVE::Storage::LVMPlugin::lvm_list_volumes() if !$cache->{lvs};
my $res = [];
if (my $dat = $cache->{lvs}->{$vgname}) {
foreach my $volname (keys %$dat) {
next if $volname !~ m/^(vm|base)-(\d+)-/;
my $owner = $2;
my $info = $dat->{$volname};
next if $info->{lv_type} ne 'V';
next if $info->{pool_lv} ne $scfg->{thinpool};
my $volid = "$storeid:$volname";
if ($vollist) {
my $found = grep { $_ eq $volid } @$vollist;
next if !$found;
} else {
next if defined($vmid) && ($owner ne $vmid);
}
push @$res, {
volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
ctime => $info->{ctime},
};
}
}
return $res;
}
sub list_thinpools {
my ($vg) = @_;
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
my $thinpools = [];
foreach my $vg (keys %$lvs) {
foreach my $lvname (keys %{$lvs->{$vg}}) {
next if $lvs->{$vg}->{$lvname}->{lv_type} ne 't';
my $lv = $lvs->{$vg}->{$lvname};
$lv->{lv} = $lvname;
$lv->{vg} = $vg;
push @$thinpools, $lv;
}
}
return $thinpools;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
return if !$lvs->{$scfg->{vgname}};
my $info = $lvs->{$scfg->{vgname}}->{$scfg->{thinpool}};
return if !$info || $info->{lv_type} ne 't' || !$info->{lv_size};
return (
$info->{lv_size},
$info->{lv_size} - $info->{used},
$info->{used},
$info->{lv_state} eq 'a' ? 1 : 0,
);
}
my $activate_lv = sub {
my ($vg, $lv, $cache) = @_;
my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
die "no such logical volume $vg/$lv" if !$lvs->{$vg} || !$lvs->{$vg}->{$lv};
return if $lvs->{$vg}->{$lv}->{lv_state} eq 'a';
run_command(['lvchange', '-ay', '-K', "$vg/$lv"], errmsg => "activating LV '$vg/$lv' failed");
$lvs->{$vg}->{$lv}->{lv_state} = 'a'; # update cache
return;
};
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$class->SUPER::activate_storage($storeid, $scfg, $cache);
$activate_lv->($scfg->{vgname}, $scfg->{thinpool}, $cache);
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
my $vg = $scfg->{vgname};
my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
$activate_lv->($vg, $lv, $cache);
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
return if !$snapname && $volname !~ /^base-/; # other volumes are kept active
my $vg = $scfg->{vgname};
my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
run_command(['lvchange', '-an', "$vg/$lv"], errmsg => "deactivate_volume '$vg/$lv' error");
$cache->{lvs}->{$vg}->{$lv}->{lv_state} = '-' # update cache
if $cache->{lvs} && $cache->{lvs}->{$vg} && $cache->{lvs}->{$vg}->{$lv};
return;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
my $vg = $scfg->{vgname};
my $lv;
if ($snap) {
$lv = "$vg/snap_${volname}_$snap";
} else {
my ($vtype, undef, undef, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
die "clone_image only works on base images\n" if !$isBase;
$lv = "$vg/$volname";
}
my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
my $cmd = ['/sbin/lvcreate', '-n', $name, '-prw', '-kn', '-s', $lv];
run_command($cmd, errmsg => "clone image '$lv' error");
return $name;
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
die "create_base not possible with base image\n" if $isBase;
my $vg = $scfg->{vgname};
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
if (my $dat = $lvs->{$vg}) {
# to avoid confusion, reject if we find volume snapshots
foreach my $lv (keys %$dat) {
die "unable to create base volume - found snaphost '$lv'\n"
if $lv =~ m/^snap_${volname}_(\w+)$/;
}
}
my $newname = $name;
$newname =~ s/^vm-/base-/;
my $cmd = ['/sbin/lvrename', $vg, $volname, $newname];
run_command($cmd, errmsg => "lvrename '$vg/$volname' => '$vg/$newname' error");
# set inactive, read-only and activationskip flags
$cmd = ['/sbin/lvchange', '-an', '-pr', '-ky', "$vg/$newname"];
eval { run_command($cmd); };
warn $@ if $@;
my $newvolname = $newname;
return $newvolname;
}
# sub volume_resize {} reuse code from parent class
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvcreate', '-n', $snapvol, '-pr', '-s', "$vg/$volname"];
run_command($cmd, errmsg => "lvcreate snapshot '$vg/$snapvol' error");
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
$cmd = ['/sbin/lvcreate', '-kn', '-n', $volname, '-s', "$vg/$snapvol"];
run_command($cmd, errmsg => "lvm rollback '$vg/$snapvol' error");
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvremove', '-f', "$vg/$snapvol"];
run_command($cmd, errmsg => "lvremove snapshot '$vg/$snapvol' error");
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => { current => 1 },
clone => { base => 1, snap => 1},
template => { current => 1},
copy => { base => 1, current => 1, snap => 1},
sparseinit => { base => 1, current => 1},
rename => {current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if($snapname){
$key = 'snap';
}else{
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
# used in LVMPlugin->volume_import
sub volume_import_write {
my ($class, $input_fh, $output_file) = @_;
run_command(['dd', "of=$output_file", 'conv=sparse', 'bs=64k'],
input => '<&'.fileno($input_fh));
}
1;