freenas-proxmox/install-pve9.sh

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 "=================================================="