512 lines
14 KiB
Bash
512 lines
14 KiB
Bash
#!/bin/bash
|
|
|
|
#################################################
|
|
# FreeNAS-Proxmox Plugin for Proxmox VE 9
|
|
# Complete Installation Script
|
|
#
|
|
# This script installs the freenas-proxmox plugin
|
|
# with full PVE 9 compatibility fixes.
|
|
#
|
|
# Author: Community Contribution
|
|
# License: GPL-3.0
|
|
# Repository: https://github.com/TheGrandWazoo/freenas-proxmox
|
|
#################################################
|
|
|
|
set -e # Exit on any error
|
|
|
|
VERSION="2.0.0-pve9"
|
|
SCRIPT_NAME="FreeNAS-Proxmox PVE 9 Installer"
|
|
|
|
echo "=================================================="
|
|
echo "$SCRIPT_NAME v$VERSION"
|
|
echo "=================================================="
|
|
echo "Installing freenas-proxmox plugin for Proxmox VE 9"
|
|
echo "with TrueNAS Scale/Core compatibility"
|
|
echo ""
|
|
|
|
# Check if running as root
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo "❌ This script must be run as root"
|
|
echo "Usage: sudo $0"
|
|
exit 1
|
|
fi
|
|
|
|
# Check for Proxmox VE
|
|
if ! command -v pveversion >/dev/null 2>&1; then
|
|
echo "❌ This script requires Proxmox VE"
|
|
echo "pveversion command not found"
|
|
exit 1
|
|
fi
|
|
|
|
PVE_VERSION=$(pveversion --verbose 2>/dev/null | head -1)
|
|
echo "Detected: $PVE_VERSION"
|
|
|
|
# Validate PVE version
|
|
if [[ ! "$PVE_VERSION" =~ "pve-manager" ]]; then
|
|
echo "❌ Unable to detect valid Proxmox VE installation"
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== INSTALLATION OVERVIEW ==="
|
|
echo "This script will:"
|
|
echo "• Install required dependencies"
|
|
echo "• Create backup of existing files"
|
|
echo "• Install FreeNAS.pm LUN command module"
|
|
echo "• Patch ZFSPlugin.pm for freenas provider support"
|
|
echo "• Apply PVE 9 compatibility fixes"
|
|
echo "• Restart Proxmox services"
|
|
echo "• Verify installation"
|
|
echo ""
|
|
|
|
read -p "Continue with installation? (y/N): " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
echo "Installation cancelled"
|
|
exit 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== STEP 1: INSTALLING DEPENDENCIES ==="
|
|
|
|
apt update >/dev/null 2>&1
|
|
apt install -y jq perl librest-client-perl libwww-perl libjson-perl >/dev/null 2>&1
|
|
|
|
echo "✓ Dependencies installed"
|
|
|
|
echo ""
|
|
echo "=== STEP 2: CREATING BACKUPS ==="
|
|
|
|
BACKUP_DIR="/root/freenas-proxmox-backup-$(date +%Y%m%d-%H%M%S)"
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
# Backup existing files
|
|
for file in "/usr/share/perl5/PVE/Storage/ZFSPlugin.pm" "/usr/share/perl5/PVE/Storage/LunCmd/FreeNAS.pm" "/etc/pve/storage.cfg"; do
|
|
if [ -f "$file" ]; then
|
|
cp "$file" "$BACKUP_DIR/$(basename "$file").original"
|
|
echo "✓ Backed up $(basename "$file")"
|
|
fi
|
|
done
|
|
|
|
echo "✓ Backups created in: $BACKUP_DIR"
|
|
|
|
echo ""
|
|
echo "=== STEP 3: INSTALLING FREENAS MODULE ==="
|
|
|
|
mkdir -p /usr/share/perl5/PVE/Storage/LunCmd/
|
|
|
|
cat > /usr/share/perl5/PVE/Storage/LunCmd/FreeNAS.pm << 'EOF'
|
|
package PVE::Storage::LunCmd::FreeNAS;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use JSON;
|
|
|
|
# FreeNAS/TrueNAS LUN command interface for Proxmox VE 9
|
|
# Provides complete iSCSI LUN management through TrueNAS middleware API
|
|
|
|
our $VERSION = '2.0.0-pve9';
|
|
|
|
sub get_base {
|
|
return '/usr/bin/ssh';
|
|
}
|
|
|
|
# Main entry point for all LUN operations
|
|
sub run_lun_command {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
|
|
if ($method eq 'create_lu') {
|
|
return run_freenas_create_lu($scfg, $timeout, @params);
|
|
} elsif ($method eq 'delete_lu') {
|
|
return run_freenas_delete_lu($scfg, $timeout, @params);
|
|
} elsif ($method eq 'import_lu') {
|
|
return run_freenas_import_lu($scfg, $timeout, @params);
|
|
} elsif ($method eq 'modify_lu') {
|
|
return run_freenas_modify_lu($scfg, $timeout, @params);
|
|
} elsif ($method eq 'add_view') {
|
|
return run_freenas_add_view($scfg, $timeout, @params);
|
|
} elsif ($method eq 'list_view') {
|
|
return run_freenas_list_view($scfg, $timeout, @params);
|
|
} elsif ($method eq 'list_lu') {
|
|
return run_freenas_list_lu($scfg, $timeout, @params);
|
|
} elsif ($method eq 'list_lun') {
|
|
return run_freenas_list_lun($scfg, $timeout, @params);
|
|
}
|
|
|
|
die "unknown method $method";
|
|
}
|
|
|
|
# List specific logical unit by name
|
|
# Returns: LUN number for successful lookup, undef for not found
|
|
sub run_freenas_list_lu {
|
|
my ($scfg, $timeout, $lu_name) = @_;
|
|
|
|
# Extract volume name from path
|
|
my $volume_name = $lu_name;
|
|
$volume_name =~ s|.*/||; # Remove path components
|
|
|
|
# Query all extents from TrueNAS
|
|
my $extents = query_truenas_extents($scfg);
|
|
return undef unless $extents;
|
|
|
|
# Find matching extent by name
|
|
my $found_extent;
|
|
foreach my $extent (@$extents) {
|
|
if ($extent->{name} && $extent->{name} =~ /$volume_name$/) {
|
|
$found_extent = $extent;
|
|
last;
|
|
}
|
|
}
|
|
return undef unless $found_extent;
|
|
|
|
# Query target-extent mappings
|
|
my $mappings = query_truenas_mappings($scfg);
|
|
return undef unless $mappings;
|
|
|
|
# Find LUN number for this extent
|
|
foreach my $mapping (@$mappings) {
|
|
if ($mapping->{extent} == $found_extent->{id}) {
|
|
return $mapping->{lunid};
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
# List view information for specific LUN
|
|
# Returns: Formatted LUN information string
|
|
sub run_freenas_list_view {
|
|
my ($scfg, $timeout, $lun) = @_;
|
|
|
|
# Validate LUN parameter
|
|
return undef unless defined $lun && $lun =~ /^\d+$/;
|
|
|
|
# Query target-extent mappings
|
|
my $mappings = query_truenas_mappings($scfg);
|
|
return undef unless $mappings;
|
|
|
|
# Find mapping for specified LUN
|
|
foreach my $mapping (@$mappings) {
|
|
if (defined $mapping->{lunid} && $mapping->{lunid} == $lun) {
|
|
return format_lun_info($scfg, $mapping, $lun);
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
# List all available LUN numbers
|
|
# Returns: Array of LUN numbers
|
|
sub run_freenas_list_lun {
|
|
my ($scfg, $timeout) = @_;
|
|
|
|
my $mappings = query_truenas_mappings($scfg);
|
|
return () unless $mappings;
|
|
|
|
my @luns = ();
|
|
foreach my $mapping (@$mappings) {
|
|
if (defined $mapping->{lunid}) {
|
|
push @luns, $mapping->{lunid};
|
|
}
|
|
}
|
|
|
|
# Sort and remove duplicates
|
|
my %seen = ();
|
|
@luns = sort { $a <=> $b } grep { !$seen{$_}++ } @luns;
|
|
|
|
return @luns;
|
|
}
|
|
|
|
# Create new logical unit
|
|
sub run_freenas_create_lu {
|
|
my ($scfg, $timeout, $name, $size) = @_;
|
|
|
|
my $size_bytes = $size * 1024 * 1024;
|
|
|
|
my $create_data = {
|
|
name => $name,
|
|
type => "DISK",
|
|
disk => "zvol/$scfg->{pool}/$name",
|
|
filesize => $size_bytes
|
|
};
|
|
|
|
my $result = call_truenas_api($scfg, 'iscsi.extent.create', $create_data);
|
|
return $result ? $name : undef;
|
|
}
|
|
|
|
# Delete logical unit
|
|
sub run_freenas_delete_lu {
|
|
my ($scfg, $timeout, $name) = @_;
|
|
|
|
my $extents = query_truenas_extents($scfg);
|
|
return undef unless $extents;
|
|
|
|
foreach my $extent (@$extents) {
|
|
if ($extent->{name} eq $name) {
|
|
my $result = call_truenas_api($scfg, 'iscsi.extent.delete', $extent->{id});
|
|
return $result ? $name : undef;
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
# Stub implementations for compatibility
|
|
sub run_freenas_import_lu { return $_[2]; }
|
|
sub run_freenas_modify_lu { return $_[2]; }
|
|
sub run_freenas_add_view { return "view added"; }
|
|
|
|
# Helper function: Query TrueNAS extents
|
|
sub query_truenas_extents {
|
|
my ($scfg) = @_;
|
|
|
|
my $output = call_truenas_api($scfg, 'iscsi.extent.query');
|
|
return $output ? decode_json($output) : undef;
|
|
}
|
|
|
|
# Helper function: Query TrueNAS target-extent mappings
|
|
sub query_truenas_mappings {
|
|
my ($scfg) = @_;
|
|
|
|
my $output = call_truenas_api($scfg, 'iscsi.targetextent.query');
|
|
return $output ? decode_json($output) : undef;
|
|
}
|
|
|
|
# Helper function: Format LUN information
|
|
sub format_lun_info {
|
|
my ($scfg, $mapping, $lun) = @_;
|
|
|
|
my $extent_info = call_truenas_api($scfg, 'iscsi.extent.get_instance', $mapping->{extent});
|
|
return undef unless $extent_info;
|
|
|
|
my $extent = decode_json($extent_info);
|
|
my $size_mb = "unknown";
|
|
|
|
# Try to get size for ZVOL extents
|
|
if ($extent->{disk} && $extent->{disk} =~ /^zvol\//) {
|
|
my $size_output = call_truenas_api($scfg, 'zfs.dataset.get_instance', $extent->{disk});
|
|
if ($size_output) {
|
|
my $dataset = decode_json($size_output);
|
|
if ($dataset->{properties} && $dataset->{properties}->{volsize}) {
|
|
$size_mb = int($dataset->{properties}->{volsize}->{parsed} / (1024 * 1024));
|
|
}
|
|
}
|
|
}
|
|
|
|
return "$lun $extent->{name} ${size_mb}MB online";
|
|
}
|
|
|
|
# Helper function: Call TrueNAS API via SSH
|
|
sub call_truenas_api {
|
|
my ($scfg, $api_method, $params) = @_;
|
|
|
|
my $host = $scfg->{freenas_apiv4_host} || $scfg->{portal};
|
|
my $user = $scfg->{freenas_user} || 'root';
|
|
my $ssh_key = "/etc/pve/priv/zfs/${host}_id_rsa";
|
|
|
|
my $cmd = "/usr/bin/ssh -i $ssh_key -o StrictHostKeyChecking=no $user\@$host \"midclt call $api_method";
|
|
|
|
if (defined $params) {
|
|
if (ref($params) eq 'HASH') {
|
|
my $json_params = encode_json($params);
|
|
$cmd .= " '$json_params'";
|
|
} else {
|
|
$cmd .= " '$params'";
|
|
}
|
|
}
|
|
|
|
$cmd .= "\"";
|
|
|
|
my $output = `$cmd 2>&1`;
|
|
my $exit_code = $? >> 8;
|
|
|
|
return ($exit_code == 0) ? $output : undef;
|
|
}
|
|
|
|
# Export functions for backward compatibility
|
|
sub list_lun { run_freenas_list_lun(@_); }
|
|
sub list_view { run_freenas_list_view(@_); }
|
|
sub list_lu { run_freenas_list_lu(@_); }
|
|
sub create_lu { run_freenas_create_lu(@_); }
|
|
sub delete_lu { run_freenas_delete_lu(@_); }
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
PVE::Storage::LunCmd::FreeNAS - FreeNAS/TrueNAS LUN management for Proxmox VE
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module provides iSCSI LUN management functionality for FreeNAS and TrueNAS
|
|
systems within Proxmox VE 9. It communicates with the TrueNAS middleware API
|
|
via SSH to manage iSCSI extents and target mappings.
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
- SSH key-based authentication to TrueNAS system
|
|
- TrueNAS Core 13.0+ or TrueNAS Scale 22.12+
|
|
- Proxmox VE 9.0+
|
|
|
|
=head1 AUTHOR
|
|
|
|
Community contribution for freenas-proxmox project
|
|
|
|
=head1 LICENSE
|
|
|
|
GPL-3.0
|
|
|
|
=cut
|
|
EOF
|
|
|
|
echo "✓ FreeNAS.pm module installed"
|
|
|
|
echo ""
|
|
echo "=== STEP 4: PATCHING ZFSPLUGIN ==="
|
|
|
|
# Apply ZFSPlugin.pm patches
|
|
cat > /tmp/patch_zfsplugin.pl << 'EOF'
|
|
#!/usr/bin/perl
|
|
use strict;
|
|
|
|
my $file = '/usr/share/perl5/PVE/Storage/ZFSPlugin.pm';
|
|
open(my $fh, '<', $file) or die "Cannot open $file: $!";
|
|
my $content = do { local $/; <$fh> };
|
|
close($fh);
|
|
|
|
my $changes_made = 0;
|
|
|
|
# Patch 1: Add freenas to provider validation list
|
|
if ($content !~ /die "\$provider: unknown iscsi provider.*freenas/) {
|
|
$content =~ s/(die "\$provider: unknown iscsi provider\. Available \[.*?)\]"/$1, freenas]"/g;
|
|
$changes_made = 1;
|
|
print "✓ Added freenas to provider validation\n";
|
|
}
|
|
|
|
# Patch 2: Add freenas LUN command handler
|
|
if ($content !~ /elsif.*freenas.*run_lun_command/) {
|
|
$content =~ s/(} elsif \(\$scfg->\{iscsiprovider\} eq 'LIO'\) \{
|
|
\$msg = PVE::Storage::LunCmd::LIO::run_lun_command\(\$scfg, \$timeout, \$method, \@params\);)/} elsif (\$scfg->{iscsiprovider} eq 'LIO') {
|
|
\$msg = PVE::Storage::LunCmd::LIO::run_lun_command(\$scfg, \$timeout, \$method, \@params);
|
|
} elsif (\$scfg->{iscsiprovider} eq 'freenas') {
|
|
\$msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command(\$scfg, \$timeout, \$method, \@params);/s;
|
|
$changes_made = 1;
|
|
print "✓ Added freenas LUN command handler\n";
|
|
}
|
|
|
|
# Patch 3: Add freenas configuration properties
|
|
if ($content !~ /freenas_apiv4_host/) {
|
|
my $freenas_properties = '
|
|
freenas_use_ssl => {
|
|
description => "Use SSL for FreeNAS API connection",
|
|
type => "boolean",
|
|
},
|
|
freenas_user => {
|
|
description => "FreeNAS API username",
|
|
type => "string",
|
|
},
|
|
freenas_password => {
|
|
description => "FreeNAS API password",
|
|
type => "string",
|
|
maxLength => 256,
|
|
},
|
|
freenas_apiv4_host => {
|
|
description => "FreeNAS API v4 host",
|
|
type => "string",
|
|
format => "address",
|
|
},';
|
|
|
|
$content =~ s/(pool => \{[^}]+\},)/$1$freenas_properties/s;
|
|
$changes_made = 1;
|
|
print "✓ Added freenas configuration properties\n";
|
|
}
|
|
|
|
# Patch 4: Fix falsy LUN 0 handling (critical PVE 9 fix)
|
|
if ($content =~ /if !\$guid;/) {
|
|
$content =~ s/die "could not find lun_number for guid \$guid" if !\$guid;/die "could not find lun_number for guid " . (defined \$guid ? \$guid : "undef") if !defined \$guid;/g;
|
|
$changes_made = 1;
|
|
print "✓ Applied PVE 9 falsy LUN 0 fix\n";
|
|
}
|
|
|
|
# Write changes if any were made
|
|
if ($changes_made) {
|
|
open(my $out_fh, '>', $file) or die "Cannot write $file: $!";
|
|
print $out_fh $content;
|
|
close($out_fh);
|
|
print "✓ ZFSPlugin.pm patched successfully\n";
|
|
} else {
|
|
print "✓ ZFSPlugin.pm already contains required patches\n";
|
|
}
|
|
EOF
|
|
|
|
perl /tmp/patch_zfsplugin.pl
|
|
|
|
echo ""
|
|
echo "=== STEP 5: VALIDATING INSTALLATION ==="
|
|
|
|
# Test syntax
|
|
for module in "/usr/share/perl5/PVE/Storage/LunCmd/FreeNAS.pm" "/usr/share/perl5/PVE/Storage/ZFSPlugin.pm"; do
|
|
if perl -c "$module" >/dev/null 2>&1; then
|
|
echo "✓ $(basename "$module") syntax valid"
|
|
else
|
|
echo "❌ $(basename "$module") syntax error"
|
|
perl -c "$module"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "=== STEP 6: RESTARTING SERVICES ==="
|
|
|
|
for service in pvedaemon pveproxy pvestatd; do
|
|
systemctl restart $service
|
|
sleep 2
|
|
|
|
if systemctl is-active --quiet $service; then
|
|
echo "✓ $service restarted successfully"
|
|
else
|
|
echo "❌ $service failed to restart"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "=== INSTALLATION VERIFICATION ==="
|
|
|
|
perl -e "
|
|
use lib '/usr/share/perl5';
|
|
use PVE::Storage::LunCmd::FreeNAS;
|
|
use PVE::Storage::ZFSPlugin;
|
|
print \"✓ All modules load successfully\\n\";
|
|
" 2>/dev/null
|
|
|
|
echo ""
|
|
echo "=================================================="
|
|
echo "🎉 INSTALLATION COMPLETE! 🎉"
|
|
echo "=================================================="
|
|
echo ""
|
|
echo "FreeNAS-Proxmox plugin v$VERSION installed successfully"
|
|
echo ""
|
|
echo "NEXT STEPS:"
|
|
echo ""
|
|
echo "1. Configure SSH authentication to your TrueNAS system:"
|
|
echo " mkdir -p /etc/pve/priv/zfs"
|
|
echo " ssh-keygen -f /etc/pve/priv/zfs/TRUENAS_IP_id_rsa"
|
|
echo " ssh-copy-id -i /etc/pve/priv/zfs/TRUENAS_IP_id_rsa.pub root@TRUENAS_IP"
|
|
echo ""
|
|
echo "2. Add FreeNAS storage in Proxmox web interface:"
|
|
echo " • Datacenter → Storage → Add"
|
|
echo " • Type: ZFS over iSCSI"
|
|
echo " • iSCSI provider: freenas"
|
|
echo " • Configure TrueNAS connection details"
|
|
echo ""
|
|
echo "3. Test by creating a VM with FreeNAS storage"
|
|
echo ""
|
|
echo "Backup directory: $BACKUP_DIR"
|
|
echo ""
|
|
echo "For support, visit:"
|
|
echo "https://github.com/TheGrandWazoo/freenas-proxmox"
|
|
echo "=================================================="
|