feat: truenas-proxmox-multipath plugin and packaging (#256)
Adds TrueNASMultipath.pm — a separate PVE storage type 'truenas-multipath'
that inherits all REST API and iSCSI resource management from TrueNAS.pm but
uses iscsiadm + dm-multipath for block device access:
path() → /dev/mapper/<wwid> (NAA from TrueNAS extent)
activate_volume() → iscsiadm login on each portal in truenas_portals;
waits for /dev/mapper device to appear
deactivate_volume()→ multipath flush + iscsiadm logout
qemu_blockdev_options → returns undef (path() used instead)
Packaging:
packaging/DEBIAN-multipath/ — control.j2, postinst, postrm,
changelog.Debian, multipath.conf.example
Distributed via the 'multipath' apt component (ADR-011)
Depends: truenas-proxmox >= 3.1.0, open-iscsi, multipath-tools
CI (build.yml):
- Third staging dir (dist-multipath) and .deb output
- multipath component pool under pool/multipath/v<major>
- Both stable and testing publish steps updated to generate
main + multipath components; Release lists both
- Lint checks TrueNASMultipath.pm and multipath packaging scripts
PoC confirmed 2026-06-07 on pve01-hq against TrueNAS SCALE 24.10:
- Two sessions (172.31.69.91 + 192.168.69.91), both active
- dm-multipath aggregated 3 disks × 2 paths each
- /dev/mapper/mpath* devices appeared correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0ce11dc6ba
commit
0d195a9074
|
|
@ -46,12 +46,16 @@ jobs:
|
|||
echo "==> Checking Perl syntax..."
|
||||
perl -c -I/tmp/pve-stub perl5/PVE/Storage/Custom/TrueNAS.pm \
|
||||
&& echo "TrueNAS.pm: OK"
|
||||
perl -c -I/tmp/pve-stub -I/tmp/pve-stub/.. \
|
||||
perl5/PVE/Storage/Custom/TrueNASMultipath.pm \
|
||||
&& echo "TrueNASMultipath.pm: OK"
|
||||
|
||||
- name: Perl static analysis (perlcritic)
|
||||
run: |
|
||||
echo "==> Running perlcritic..."
|
||||
perlcritic --profile .perlcriticrc \
|
||||
perl5/PVE/Storage/Custom/TrueNAS.pm
|
||||
perl5/PVE/Storage/Custom/TrueNAS.pm \
|
||||
perl5/PVE/Storage/Custom/TrueNASMultipath.pm
|
||||
|
||||
- name: Shell script lint (shellcheck)
|
||||
run: |
|
||||
|
|
@ -59,7 +63,9 @@ jobs:
|
|||
shellcheck --severity=warning \
|
||||
packaging/DEBIAN/postinst \
|
||||
packaging/DEBIAN/postrm \
|
||||
packaging/DEBIAN-transitional/postinst
|
||||
packaging/DEBIAN-transitional/postinst \
|
||||
packaging/DEBIAN-multipath/postinst \
|
||||
packaging/DEBIAN-multipath/postrm
|
||||
echo "Shell scripts OK"
|
||||
|
||||
# ── Job 2: Build .deb ────────────────────────────────────────────────────────
|
||||
|
|
@ -72,6 +78,7 @@ jobs:
|
|||
version: ${{ steps.vars.outputs.version }}
|
||||
deb_file: ${{ steps.vars.outputs.deb_file }}
|
||||
transitional_deb_file: ${{ steps.vars.outputs.transitional_deb_file }}
|
||||
multipath_deb_file: ${{ steps.vars.outputs.multipath_deb_file }}
|
||||
channel: ${{ steps.vars.outputs.channel }}
|
||||
cloudsmith_repo: ${{ steps.vars.outputs.cloudsmith_repo }}
|
||||
is_release: ${{ steps.vars.outputs.is_release }}
|
||||
|
|
@ -129,10 +136,12 @@ jobs:
|
|||
|
||||
DEB_FILE="${PACKAGE_NAME}_${VERSION}_all.deb"
|
||||
TRANSITIONAL_DEB_FILE="${TRANSITIONAL_PACKAGE_NAME}_${VERSION}_all.deb"
|
||||
MULTIPATH_DEB_FILE="truenas-proxmox-multipath_${VERSION}_all.deb"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT"
|
||||
echo "transitional_deb_file=${TRANSITIONAL_DEB_FILE}" >> "$GITHUB_OUTPUT"
|
||||
echo "multipath_deb_file=${MULTIPATH_DEB_FILE}" >> "$GITHUB_OUTPUT"
|
||||
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
|
||||
echo "cloudsmith_repo=${CLOUDSMITH_REPO}" >> "$GITHUB_OUTPUT"
|
||||
echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT"
|
||||
|
|
@ -198,21 +207,49 @@ jobs:
|
|||
echo "==> Transitional package contents:"
|
||||
find "${STAGING}" | sort
|
||||
|
||||
- name: Assemble multipath package staging directory
|
||||
run: |
|
||||
VERSION="${{ steps.vars.outputs.version }}"
|
||||
STAGING="dist-multipath"
|
||||
|
||||
mkdir -p "${STAGING}/DEBIAN"
|
||||
mkdir -p "${STAGING}/usr/share/truenas-proxmox-multipath"
|
||||
|
||||
sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN-multipath/control.j2 \
|
||||
> "${STAGING}/DEBIAN/control"
|
||||
|
||||
cp packaging/DEBIAN-multipath/postinst "${STAGING}/DEBIAN/postinst"
|
||||
cp packaging/DEBIAN-multipath/postrm "${STAGING}/DEBIAN/postrm"
|
||||
chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm"
|
||||
|
||||
cp perl5/PVE/Storage/Custom/TrueNASMultipath.pm \
|
||||
"${STAGING}/usr/share/truenas-proxmox-multipath/TrueNASMultipath.pm"
|
||||
cp packaging/DEBIAN-multipath/multipath.conf.example \
|
||||
"${STAGING}/usr/share/truenas-proxmox-multipath/multipath.conf.example"
|
||||
|
||||
mkdir -p "${STAGING}/usr/share/doc/truenas-proxmox-multipath"
|
||||
gzip -9c packaging/DEBIAN-multipath/changelog.Debian \
|
||||
> "${STAGING}/usr/share/doc/truenas-proxmox-multipath/changelog.Debian.gz"
|
||||
|
||||
echo "==> Multipath package contents:"
|
||||
find "${STAGING}" | sort
|
||||
|
||||
- name: Build .deb packages
|
||||
run: |
|
||||
sudo dpkg-deb -Zgzip --build dist "${{ steps.vars.outputs.deb_file }}"
|
||||
sudo dpkg-deb -Zgzip --build dist-transitional "${{ steps.vars.outputs.transitional_deb_file }}"
|
||||
sudo dpkg-deb -Zgzip --build dist-multipath "${{ steps.vars.outputs.multipath_deb_file }}"
|
||||
|
||||
- name: Verify .deb packages
|
||||
run: |
|
||||
echo "==> Package info (truenas-proxmox):"
|
||||
dpkg-deb --info "${{ steps.vars.outputs.deb_file }}"
|
||||
echo ""
|
||||
echo "==> Package contents (truenas-proxmox):"
|
||||
dpkg-deb --contents "${{ steps.vars.outputs.deb_file }}"
|
||||
echo ""
|
||||
echo "==> Package info (freenas-proxmox transitional):"
|
||||
dpkg-deb --info "${{ steps.vars.outputs.transitional_deb_file }}"
|
||||
echo ""
|
||||
echo "==> Package info (truenas-proxmox-multipath):"
|
||||
dpkg-deb --info "${{ steps.vars.outputs.multipath_deb_file }}"
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
@ -221,6 +258,7 @@ jobs:
|
|||
path: |
|
||||
${{ steps.vars.outputs.deb_file }}
|
||||
${{ steps.vars.outputs.transitional_deb_file }}
|
||||
${{ steps.vars.outputs.multipath_deb_file }}
|
||||
retention-days: 30
|
||||
|
||||
# ── Job 3: Security scan ─────────────────────────────────────────────────────
|
||||
|
|
@ -340,54 +378,55 @@ jobs:
|
|||
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
|
||||
"$PAGES_DIR"
|
||||
|
||||
# Add .deb files to the versioned pool directory
|
||||
# Add .deb files to the versioned pool directories
|
||||
mkdir -p "${PAGES_DIR}/pool/v${MAJOR}"
|
||||
mkdir -p "${PAGES_DIR}/pool/multipath/v${MAJOR}"
|
||||
cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/"
|
||||
cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/"
|
||||
cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/v${MAJOR}/"
|
||||
|
||||
# Install apt tooling
|
||||
sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils
|
||||
|
||||
# Determine which dists to update
|
||||
# v2.x: update v2, main (alias), and v2 (explicit)
|
||||
# v3.x and above: update only the versioned dist
|
||||
if [[ "$MAJOR" == "2" ]]; then
|
||||
DISTS="v2 main"
|
||||
else
|
||||
DISTS="v${MAJOR}"
|
||||
fi
|
||||
|
||||
for DIST in $DISTS; do
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-all"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-amd64"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-arm64"
|
||||
|
||||
# Generate Packages file (paths relative to repo root)
|
||||
dpkg-scanpackages --multiversion "${PAGES_DIR}/pool/v${MAJOR}" \
|
||||
_gen_component() {
|
||||
local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64"
|
||||
dpkg-scanpackages --multiversion "$POOL" \
|
||||
| sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \
|
||||
> "${PAGES_DIR}/dists/${DIST}/main/binary-all/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-all/Packages"
|
||||
> "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
|
||||
}
|
||||
|
||||
# Empty arch-specific Packages — all our packages are Architecture: all;
|
||||
# these files silence the apt "doesn't support architecture" warning on
|
||||
# amd64 and arm64 hosts without duplicating package entries.
|
||||
touch "${PAGES_DIR}/dists/${DIST}/main/binary-amd64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-amd64/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/main/binary-arm64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-arm64/Packages"
|
||||
for DIST in $DISTS; do
|
||||
_gen_component "$PAGES_DIR" "$DIST" "main" \
|
||||
"${PAGES_DIR}/pool/v${MAJOR}"
|
||||
_gen_component "$PAGES_DIR" "$DIST" "multipath" \
|
||||
"${PAGES_DIR}/pool/multipath/v${MAJOR}"
|
||||
|
||||
# Generate Release file
|
||||
# Generate Release file listing both components
|
||||
apt-ftparchive \
|
||||
-o "APT::FTPArchive::Release::Origin=truenas-proxmox" \
|
||||
-o "APT::FTPArchive::Release::Label=truenas-proxmox" \
|
||||
-o "APT::FTPArchive::Release::Suite=${DIST}" \
|
||||
-o "APT::FTPArchive::Release::Codename=${DIST}" \
|
||||
-o "APT::FTPArchive::Release::Components=main" \
|
||||
-o "APT::FTPArchive::Release::Components=main multipath" \
|
||||
-o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \
|
||||
release "${PAGES_DIR}/dists/${DIST}" \
|
||||
> "${PAGES_DIR}/dists/${DIST}/Release"
|
||||
|
||||
# Sign → InRelease
|
||||
gpg --batch --yes \
|
||||
--passphrase "${APT_SIGNING_KEY_PASSPHRASE}" \
|
||||
--default-key "${GPG_KEY_ID}" \
|
||||
|
|
@ -433,32 +472,39 @@ jobs:
|
|||
# Replace pool/testing/ contents with the latest build only
|
||||
rm -rf "${PAGES_DIR}/pool/testing"
|
||||
mkdir -p "${PAGES_DIR}/pool/testing"
|
||||
mkdir -p "${PAGES_DIR}/pool/multipath/testing"
|
||||
cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/testing/"
|
||||
cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/testing/"
|
||||
cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/testing/"
|
||||
|
||||
sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils
|
||||
|
||||
DIST="testing"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-all"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-amd64"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/main/binary-arm64"
|
||||
|
||||
dpkg-scanpackages --multiversion "${PAGES_DIR}/pool/testing" \
|
||||
| sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \
|
||||
> "${PAGES_DIR}/dists/${DIST}/main/binary-all/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-all/Packages"
|
||||
_gen_component() {
|
||||
local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64"
|
||||
mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64"
|
||||
dpkg-scanpackages --multiversion "$POOL" \
|
||||
| sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \
|
||||
> "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages"
|
||||
}
|
||||
|
||||
touch "${PAGES_DIR}/dists/${DIST}/main/binary-amd64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-amd64/Packages"
|
||||
touch "${PAGES_DIR}/dists/${DIST}/main/binary-arm64/Packages"
|
||||
gzip -kf "${PAGES_DIR}/dists/${DIST}/main/binary-arm64/Packages"
|
||||
_gen_component "$PAGES_DIR" "$DIST" "main" "${PAGES_DIR}/pool/testing"
|
||||
_gen_component "$PAGES_DIR" "$DIST" "multipath" "${PAGES_DIR}/pool/multipath/testing"
|
||||
|
||||
apt-ftparchive \
|
||||
-o "APT::FTPArchive::Release::Origin=truenas-proxmox" \
|
||||
-o "APT::FTPArchive::Release::Label=truenas-proxmox" \
|
||||
-o "APT::FTPArchive::Release::Suite=${DIST}" \
|
||||
-o "APT::FTPArchive::Release::Codename=${DIST}" \
|
||||
-o "APT::FTPArchive::Release::Components=main" \
|
||||
-o "APT::FTPArchive::Release::Components=main multipath" \
|
||||
-o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \
|
||||
release "${PAGES_DIR}/dists/${DIST}" \
|
||||
> "${PAGES_DIR}/dists/${DIST}/Release"
|
||||
|
|
@ -487,6 +533,7 @@ jobs:
|
|||
files: |
|
||||
${{ needs.build.outputs.deb_file }}
|
||||
${{ needs.build.outputs.transitional_deb_file }}
|
||||
${{ needs.build.outputs.multipath_deb_file }}
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
## truenas-proxmox v${{ needs.build.outputs.version }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
truenas-proxmox-multipath (3.1.0-1) bookworm; urgency=low
|
||||
|
||||
* Initial release — kernel iSCSI multipath via iscsiadm + dm-multipath
|
||||
* Registers 'truenas-multipath' PVE storage type
|
||||
* Inherits all REST API and iSCSI resource management from truenas-proxmox
|
||||
* path() returns /dev/mapper/<wwid> for QEMU block device access
|
||||
* activate_volume() logs in via all configured portals
|
||||
* Distributed via the 'multipath' apt component
|
||||
|
||||
-- KSA Technologies, LLC <theprofessor@ksatechnologies.com> Sun, 07 Jun 2026 00:00:00 +0000
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Package: truenas-proxmox-multipath
|
||||
Version: ${VERSION}
|
||||
Architecture: all
|
||||
Maintainer: KSA Technologies, LLC <theprofessor@ksatechnologies.com>
|
||||
Depends: truenas-proxmox (>= 3.1.0), open-iscsi, multipath-tools
|
||||
Section: perl
|
||||
Priority: optional
|
||||
Homepage: https://github.com/TheGrandWazoo/truenas-proxmox
|
||||
Description: TrueNAS Multipath Storage Plugin for Proxmox VE
|
||||
Extends truenas-proxmox with kernel iSCSI multipath support via iscsiadm
|
||||
and dm-multipath. Registers a separate 'truenas-multipath' storage type
|
||||
that presents /dev/mapper/<wwid> block devices to QEMU instead of
|
||||
iscsi:// URIs, enabling active/active multipath with failover.
|
||||
.
|
||||
Requires two or more network paths between Proxmox and TrueNAS and
|
||||
TrueNAS iSCSI portals configured on each path.
|
||||
.
|
||||
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
|
||||
Copyright (c) 2020 KSA Technologies, LLC.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# /etc/multipath.conf — installed by truenas-proxmox-multipath
|
||||
# This file is only created if /etc/multipath.conf does not already exist.
|
||||
# If you have an existing multipath.conf, merge the 'devices' section below
|
||||
# into it manually.
|
||||
|
||||
defaults {
|
||||
find_multipaths yes
|
||||
}
|
||||
|
||||
devices {
|
||||
device {
|
||||
vendor "TrueNAS"
|
||||
product "iSCSI Disk"
|
||||
path_grouping_policy multibus
|
||||
path_checker tur
|
||||
failback immediate
|
||||
rr_weight uniform
|
||||
no_path_retry 5
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#!/bin/bash
|
||||
# postinst: truenas-proxmox-multipath install/upgrade script
|
||||
set -e
|
||||
|
||||
INSTALL_DIR="/usr/share/truenas-proxmox-multipath"
|
||||
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNASMultipath.pm"
|
||||
MULTIPATH_CONF="/etc/multipath.conf"
|
||||
LOG_FILE="/var/log/truenas-proxmox-install.log"
|
||||
|
||||
log() {
|
||||
echo "[truenas-proxmox-multipath] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
install_plugin() {
|
||||
log "Installing ${PLUGIN_DST}"
|
||||
mkdir -p "$(dirname "$PLUGIN_DST")"
|
||||
cp "${INSTALL_DIR}/TrueNASMultipath.pm" "$PLUGIN_DST"
|
||||
}
|
||||
|
||||
install_multipath_conf() {
|
||||
if [ -f "$MULTIPATH_CONF" ]; then
|
||||
log "${MULTIPATH_CONF} already exists — not overwriting"
|
||||
log "Ensure it contains a 'devices' section for TrueNAS iSCSI Disk"
|
||||
log "See ${INSTALL_DIR}/multipath.conf.example for reference"
|
||||
return 0
|
||||
fi
|
||||
log "Installing ${MULTIPATH_CONF}"
|
||||
cp "${INSTALL_DIR}/multipath.conf.example" "$MULTIPATH_CONF"
|
||||
systemctl enable multipathd 2>/dev/null || true
|
||||
systemctl start multipathd 2>/dev/null || true
|
||||
log "multipathd enabled and started"
|
||||
}
|
||||
|
||||
restart_pve_services() {
|
||||
log "Restarting Proxmox VE services ..."
|
||||
pvedaemon restart && log "pvedaemon restarted"
|
||||
pveproxy restart && log "pveproxy restarted"
|
||||
log "Done. Refresh your Proxmox browser tab."
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
log "Configuring truenas-proxmox-multipath (previous version: ${2:-none})"
|
||||
install_plugin
|
||||
install_multipath_conf
|
||||
restart_pve_services
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
*)
|
||||
echo "$0: called with unknown argument '$1'" >&2
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
# postrm: truenas-proxmox-multipath remove/purge script
|
||||
set -e
|
||||
|
||||
PLUGIN_DST="/usr/share/perl5/PVE/Storage/Custom/TrueNASMultipath.pm"
|
||||
LOG_FILE="/var/log/truenas-proxmox-install.log"
|
||||
|
||||
log() {
|
||||
echo "[truenas-proxmox-multipath] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
remove|purge)
|
||||
if [ -f "$PLUGIN_DST" ]; then
|
||||
log "Removing ${PLUGIN_DST}"
|
||||
rm -f "$PLUGIN_DST"
|
||||
fi
|
||||
|
||||
log "Restarting Proxmox VE services ..."
|
||||
pvedaemon restart 2>/dev/null && log "pvedaemon restarted" || true
|
||||
pveproxy restart 2>/dev/null && log "pveproxy restarted" || true
|
||||
log "Done."
|
||||
;;
|
||||
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
;;
|
||||
*)
|
||||
echo "$0: called with unknown argument '$1'" >&2
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package PVE::Storage::Custom::TrueNASMultipath;
|
||||
|
||||
# TrueNAS Multipath Storage Plugin for Proxmox VE
|
||||
#
|
||||
# Extends TrueNAS.pm with kernel iSCSI multipath support (iscsiadm + dm-multipath).
|
||||
# Registers as PVE storage type 'truenas-multipath'.
|
||||
#
|
||||
# Requires: truenas-proxmox >= 3.1.0, open-iscsi, multipath-tools
|
||||
# Auth: Same Bearer token model as TrueNAS.pm
|
||||
# Path: /dev/mapper/<wwid> instead of iscsi:// — QEMU uses the block device directly
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Storage::Custom::TrueNAS;
|
||||
use base qw(PVE::Storage::Custom::TrueNAS);
|
||||
|
||||
# ── Plugin identity ───────────────────────────────────────────────────────────
|
||||
|
||||
sub type { return 'truenas-multipath'; }
|
||||
|
||||
sub plugindata {
|
||||
return {
|
||||
content => [ { images => 1 }, { images => 1 } ],
|
||||
format => [ { raw => 1 }, 'raw' ],
|
||||
};
|
||||
}
|
||||
|
||||
# ── Config properties (extends parent) ───────────────────────────────────────
|
||||
|
||||
sub properties {
|
||||
my $props = PVE::Storage::Custom::TrueNAS::properties();
|
||||
$props->{truenas_portals} = {
|
||||
description => "Comma-separated iSCSI portal IP addresses for multipath "
|
||||
. "(e.g. '172.31.69.91,192.168.69.91'). Each portal is logged "
|
||||
. "into separately; dm-multipath aggregates the sessions.",
|
||||
type => 'string',
|
||||
};
|
||||
return $props;
|
||||
}
|
||||
|
||||
sub options {
|
||||
my $opts = PVE::Storage::Custom::TrueNAS::options();
|
||||
$opts->{truenas_portals} = { optional => 0 };
|
||||
return $opts;
|
||||
}
|
||||
|
||||
# ── Private helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
# Parse truenas_portals into a list of IP strings.
|
||||
sub _portals {
|
||||
my ($scfg) = @_;
|
||||
return grep { length } split /\s*,\s*/, ($scfg->{truenas_portals} // '');
|
||||
}
|
||||
|
||||
# Convert TrueNAS NAA string (e.g. "0x6589cfc...") to the dm-multipath WWID
|
||||
# used in /dev/mapper/ (e.g. "36589cfc...").
|
||||
sub _naa_to_wwid {
|
||||
my ($naa) = @_;
|
||||
$naa =~ s/^0x//i;
|
||||
return "3$naa";
|
||||
}
|
||||
|
||||
# Run an iscsiadm command, returning (exit_code, output).
|
||||
# Suppresses errors that indicate a session already exists.
|
||||
sub _iscsiadm {
|
||||
my (@args) = @_;
|
||||
my $out = '';
|
||||
open(my $fh, '-|', 'iscsiadm', @args, '2>&1')
|
||||
or return (1, "failed to exec iscsiadm: $!");
|
||||
$out = do { local $/; <$fh> };
|
||||
close $fh;
|
||||
my $rc = $? >> 8;
|
||||
return ($rc, $out);
|
||||
}
|
||||
|
||||
# Wait up to $timeout seconds for a block device path to appear.
|
||||
sub _wait_for_device {
|
||||
my ($path, $timeout) = @_;
|
||||
$timeout //= 30;
|
||||
while ($timeout-- > 0) {
|
||||
return 1 if -b $path;
|
||||
sleep 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
# ── Path — block device via dm-multipath ─────────────────────────────────────
|
||||
|
||||
sub path {
|
||||
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
|
||||
|
||||
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
|
||||
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}.\n" unless $ext;
|
||||
|
||||
my $wwid = _naa_to_wwid($ext->{naa})
|
||||
or die "Volume '$volname' has no NAA/WWID on $scfg->{truenas_host}.\n";
|
||||
|
||||
return ("/dev/mapper/$wwid", $ext->{lun_id}, $storeid);
|
||||
}
|
||||
|
||||
# ── qemu_blockdev_options — not used (path returns a block device) ────────────
|
||||
|
||||
# The base class qemu_blockdev_options builds an iscsi:// blockdev.
|
||||
# For multipath we return undef so PVE uses path() instead.
|
||||
sub qemu_blockdev_options { return undef; }
|
||||
|
||||
# ── activate_volume — login via all portals, wait for mapper device ───────────
|
||||
|
||||
sub activate_volume {
|
||||
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
|
||||
|
||||
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
|
||||
die "Volume '$volname' has no iSCSI extent on $scfg->{truenas_host}.\n" unless $ext;
|
||||
die "Volume '$volname' is not mapped to any iSCSI target.\n"
|
||||
unless 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 $wwid = _naa_to_wwid($ext->{naa})
|
||||
or die "Volume '$volname' has no NAA/WWID.\n";
|
||||
my $mapper = "/dev/mapper/$wwid";
|
||||
|
||||
return if -b $mapper; # already active
|
||||
|
||||
my @portals = _portals($scfg);
|
||||
die "No portals configured — set truenas_portals in storage config.\n" unless @portals;
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_log('info',
|
||||
"multipath activate: $volname iqn=$iqn wwid=$wwid portals=" . join(',', @portals));
|
||||
|
||||
for my $ip (@portals) {
|
||||
my $portal = "$ip:3260";
|
||||
|
||||
# Ensure node entry exists
|
||||
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '-o', 'new');
|
||||
|
||||
# Login — ISCSI_ERR_SESS_EXISTS (15) is fine
|
||||
my ($rc, $out) = _iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '--login');
|
||||
if ($rc && $rc != 15) {
|
||||
die "iscsiadm login to $portal failed (rc=$rc): $out\n";
|
||||
}
|
||||
}
|
||||
|
||||
# Tell multipathd to rescan now
|
||||
system('multipathd', 'reconfigure') if -x '/usr/sbin/multipathd';
|
||||
system('multipath') if !-b $mapper;
|
||||
|
||||
unless (_wait_for_device($mapper, 30)) {
|
||||
die "Multipath device $mapper did not appear after 30s. "
|
||||
. "Check that multipathd is running and multipath.conf includes TrueNAS devices.\n";
|
||||
}
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_log('info', "multipath activate: $mapper ready");
|
||||
return;
|
||||
}
|
||||
|
||||
# ── deactivate_volume — logout from all portals, flush mapper device ──────────
|
||||
|
||||
sub deactivate_volume {
|
||||
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_resolve_token($storeid, $scfg);
|
||||
|
||||
my $ext = PVE::Storage::Custom::TrueNAS::_find_extent($scfg, $volname);
|
||||
return unless $ext && 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 $wwid = _naa_to_wwid($ext->{naa} // '');
|
||||
my $mapper = $wwid ? "/dev/mapper/$wwid" : undef;
|
||||
|
||||
# Flush multipath device first
|
||||
if ($mapper && -b $mapper) {
|
||||
system('multipath', '-f', $wwid);
|
||||
}
|
||||
|
||||
for my $ip (_portals($scfg)) {
|
||||
my $portal = "$ip:3260";
|
||||
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '--logout');
|
||||
_iscsiadm('-m', 'node', '-T', $iqn, '-p', $portal, '-o', 'delete');
|
||||
}
|
||||
|
||||
PVE::Storage::Custom::TrueNAS::_log('info', "multipath deactivate: $volname done");
|
||||
return;
|
||||
}
|
||||
|
||||
1;
|
||||
Loading…
Reference in New Issue