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 D94661FF16B for ; Mon, 30 Sep 2024 13:31:56 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4C37E1554C; Mon, 30 Sep 2024 13:32:08 +0200 (CEST) To: pve-devel@lists.proxmox.com Date: Mon, 30 Sep 2024 13:31:53 +0200 In-Reply-To: <20240930113153.2896648-1-alexandre.derumier@groupe-cyllene.com> References: <20240930113153.2896648-1-alexandre.derumier@groupe-cyllene.com> MIME-Version: 1.0 Message-ID: List-Id: Proxmox VE development discussion List-Post: From: Alexandre Derumier via pve-devel Precedence: list Cc: Alexandre Derumier X-Mailman-Version: 2.1.29 X-BeenThere: pve-devel@lists.proxmox.com List-Subscribe: , List-Unsubscribe: , List-Archive: Reply-To: Proxmox VE development discussion List-Help: Subject: [pve-devel] [PATCH v2 pve-storage 2/2] add lvmqcow2 plugin: (lvm with external qcow2 snapshot) Content-Type: multipart/mixed; boundary="===============1020343309344307446==" Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" --===============1020343309344307446== Content-Type: message/rfc822 Content-Disposition: inline Return-Path: X-Original-To: pve-devel@lists.proxmox.com Delivered-To: pve-devel@lists.proxmox.com Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id BAB68C1B7F for ; Mon, 30 Sep 2024 13:32:06 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A4FC015394 for ; Mon, 30 Sep 2024 13:32:06 +0200 (CEST) Received: from bastiontest.odiso.net (unknown [IPv6:2a0a:1580:2000:6700::14]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 30 Sep 2024 13:32:04 +0200 (CEST) Received: from formationkvm1.odiso.net (unknown [10.11.201.57]) by bastiontest.odiso.net (Postfix) with ESMTP id E2A7A82E03F; Mon, 30 Sep 2024 13:31:56 +0200 (CEST) Received: by formationkvm1.odiso.net (Postfix, from userid 0) id 4FDDF102037E; Mon, 30 Sep 2024 13:31:54 +0200 (CEST) From: Alexandre Derumier To: pve-devel@lists.proxmox.com Subject: [PATCH v2 pve-storage 2/2] add lvmqcow2 plugin: (lvm with external qcow2 snapshot) Date: Mon, 30 Sep 2024 13:31:53 +0200 Message-Id: <20240930113153.2896648-4-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240930113153.2896648-1-alexandre.derumier@groupe-cyllene.com> References: <20240930113153.2896648-1-alexandre.derumier@groupe-cyllene.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.214 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_NONE 0.1 DMARC none policy HEADER_FROM_DIFFERENT_DOMAINS 0.248 From and EnvelopeFrom 2nd level mail domains are different KAM_DMARC_NONE 0.25 DKIM has Failed or SPF has failed on the message and the domain has no DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URI_NOVOWEL 0.5 URI hostname has long non-vowel sequence Signed-off-by: Alexandre Derumier --- src/PVE/Storage.pm | 2 + src/PVE/Storage/LvmQcow2Plugin.pm | 460 ++++++++++++++++++++++++++++++ src/PVE/Storage/Makefile | 3 +- 3 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 src/PVE/Storage/LvmQcow2Plugin.pm diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm index 57b2038..119998f 100755 --- a/src/PVE/Storage.pm +++ b/src/PVE/Storage.pm @@ -28,6 +28,7 @@ use PVE::Storage::Plugin; use PVE::Storage::DirPlugin; use PVE::Storage::LVMPlugin; use PVE::Storage::LvmThinPlugin; +use PVE::Storage::LvmQcow2Plugin; use PVE::Storage::NFSPlugin; use PVE::Storage::CIFSPlugin; use PVE::Storage::ISCSIPlugin; @@ -54,6 +55,7 @@ our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', PVE::Storage::DirPlugin->register(); PVE::Storage::LVMPlugin->register(); PVE::Storage::LvmThinPlugin->register(); +PVE::Storage::LvmQcow2Plugin->register(); PVE::Storage::NFSPlugin->register(); PVE::Storage::CIFSPlugin->register(); PVE::Storage::ISCSIPlugin->register(); diff --git a/src/PVE/Storage/LvmQcow2Plugin.pm b/src/PVE/Storage/LvmQcow2Plugin.pm new file mode 100644 index 0000000..68c8686 --- /dev/null +++ b/src/PVE/Storage/LvmQcow2Plugin.pm @@ -0,0 +1,460 @@ +package PVE::Storage::LvmQcow2Plugin; + +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); + +use base qw(PVE::Storage::LVMPlugin); + +# Configuration + +sub type { + return 'lvmqcow2'; +} + +sub plugindata { + return { + #container not yet implemented #need to implemented dm-qcow2 + content => [ {images => 1, rootdir => 1}, { images => 1 }], + }; +} + +sub properties { + return { + }; +} + +sub options { + return { + vgname => { fixed => 1 }, + nodes => { optional => 1 }, + shared => { optional => 1 }, + disable => { optional => 1 }, + saferemove => { optional => 1 }, + saferemove_throughput => { optional => 1 }, + content => { optional => 1 }, + base => { fixed => 1, optional => 1 }, + tagged_only => { optional => 1 }, + bwlimit => { optional => 1 }, + snapext => { fixed => 1 }, + }; +} + +# Storage implementation + +sub parse_volname { + my ($class, $volname) = @_; + + PVE::Storage::Plugin::parse_lvm_name($volname); + my $format = $volname =~ m/^(.*)-snap-/ ? 'qcow2' : 'raw'; + + if ($volname =~ m/^((vm|base)-(\d+)-\S+)$/) { + return ('images', $1, $3, undef, undef, $2 eq 'base', $format); + } + + die "unable to parse lvm volume name '$volname'\n"; +} + +sub filesystem_path { + my ($class, $scfg, $volname, $snapname, $current_snap) = @_; + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + my $vg = $scfg->{vgname}; + + my $path = "/dev/$vg/$name"; + + if($snapname) { + $path = get_snap_volname($path, $snapname); + } elsif ($current_snap) { + $path = $current_snap->{file}; + } + + return wantarray ? ($path, $vmid, $vtype) : $path; +} + +sub create_base { + my ($class, $storeid, $scfg, $volname) = @_; + + my $vg = $scfg->{vgname}; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = + $class->parse_volname($volname); + + die "create_base not possible with base image\n" if $isBase; + + die "unable to create base volume - found snapshot" if $class->snapshot_exist($scfg, $storeid, $volname); + + 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 flags + $cmd = ['/sbin/lvchange', '-an', '-pr', "$vg/$newname"]; + eval { run_command($cmd); }; + warn $@ if $@; + + my $newvolname = $newname; + + return $newvolname; +} + +sub clone_image { + my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; + + die "can't clone images in lvm storage\n"; +} + +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}); + + my $free = int($vgs->{$vg}->{free}); + + die "not enough free space ($free < $size)\n" if $free < $size; + + $name = $class->find_free_diskname($storeid, $scfg, $vmid) + if !$name; + + my $tags = ["pve-vm-$vmid"]; + if ($name =~ m/^(((vm|base)-(\d+)-disk-(\d+)))(-snap-(.*))?/) { + push @$tags, "\@pve-$1"; + } + + PVE::Storage::LVMPlugin::lvcreate($vg, $name, $size, $tags); + + return $name; +} + +sub volume_snapshot_info { + my ($class, $scfg, $storeid, $volname) = @_; + + return $class->list_snapshots($scfg, $storeid, $volname); +} + +sub activate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + my $lvm_activate_mode = 'ey'; + my $tag = undef; + + #activate volume && all volumes snapshots by tag + if($volname =~ m/^(((vm|base)-(\d+)-disk-(\d+)))(-snap-(.*))?/) { + $tag = "\@pve-vm-$4-disk-$5"; + } + + my $cmd = ['/sbin/lvchange', "-a$lvm_activate_mode", $tag]; + run_command($cmd, errmsg => "can't activate LV '$tag'"); + + $cmd = ['/sbin/lvchange', '--refresh', $tag]; + run_command($cmd, errmsg => "can't refresh LV '$tag' for activation"); +} + +sub deactivate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + my $tag = undef; + #deactivate volume && all volumes snasphots by tag + if($volname =~ m/^(((vm|base)-(\d+)-disk-(\d+)))(-snap-(.*))?/) { + $tag = "\@pve-vm-$4-disk-$5"; + } + + my $cmd = ['/sbin/lvchange', '-aln', $tag]; + run_command($cmd, errmsg => "can't deactivate LV '$tag'"); +} + +sub volume_resize { + my ($class, $scfg, $storeid, $volname, $size, $running) = @_; + + #we should resize the base image and parents snapshots, + #but how to manage rollback ? + + die "can't resize if snasphots exist" if $class->snapshot_exist($scfg, $storeid, $volname); + + return 1; +} + +sub volume_snapshot { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + + $class->activate_volume($storeid, $scfg, $volname, undef, {}); + + my $current_path = $class->path($scfg, $volname, $storeid); + my $current_format = (PVE::Storage::Plugin::file_size_info($current_path))[1]; + my $snappath = get_snap_volname($current_path, $snap); + + my $snapvolname = get_snap_volname($volname, $snap); + #allocate lvm snapshot volume + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = + $class->parse_volname($volname); + my $size = $class->volume_size_info($scfg, $storeid, $volname, 5); + #add 100M for qcow2 headers + $size = int($size/1024) + (100*1024); + + $class->alloc_image($storeid, $scfg, $vmid, 'raw', $snapvolname, $size); + + # create the qcow2 fs + eval { + my $cmd = ['/usr/bin/qemu-img', 'create', '-b', $current_path, + '-F', $current_format, '-f', 'qcow2', $snappath]; + my $options = "extended_l2=on,"; + $options .= PVE::Storage::Plugin::preallocation_cmd_option($scfg, 'qcow2'); + push @$cmd, '-o', $options; + run_command($cmd); + }; + if ($@) { + eval { $class->free_image($storeid, $scfg, $snapvolname, 0) }; + warn $@ if $@; + } +} + +# 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, $blockers) = @_; + + my $path = $class->filesystem_path($scfg, $volname); + my $snappath = get_snap_volname($path, $snap); + my $currentpath = $class->path($scfg, $volname, $storeid); + return 1 if $currentpath eq $snappath; + + die "can't rollback, '$snap' is not most recent snapshot on '$volname'\n"; + + return 1; +} + +sub volume_snapshot_rollback { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + + $class->activate_volume($storeid, $scfg, $volname, undef, {}); + #simply delete the current snapshot and recreate it + + my $snapvolname = get_snap_volname($volname, $snap); + + $class->free_image($storeid, $scfg, $snapvolname, 0); + $class->volume_snapshot($scfg, $storeid, $volname, $snap); +} + +sub volume_snapshot_delete { + my ($class, $scfg, $storeid, $volname, $snap, $running) = @_; + + return 1 if $running; + + $class->activate_volume($storeid, $scfg, $volname, undef, {}); + + my $snapshots = $class->volume_snapshot_info($scfg, $storeid, $volname); + my $snappath = $snapshots->{$snap}->{file}; + if(!$snappath) { + warn "$snap already deleted. skip\n"; + return; + } + + my $snapvolname = $snapshots->{$snap}->{volname}; + my $parentsnap = $snapshots->{$snap}->{parent}; + my $childsnap = $snapshots->{$snap}->{child}; + die "error: can't find a parent for this snapshot" if !$parentsnap; + + my $parentpath = $snapshots->{$parentsnap}->{file}; + my $parentformat = $snapshots->{$parentsnap}->{'format'} if $parentsnap; + my $childpath = $snapshots->{$childsnap}->{file} if $childsnap; + my $childformat = $snapshots->{$childsnap}->{'format'} if $childsnap; + + print "merge snapshot $snap to $parentsnap\n"; + my $cmd = ['/usr/bin/qemu-img', 'commit', $snappath]; + run_command($cmd); + + #if we delete an intermediate snapshot, we need to link upper snapshot to base snapshot + if($childpath && -e $childpath) { + die "missing parentsnap snapshot to rebase child $childpath\n" if !$parentpath; + print "link $childsnap to $parentsnap\n"; + $cmd = ['/usr/bin/qemu-img', 'rebase', '-u', '-b', $parentpath, '-F', $parentformat, '-f', $childformat, $childpath]; + run_command($cmd); + } + + #delete the snapshot + $class->free_image($storeid, $scfg, $snapvolname, 0); + + return; +} + +sub volume_has_feature { + my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; + + my $features = { + snapshot => { current => 1 }, +# clone => { base => 1, snap => 1}, #don't allow to clone as we can't activate the base between different host ? + 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; +} + +sub get_snap_volname { + my ($path, $snap) = @_; + + my $basepath = ""; + my $baseformat = ""; + if ($path =~ m/^((.*)((vm|base)-(\d+)-disk-(\d+)))(-snap-([a-zA-Z0-9]+))?(\.(raw|qcow2))?/) { + $basepath = $1; + $baseformat = $8; + } + my $snapvolname = $basepath."-snap-$snap.qcow2"; + return $snapvolname; +} + +sub get_snapname_from_path { + my ($path) = @_; + + if ($path =~ m/^((.*)((vm|base)-(\d+)-disk-(\d+)))(-snap-([a-zA-Z0-9]+))?(\.(raw|qcow2))?/) { + my $snapname = $7; + return $snapname; + } + die "can't parse snapname from path $path"; +} + +sub get_current_snapshot { + my ($class, $scfg, $storeid, $volname) = @_; + + #get more recent ctime volume + return $class->list_snapshots($scfg, $storeid, $volname, 1); +} +my $check_tags = sub { + my ($tags) = @_; + + return defined($tags) && $tags =~ /(^|,)pve-vm-\d+(,|$)/; +}; + +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 $scfg->{tagged_only} && !&$check_tags($info->{tags}); + + # Allow mirrored and RAID LVs + next if $info->{lv_type} !~ m/^[-mMrR]$/; + + 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_snapshots { + my ($class, $scfg, $storeid, $volname, $current_only) = @_; + + my $vgname = $scfg->{vgname}; + + my $basevolname = $volname; + my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vgname); + + my $vg = $lvs->{$vgname}; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname); + my $snapshots = $class->list_images($storeid, $scfg, $vmid); + + my $info = {}; + for my $snap (@$snapshots) { + my $snap_volid = $snap->{volid}; + next if ($snap_volid !~ m/$basevolname/); + + my $snapname = get_snapname_from_path($snap_volid); + my (undef, $snap_volname) = PVE::Storage::parse_volume_id($snap_volid); + my $snapfile = $class->filesystem_path($scfg, $snap_volname, $snapname); + $snapname = 'base' if !$snapname; + $info->{$snapname}->{file} = $snapfile; + $info->{$snapname}->{volname} = $snap_volname; + $info->{$snapname}->{volid} = $snap_volid; + $info->{$snapname}->{ctime} = $snap->{ctime}; + + if (!$current_only) { + my (undef, $format, undef, $parentfile, undef) = PVE::Storage::Plugin::file_size_info($snapfile); + next if !$parentfile && $snapname ne 'base'; #bad unlinked snasphot + + my $parentname = get_snapname_from_path($parentfile) if $parentfile; + $parentname = 'base' if !$parentname && $parentfile; + + $info->{$snapname}->{'format'} = $format; + $info->{$snapname}->{parent} = $parentname if $parentname; + $info->{$parentname}->{child} = $snapname if $parentname; + } + } + + my @snapshots_sorted = sort { $info->{$b}{ctime} <=> $info->{$a}{ctime} } keys %$info; + my $current_snapname = $snapshots_sorted[0]; + my $current_snapshot = $info->{$current_snapname}; + return $current_snapshot if $current_only; + + $info->{current} = { %$current_snapshot }; + return $info; +} + +sub snapshot_exist { + my ($class, $scfg, $storeid, $volname) = @_; + + my $basepath = $class->filesystem_path($scfg, $volname); + my $currentpath = $class->path($scfg, $volname, $storeid); + + die "can't resize if snasphots exist" if $currentpath ne $basepath; + +} +1; diff --git a/src/PVE/Storage/Makefile b/src/PVE/Storage/Makefile index d5cc942..1af8aab 100644 --- a/src/PVE/Storage/Makefile +++ b/src/PVE/Storage/Makefile @@ -14,7 +14,8 @@ SOURCES= \ PBSPlugin.pm \ BTRFSPlugin.pm \ LvmThinPlugin.pm \ - ESXiPlugin.pm + ESXiPlugin.pm \ + LvmQcow2Plugin.pm .PHONY: install install: -- 2.39.2 --===============1020343309344307446== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel --===============1020343309344307446==--