mirror of
https://github.com/TheGrandWazoo/freenas-proxmox.git
synced 2026-10-22 06:28:20 +02:00
feat: implement volume_resize for truenas and truenas-multipath (#294)
Without an override the base Plugin.pm resize dies "can't resize this
image format", which blocked qm disk resize, the GUI and Terraform's
cloud-image provisioning. Grows the zvol via pool/dataset update
(1 MiB-aligned, integer volsize for SCALE's Pydantic). The WebSocket
transport gains the PUT -> pool.dataset.update mapping it needs.
Multipath additionally rescans the target's kernel sessions and runs
multipathd resize map when the device is active on this node, since
QMP block_resize on a host_device can't grow past the stale dm size.
Backport note: release/3.x is REST-only, so the WebSocket PUT ->
pool.dataset.update mapping from the original commit is dropped.
(cherry picked from commit 312cfa8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8046880499
commit
d2c8dd21af
@@ -35,6 +35,7 @@ This plugin is a native `PVE::Storage::Custom` type — Proxmox discovers it aut
|
||||
- Bearer Token (API key) authentication — no username/password, no SSH keys
|
||||
- Per-VM iSCSI targets (`proxmox-vm-<vmid>`) — removing one disk never affects other VMs
|
||||
- Automatic rollback when operations fail (no dangling iSCSI extents)
|
||||
- Disk resize, including on a running VM. Cloud-init drives are supported (v4.0.2+).
|
||||
- Supports TrueNAS CORE 13.x and TrueNAS SCALE 24.10–25.10
|
||||
|
||||
---
|
||||
@@ -607,6 +608,18 @@ If you see iSCSI extents in TrueNAS that are not associated with any target, the
|
||||
perl -MPVE::Storage -e 'PVE::Storage::vdisk_free(PVE::Storage::config(), "STORAGE:vm-101-disk-0")'
|
||||
```
|
||||
|
||||
### Resize, disk info or cloud-init operations fail on 4.0.1 and earlier
|
||||
|
||||
These errors come from plugin gaps that are **fixed in v4.0.2** ([#294](https://github.com/TheGrandWazoo/freenas-proxmox/issues/294)):
|
||||
|
||||
| Error | Cause | Workaround before upgrading |
|
||||
|---|---|---|
|
||||
| `can't resize this image format` (`qm disk resize`, GUI Resize, Terraform) | No `volume_resize` | Grow the zvol in TrueNAS (Datasets → zvol → Edit → Size). Then run `qm rescan --vmid <id>` so Proxmox picks up the new size. |
|
||||
| `volume_size_info on '<volid>' failed - no format` (content API, `bpg/proxmox` Terraform refresh) | `volume_size_info` returned only a size | None. Upgrade. |
|
||||
| `unable to parse TrueNAS volume name 'vm-<id>-cloudinit'` (`qm destroy`, cloud-init regenerate) | `parse_volname` rejected cloud-init names | Remove the cloud-init drive from the VM. Then delete `vm-<id>-cloudinit` in TrueNAS along with its iSCSI extent (and the `proxmox-vm-<id>` target if it's now empty). |
|
||||
|
||||
Resizing only grows the disk. You still need to grow the partition and filesystem inside the guest yourself (cloud-init's `growpart` does this on next boot).
|
||||
|
||||
### `qm destroy` left multipath disks behind on TrueNAS
|
||||
|
||||
**Symptom:** you destroy a VM whose disks are on a `truenas-multipath` storage. The task reports `OK`, but the zvols, extents and the `proxmox-vm-<id>` target are still on TrueNAS. The journal has no `free_image` line for those disks.
|
||||
|
||||
@@ -634,6 +634,33 @@ sub volume_size_info {
|
||||
return wantarray ? ($size, $format, $used, $parent) : $size;
|
||||
}
|
||||
|
||||
# Grows the zvol. Without this override the base Plugin.pm version dies with
|
||||
# "can't resize this image format" (#294). For a running VM, qemu-server then
|
||||
# issues QMP block_resize, which makes libiscsi re-read the LUN capacity —
|
||||
# TrueNAS resizes the LUN with the zvol, so no iSCSI-side call is needed here.
|
||||
sub volume_resize {
|
||||
my ($class, $scfg, $storeid, $volname, $size, $running, $snapname) = @_;
|
||||
_resolve_token($storeid, $scfg);
|
||||
|
||||
die "resizing a snapshot is not supported\n" if $snapname;
|
||||
$class->parse_volname($volname); # dies on names we don't own
|
||||
|
||||
# Align to 1 MiB like ZFSPoolPlugin, so volsize is always a multiple of
|
||||
# the zvol's volblocksize (TrueNAS rejects anything else).
|
||||
my $new_kb = int($size / 1024);
|
||||
$new_kb += (1024 - $new_kb % 1024) % 1024;
|
||||
|
||||
my $zvol = _zvol_prefix($scfg) . "/$volname";
|
||||
my $enc = uri_escape($zvol, "^A-Za-z0-9\\-_.~");
|
||||
|
||||
_log('info', "volume_resize: $zvol to ${new_kb}K");
|
||||
|
||||
# int() for a fresh IV: SCALE 25.10 Pydantic rejects string-typed integers.
|
||||
_api($scfg, 'PUT', "/pool/dataset/id/$enc", { volsize => int($new_kb * 1024) });
|
||||
|
||||
return $new_kb;
|
||||
}
|
||||
|
||||
sub path {
|
||||
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
|
||||
_resolve_token($storeid, $scfg);
|
||||
|
||||
@@ -116,6 +116,36 @@ sub path {
|
||||
return ("/dev/mapper/$wwid", $ds_vmid, $vtype);
|
||||
}
|
||||
|
||||
# ── volume_resize — grow zvol, then refresh kernel view of the LUN ────────────
|
||||
|
||||
# The base class grows the zvol. If the volume is active on this node, the
|
||||
# kernel iSCSI sessions and the dm map still report the old size, and
|
||||
# qemu-server's QMP block_resize on the host_device would fail. Rescan every
|
||||
# session to the target, then have multipathd resize the map.
|
||||
sub volume_resize {
|
||||
my ($class, $scfg, $storeid, $volname, $size, $running, $snapname) = @_;
|
||||
|
||||
my $new_kb = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running, $snapname);
|
||||
|
||||
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
|
||||
my $wwid = $ext ? _naa_to_wwid($ext->{naa}) : undef;
|
||||
return $new_kb unless $wwid && -b "/dev/mapper/$wwid" && defined $ext->{target_id};
|
||||
|
||||
my $t = PVE::Storage::Custom::TrueNAS::_api($scfg, 'GET',
|
||||
"/iscsi/target/id/$ext->{target_id}") // {};
|
||||
my $iqn = PVE::Storage::Custom::TrueNAS::_basename($scfg) . ":$t->{name}";
|
||||
|
||||
my ($rc, $out) = _iscsiadm('-m', 'node', '-T', $iqn, '-R');
|
||||
die "iscsiadm rescan of $iqn failed (rc=$rc): $out\n" if $rc;
|
||||
|
||||
my $multipathd = -x '/usr/sbin/multipathd' ? '/usr/sbin/multipathd' : '/sbin/multipathd';
|
||||
system($multipathd, 'resize', 'map', $wwid) == 0
|
||||
or die "multipathd resize map $wwid failed (rc=" . ($? >> 8) . ")\n";
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_log('info', "multipath resize: $wwid rescanned and resized");
|
||||
return $new_kb;
|
||||
}
|
||||
|
||||
# ── qemu_blockdev_options — delegate to Plugin.pm (host_device for /dev/mapper) ─
|
||||
|
||||
# TrueNAS.pm overrides qemu_blockdev_options to return an iscsi:// blockdev —
|
||||
|
||||
@@ -66,7 +66,7 @@ sub _route {
|
||||
}
|
||||
}
|
||||
|
||||
return if $method eq 'DELETE' || $method eq 'POST';
|
||||
return if $method eq 'DELETE' || $method eq 'POST' || $method eq 'PUT';
|
||||
|
||||
die "MockTrueNAS: unhandled $method $path\n";
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/perl
|
||||
# volume_resize (#294): grows the zvol via PUT /pool/dataset/id/<zvol> with a
|
||||
# 1 MiB-aligned integer volsize, returns the new size in KiB (ZFSPoolPlugin
|
||||
# convention), and refuses snapshots / foreign volume names.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/lib", "$FindBin::Bin/../perl5";
|
||||
|
||||
use Test::More;
|
||||
use JSON ();
|
||||
|
||||
use MockTrueNAS;
|
||||
use PVE::Storage::Custom::TrueNAS;
|
||||
use PVE::Storage::Custom::TrueNASMultipath;
|
||||
|
||||
my $TN = 'PVE::Storage::Custom::TrueNAS';
|
||||
my $MP = 'PVE::Storage::Custom::TrueNASMultipath';
|
||||
my $GiB = 1024 ** 3;
|
||||
|
||||
sub puts { my ($api) = @_; return grep { $_->[0] eq 'PUT' } @{ $api->calls }; }
|
||||
|
||||
subtest 'grows zvol to the requested size' => sub {
|
||||
my $api = MockTrueNAS->install();
|
||||
my $kb = $TN->volume_resize(MockTrueNAS::scfg(), 'store', 'vm-100-disk-0', 40 * $GiB, 0);
|
||||
is($kb, 40 * 1024 * 1024, 'returns new size in KiB');
|
||||
|
||||
my @p = puts($api);
|
||||
is(scalar @p, 1, 'one PUT');
|
||||
is($p[0][1], '/pool/dataset/id/tank%2Fvm%2Fvm-100-disk-0', 'targets the zvol');
|
||||
is($p[0][2]{volsize}, 40 * $GiB, 'volsize in bytes');
|
||||
is(JSON->new->canonical->encode($p[0][2]), '{"volsize":' . (40 * $GiB) . '}',
|
||||
'volsize encodes as a JSON integer, not a string');
|
||||
};
|
||||
|
||||
subtest 'rounds up to a 1 MiB boundary' => sub {
|
||||
my $api = MockTrueNAS->install();
|
||||
my $kb = $TN->volume_resize(MockTrueNAS::scfg(), 'store', 'vm-100-disk-0', 10 * $GiB + 1536, 0);
|
||||
is($kb, 10 * 1024 * 1024 + 1024, 'next whole MiB');
|
||||
is((puts($api))[0][2]{volsize} % (1024 * 1024), 0, 'volsize is MiB-aligned');
|
||||
};
|
||||
|
||||
subtest 'cloud-init drive can be resized' => sub {
|
||||
my $api = MockTrueNAS->install();
|
||||
$TN->volume_resize(MockTrueNAS::scfg(), 'store', 'vm-100-cloudinit', 8 * 1024 * 1024, 0);
|
||||
is(scalar(puts($api)), 1, 'PUT issued');
|
||||
};
|
||||
|
||||
subtest 'snapshot resize refused' => sub {
|
||||
my $api = MockTrueNAS->install();
|
||||
eval { $TN->volume_resize(MockTrueNAS::scfg(), 'store', 'vm-100-disk-0', $GiB, 0, 'snap1') };
|
||||
like($@, qr/snapshot/, 'dies');
|
||||
is(scalar(puts($api)), 0, 'no API write');
|
||||
};
|
||||
|
||||
subtest 'foreign volume name refused before any API write' => sub {
|
||||
my $api = MockTrueNAS->install();
|
||||
eval { $TN->volume_resize(MockTrueNAS::scfg(), 'store', '../other-zvol', $GiB, 0) };
|
||||
like($@, qr/unable to parse/, 'dies');
|
||||
is(scalar(puts($api)), 0, 'no API write');
|
||||
};
|
||||
|
||||
subtest 'multipath: inactive volume only grows the zvol' => sub {
|
||||
my $api = MockTrueNAS->install(
|
||||
extents => [ { id => 3, name => 'vm-100-disk-0', naa => '0x6589cfc000000abc' } ],
|
||||
targetextents => [ { id => 5, extent => 3, target => 7, lunid => 0 } ],
|
||||
targets => [ { id => 7, name => 'proxmox-vm-100' } ],
|
||||
);
|
||||
# /dev/mapper/36589cfc000000abc does not exist here, so no rescan is attempted.
|
||||
my $kb = $MP->volume_resize(MockTrueNAS::scfg(), 'store', 'vm-100-disk-0', 20 * $GiB, 0);
|
||||
is($kb, 20 * 1024 * 1024, 'returns new size in KiB');
|
||||
is(scalar(puts($api)), 1, 'zvol PUT issued');
|
||||
};
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user