From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pve-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 3B2ED1FF15E for <inbox@lore.proxmox.com>; Tue, 3 Jun 2025 09:58:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AF59B1195C; Tue, 3 Jun 2025 09:57:14 +0200 (CEST) To: pve-devel@lists.proxmox.com Date: Tue, 3 Jun 2025 09:55:50 +0200 In-Reply-To: <20250603075558.627850-1-alexandre.derumier@groupe-cyllene.com> References: <20250603075558.627850-1-alexandre.derumier@groupe-cyllene.com> MIME-Version: 1.0 Message-ID: <mailman.222.1748937433.395.pve-devel@lists.proxmox.com> List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com> List-Post: <mailto:pve-devel@lists.proxmox.com> From: Alexandre Derumier via pve-devel <pve-devel@lists.proxmox.com> Precedence: list Cc: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com> X-Mailman-Version: 2.1.29 X-BeenThere: pve-devel@lists.proxmox.com List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/> Reply-To: Proxmox VE development discussion <pve-devel@lists.proxmox.com> List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help> Subject: [pve-devel] [PATCH pve-storage 7/9] qcow2: add external snapshot support Content-Type: multipart/mixed; boundary="===============6681596957087559907==" Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" <pve-devel-bounces@lists.proxmox.com> --===============6681596957087559907== Content-Type: message/rfc822 Content-Disposition: inline Return-Path: <root@formationkvm1.odiso.net> 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 5A565CB717 for <pve-devel@lists.proxmox.com>; Tue, 3 Jun 2025 09:57:12 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A7EA910D24 for <pve-devel@lists.proxmox.com>; Tue, 3 Jun 2025 09:56:21 +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 <pve-devel@lists.proxmox.com>; Tue, 3 Jun 2025 09:56:14 +0200 (CEST) Received: from formationkvm1.odiso.net (unknown [10.11.201.57]) by bastiontest.odiso.net (Postfix) with ESMTP id 5421F862E52; Tue, 3 Jun 2025 09:56:02 +0200 (CEST) Received: by formationkvm1.odiso.net (Postfix, from userid 0) id 82D5D110DBD3; Tue, 3 Jun 2025 09:56:01 +0200 (CEST) From: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com> To: pve-devel@lists.proxmox.com Subject: [PATCH pve-storage 7/9] qcow2: add external snapshot support Date: Tue, 3 Jun 2025 09:55:50 +0200 Message-Id: <20250603075558.627850-16-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250603075558.627850-1-alexandre.derumier@groupe-cyllene.com> References: <20250603075558.627850-1-alexandre.derumier@groupe-cyllene.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 1 AWL -0.898 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.001 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 KAM_MAILER 2 Automated Mailer Tag Left in Email 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 add a snapext option to enable the feature When a snapshot is taken, the current volume is renamed to snap volname and a current image is created with the snap volume as backing file Signed-off-by: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com> --- src/PVE/Storage.pm | 1 - src/PVE/Storage/Common.pm | 3 +- src/PVE/Storage/DirPlugin.pm | 1 + src/PVE/Storage/Plugin.pm | 241 +++++++++++++++++++++++++++++++++-- 4 files changed, 230 insertions(+), 16 deletions(-) diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm index 973161f..2a2005b 100755 --- a/src/PVE/Storage.pm +++ b/src/PVE/Storage.pm @@ -378,7 +378,6 @@ sub volume_snapshot_rollback { } } -# FIXME PVE 8.x remove $running parameter (needs APIAGE reset) sub volume_snapshot_delete { my ($cfg, $volid, $snap, $running) = @_; diff --git a/src/PVE/Storage/Common.pm b/src/PVE/Storage/Common.pm index 11b5b94..7e87a54 100644 --- a/src/PVE/Storage/Common.pm +++ b/src/PVE/Storage/Common.pm @@ -217,10 +217,11 @@ Returns a json with qemu image C<$filename> informations with format <$file_form =cut sub qemu_img_info { - my ($filename, $file_format, $timeout) = @_; + my ($filename, $file_format, $timeout, $follow_backing_files) = @_; my $cmd = ['/usr/bin/qemu-img', 'info', '--output=json', $filename]; push $cmd->@*, '-f', $file_format if $file_format; + push $cmd->@*, '--backing-chain' if $follow_backing_files; my $json = ''; my $err_output = ''; diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm index 734309f..54d8d74 100644 --- a/src/PVE/Storage/DirPlugin.pm +++ b/src/PVE/Storage/DirPlugin.pm @@ -83,6 +83,7 @@ sub options { is_mountpoint => { optional => 1 }, bwlimit => { optional => 1 }, preallocation => { optional => 1 }, + snapext => { optional => 1 }, }; } diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm index 00eebd3..fd50b9e 100644 --- a/src/PVE/Storage/Plugin.pm +++ b/src/PVE/Storage/Plugin.pm @@ -202,6 +202,11 @@ my $defaultData = { maximum => 65535, optional => 1, }, + 'snapext' => { + type => 'boolean', + description => 'enable external snapshot.', + optional => 1, + }, }, }; @@ -696,6 +701,8 @@ sub filesystem_path { my ($vtype, $name, $vmid, undef, undef, $isBase, $format) = $class->parse_volname($volname); + $name = $class->get_snap_name($volname, $snapname) if $scfg->{snapext} && $snapname; + # Note: qcow2/qed has internal snapshot, so path is always # the same (with or without snapshot => same file). die "can't snapshot this image format\n" @@ -896,6 +903,26 @@ sub alloc_image { return "$vmid/$name"; } +my sub alloc_backed_image { + my ($class, $storeid, $scfg, $volname, $backing_snap) = @_; + + my $path = $class->path($scfg, $volname, $storeid); + my $backing_path = $class->path($scfg, $volname, $storeid, $backing_snap); + + eval { PVE::Storage::Common::qemu_img_create_qcow2_backed($scfg, $path, $backing_path, 'qcow2') }; + if ($@) { + unlink $path; + die "$@"; + } +} + +my sub free_snap_image { + my ($class, $storeid, $scfg, $volname, $snap) = @_; + + my $path = $class->path($scfg, $volname, $storeid, $snap); + unlink($path) || die "unlink '$path' failed - $!\n"; +} + sub free_image { my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_; @@ -918,6 +945,17 @@ sub free_image { return undef; } + #delete external snapshots + if($scfg->{snapext}) { + my $snapshots = $class->volume_snapshot_info($scfg, $storeid, $volname); + for my $snapid (sort { $snapshots->{$b}->{order} <=> $snapshots->{$a}->{order} } keys %$snapshots) { + my $snap = $snapshots->{$snapid}; + next if $snapid eq 'current'; + next if !$snap->{ext}; + free_snap_image($class, $storeid, $scfg, $volname, $snapid); + } + } + unlink($path) || die "unlink '$path' failed - $!\n"; } @@ -1122,11 +1160,33 @@ sub volume_snapshot { die "can't snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/; - my $path = $class->filesystem_path($scfg, $volname); + if($scfg->{snapext}) { - my $cmd = ['/usr/bin/qemu-img', 'snapshot','-c', $snap, $path]; + my $vmid = ($class->parse_volname($volname))[2]; - run_command($cmd); + #if running, the old current has been renamed with blockdev-reopen by qemu + if (!$running) { + #rename current volume to snap volume + $class->rename_volume($scfg, $storeid, $volname, $vmid, undef, 'current', $snap); + } + + eval { alloc_backed_image($class, $storeid, $scfg, $volname, $snap) }; + if ($@) { + warn "$@ \n"; + #if running, the revert is done by qemu with blockdev-reopen + if (!$running) { + eval { $class->rename_volume($scfg, $storeid, $volname, $vmid, undef, $snap, 'current') }; + warn $@ if $@; + } + die "can't allocate new volume $volname with $snap backing image\n"; + } + + } else { + + my $path = $class->filesystem_path($scfg, $volname); + my $cmd = ['/usr/bin/qemu-img', 'snapshot','-c', $snap, $path]; + run_command($cmd); + } return undef; } @@ -1137,6 +1197,21 @@ sub volume_snapshot { sub volume_rollback_is_possible { my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_; + if ($scfg->{snapext}) { + #technically, we could manage multibranch, we it need lot more work for snapshot delete + #we need to implemente block-stream from deleted snapshot to all others child branchs + #when online, we need to do a transaction for multiple disk when delete the last snapshot + #and need to merge in current running file + + my $snappath = $class->path($scfg, $volname, $storeid, $snap); + my $snapshots = $class->volume_snapshot_info($scfg, $storeid, $volname); + my $parentsnap = $snapshots->{current}->{parent}; + + return 1 if $parentsnap eq $snap; + + die "can't rollback, '$snap' is not most recent snapshot on '$volname'\n"; + } + return 1; } @@ -1145,11 +1220,22 @@ sub volume_snapshot_rollback { die "can't rollback snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/; - my $path = $class->filesystem_path($scfg, $volname); - - my $cmd = ['/usr/bin/qemu-img', 'snapshot','-a', $snap, $path]; + if ($scfg->{snapext}) { + #simply delete the current snapshot and recreate it + eval { free_snap_image($class, $storeid, $scfg, $volname, 'current') }; + if ($@) { + die "can't delete old volume $volname: $@\n"; + } - run_command($cmd); + eval { alloc_backed_image($class, $storeid, $scfg, $volname, $snap) }; + if ($@) { + die "can't allocate new volume $volname: $@\n"; + } + } else { + my $path = $class->filesystem_path($scfg, $volname); + my $cmd = ['/usr/bin/qemu-img', 'snapshot','-a', $snap, $path]; + run_command($cmd); + } return undef; } @@ -1159,15 +1245,71 @@ sub volume_snapshot_delete { die "can't delete snapshot for this image format\n" if $volname !~ m/\.(qcow2|qed)$/; - return 1 if $running; + my $cmd = ""; - my $path = $class->filesystem_path($scfg, $volname); + if ($scfg->{snapext}) { - $class->deactivate_volume($storeid, $scfg, $volname, $snap, {}); + #qemu has already live commit|stream the snapshot, therefore we only have to drop the image itself + if ($running) { + eval { free_snap_image($class, $storeid, $scfg, $volname, $snap) }; + if ($@) { + die "can't delete snapshot $snap of volume $volname: $@\n"; + } + return; + } + + my $snapshots = $class->volume_snapshot_info($scfg, $storeid, $volname); + my $snappath = $snapshots->{$snap}->{file}; + my $snap_volname = $snapshots->{$snap}->{volname}; + die "volume $snappath is missing" if !-e $snappath; + + my $parentsnap = $snapshots->{$snap}->{parent}; + my $childsnap = $snapshots->{$snap}->{child}; + my $childpath = $snapshots->{$childsnap}->{file}; + + #if first snapshot,as it should be bigger, we merge child, and rename the snapshot to child + if(!$parentsnap) { + print "$volname: deleting snapshot '$snap' by commiting snapshot '$childsnap'\n"; + print "running 'qemu-img commit $childpath'\n"; + $cmd = ['/usr/bin/qemu-img', 'commit', $childpath]; + eval { run_command($cmd) }; + if ($@) { + warn "The state of $snap is now invalid. Don't try to clone or rollback it. You can only try to delete it again later\n"; + die "error commiting $childsnap to $snap; $@\n"; + } + + print"rename $snappath to $childpath\n"; + rename($snappath, $childpath) || + die "rename '$snappath' to '$childpath' failed - $!\n"; + + } else { + #we rebase the child image on the parent as new backing image + my $parentpath = $snapshots->{$parentsnap}->{file}; + print "$volname: deleting snapshot '$snap' by rebasing '$childsnap' on top of '$parentsnap'\n"; + print "running 'qemu-img rebase -b $parentpath -F qcow -f qcow2 $childpath'\n"; + $cmd = ['/usr/bin/qemu-img', 'rebase', '-b', $parentpath, '-F', 'qcow2', '-f', 'qcow2', $childpath]; + eval { run_command($cmd) }; + if ($@) { + #in case of abort, the state of the snap is still clean, just a little bit bigger + die "error rebase $childsnap from $parentsnap; $@\n"; + } + #delete the old snapshot file (not part of the backing chain anymore) + eval { free_snap_image($class, $storeid, $scfg, $volname, $snap) }; + if ($@) { + die "error delete old snapshot volume $snap_volname: $@\n"; + } + } + + } else { - my $cmd = ['/usr/bin/qemu-img', 'snapshot','-d', $snap, $path]; + return 1 if $running; - run_command($cmd); + my $path = $class->filesystem_path($scfg, $volname); + $class->deactivate_volume($storeid, $scfg, $volname, $snap, {}); + + $cmd = ['/usr/bin/qemu-img', 'snapshot','-d', $snap, $path]; + run_command($cmd); + } return undef; } @@ -1441,7 +1583,52 @@ sub status { sub volume_snapshot_info { my ($class, $scfg, $storeid, $volname) = @_; - die "volume_snapshot_info is not implemented for $class"; + my $path = $class->filesystem_path($scfg, $volname); + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname); + + my $json = PVE::Storage::Common::qemu_img_info($path, undef, 10, 1); + die "failed to query file information with qemu-img\n" if !$json; + my $json_decode = eval { decode_json($json) }; + if ($@) { + die "Can't decode qemu snapshot list. Invalid JSON: $@\n"; + } + my $info = {}; + my $order = 0; + if (ref($json_decode) eq 'HASH') { + #internal snapshots is a hashref + my $snapshots = $json_decode->{snapshots}; + for my $snap (@$snapshots) { + my $snapname = $snap->{name}; + $info->{$snapname}->{order} = $snap->{id}; + $info->{$snapname}->{timestamp} = $snap->{'date-sec'}; + + } + } elsif (ref($json_decode) eq 'ARRAY') { + #no snapshot or external snapshots is an arrayref + my $snapshots = $json_decode; + for my $snap (@$snapshots) { + my $snapfile = $snap->{filename}; + my $snapname = parse_snapname($snapfile); + $snapname = 'current' if !$snapname; + my $snapvolname = $class->get_snap_volname($volname, $snapname); + + $info->{$snapname}->{order} = $order; + $info->{$snapname}->{file}= $snapfile; + $info->{$snapname}->{volname} = "$snapvolname"; + $info->{$snapname}->{volid} = "$storeid:$snapvolname"; + $info->{$snapname}->{ext} = 1; + + my $parentfile = $snap->{'backing-filename'}; + if ($parentfile) { + my $parentname = parse_snapname($parentfile); + $info->{$snapname}->{parent} = $parentname; + $info->{$parentname}->{child} = $snapname; + } + $order++; + } + } + + return $info; } sub activate_storage { @@ -1896,7 +2083,7 @@ sub qemu_blockdev_options { # the snapshot alone. my $format = ($class->parse_volname($volname))[6]; die "cannot attach only the snapshot of a '$format' image\n" - if $options->{'snapshot-name'} && ($format eq 'qcow2' || $format eq 'qed'); + if $options->{'snapshot-name'} && !$scfg->{snapext} && ($format eq 'qcow2' || $format eq 'qed'); # The 'file' driver only works for regular files. The check below is taken from # block/file-posix.c:hdev_probe_device() in QEMU. Do not bother with detecting 'host_cdrom' @@ -1935,4 +2122,30 @@ sub config_aware_base_mkdir { } } +sub get_snap_name { + my ($class, $volname, $snapname) = @_; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname); + $name = !$snapname || $snapname eq 'current' ? $name : "snap-$snapname-$name"; + return $name; +} + +sub get_snap_volname { + my ($class, $volname, $snapname) = @_; + + my $vmid = ($class->parse_volname($volname))[2]; + my $name = $class->get_snap_name($volname, $snapname); + return "$vmid/$name"; +} + +sub parse_snapname { + my ($name) = @_; + + my $basename = basename($name); + if ($basename =~ m/^snap-(.*)-vm(.*)$/) { + return $1; + } + return undef; +} + 1; -- 2.39.5 --===============6681596957087559907== 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 --===============6681596957087559907==--