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