all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH storage/manager/docs v2 0/3] add FreeBSD CTL support for ZFS over iSCSI
@ 2026-04-06  4:38 Микола Микола
  2026-04-06  4:39 ` [PATCH storage v2 1/3] " Микола Микола
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Микола Микола @ 2026-04-06  4:38 UTC (permalink / raw)
  To: pve-devel

Hi

Resending this as inline patches instead of attachments

Thanks for understanding

^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH storage v2 1/3] add FreeBSD CTL support for ZFS over iSCSI
  2026-04-06  4:38 [PATCH storage/manager/docs v2 0/3] add FreeBSD CTL support for ZFS over iSCSI Микола Микола
@ 2026-04-06  4:39 ` Микола Микола
  2026-04-06  4:40 ` [PATCH manager v2 2/3] " Микола Микола
  2026-04-06  4:41 ` [PATCH docs v2 3/3] " Микола Микола
  2 siblings, 0 replies; 4+ messages in thread
From: Микола Микола @ 2026-04-06  4:39 UTC (permalink / raw)
  To: pve-devel

>From 4db2d62cf83381e7fe88691f8e149a51f52395d9 Mon Sep 17 00:00:00 2001
From: mykola2312 <49044616+mykola2312@users.noreply.github.com>
Date: Mon, 6 Apr 2026 03:09:21 +0300
Subject: [PATCH] storage: zfs: add FreeBSD ctld provider

Add native FreeBSD CTL/ctld support to the ZFS over iSCSI backend.

The provider manages inline LUN entries in /etc/ctl.conf, reloads ctld
after validated config updates, and uses the zvol path as the stable LU
identity.

Signed-off-by: mykola2312 <49044616+mykola2312@users.noreply.github.com>
---
 src/PVE/Storage/LunCmd/Ctld.pm  | 675 ++++++++++++++++++++++++++++++++
 src/PVE/Storage/LunCmd/Makefile |   2 +-
 src/PVE/Storage/ZFSPlugin.pm    |   9 +-
 3 files changed, 684 insertions(+), 2 deletions(-)
 create mode 100644 src/PVE/Storage/LunCmd/Ctld.pm

diff --git a/src/PVE/Storage/LunCmd/Ctld.pm b/src/PVE/Storage/LunCmd/Ctld.pm
new file mode 100644
index 0000000..26357d8
--- /dev/null
+++ b/src/PVE/Storage/LunCmd/Ctld.pm
@@ -0,0 +1,675 @@
+package PVE::Storage::LunCmd::Ctld;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(run_command);
+
+sub get_base;
+sub run_lun_command;
+
+my $CONFIG_FILE = '/etc/ctl.conf';
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+
+my $split_lines = sub {
+    my ($text) = @_;
+
+    my @lines = split /(?<=\n)/, $text, -1;
+    pop @lines if @lines && $lines[-1] eq '';
+
+    return @lines;
+};
+
+my $normalize_timeout = sub {
+    my ($timeout) = @_;
+    return $timeout || 10;
+};
+
+my $get_target = sub {
+    my ($scfg) = @_;
+    return 'root@' . $scfg->{portal};
+};
+
+my $ssh_base = sub {
+    my ($scfg) = @_;
+
+    return [
+        @ssh_cmd,
+        '-i',
+        "$id_rsa_path/$scfg->{portal}_id_rsa",
+        $get_target->($scfg),
+    ];
+};
+
+my $scp_base = sub {
+    my ($scfg) = @_;
+
+    return [
+        @scp_cmd,
+        '-i',
+        "$id_rsa_path/$scfg->{portal}_id_rsa",
+    ];
+};
+
+my $run_remote_command = sub {
+    my ($scfg, $timeout, @remote_cmd) = @_;
+
+    my $msg = '';
+    my $cmd = [@{ $ssh_base->($scfg) }, @remote_cmd];
+
+    my $output = sub {
+        my $line = shift;
+        $msg .= "$line\n";
+    };
+
+    run_command($cmd, outfunc => $output, timeout =>
$normalize_timeout->($timeout));
+
+    return $msg;
+};
+
+my $run_remote_shell = sub {
+    my ($scfg, $timeout, $script) = @_;
+    return $run_remote_command->(
+        $scfg,
+        $timeout,
+        'sh',
+        '-c',
+        PVE::Tools::shell_quote($script),
+    );
+};
+
+my $scp_to_remote = sub {
+    my ($scfg, $timeout, $local, $remote) = @_;
+
+    my $cmd = [@{ $scp_base->($scfg) }, $local, $get_target->($scfg)
. ":$remote"];
+    run_command($cmd, timeout => $normalize_timeout->($timeout));
+
+    return;
+};
+
+my $unquote = sub {
+    my ($value) = @_;
+
+    return undef if !defined($value);
+
+    if ($value =~ /^"(.*)"$/s) {
+        $value = $1;
+        $value =~ s/\\"/"/g;
+        $value =~ s/\\\\/\\/g;
+    }
+
+    return $value;
+};
+
+my $quote = sub {
+    my ($value) = @_;
+
+    $value =~ s/\\/\\\\/g;
+    $value =~ s/"/\\"/g;
+
+    return qq("$value");
+};
+
+my $brace_delta = sub {
+    my ($line) = @_;
+
+    my $tmp = $line;
+    $tmp =~ s/"(?:[^"\\]|\\.)*"//g;
+    $tmp =~ s/#.*$//;
+
+    my $open = ($tmp =~ tr/{//);
+    my $close = ($tmp =~ tr/}//);
+
+    return $open - $close;
+};
+
+my $ensure_trailing_newline = sub {
+    my ($text) = @_;
+
+    $text .= "\n" if $text !~ /\n\z/;
+
+    return $text;
+};
+
+my $block_indent = sub {
+    my ($indent) = @_;
+
+    return ($indent =~ /\t/) ? "$indent\t" : "$indent    ";
+};
+
+my $read_config = sub {
+    my ($scfg, $timeout) = @_;
+
+    my $config = eval { $run_remote_command->($scfg, $timeout, 'cat',
$CONFIG_FILE) };
+    if (my $err = $@) {
+        die "Missing config file $CONFIG_FILE on $scfg->{portal}\n"
+            if $err =~ /No such file or directory/;
+        die $err;
+    }
+
+    die "Missing config file $CONFIG_FILE on $scfg->{portal}\n" if !$config;
+
+    return $config;
+};
+
+my $parse_lun_block = sub {
+    my ($lun, $raw) = @_;
+
+    my $path;
+    my $blocksize;
+
+    for my $line ($split_lines->($raw)) {
+        if ($line =~ /^\s*path\s+("(?:[^"\\]|\\.)+"|\S+)\s*(?:#.*)?$/) {
+            $path = $unquote->($1);
+        } elsif ($line =~ /^\s*blocksize\s+(\d+)\s*(?:#.*)?$/) {
+            $blocksize = int($1);
+        }
+    }
+
+    return {
+        lun => int($lun),
+        path => $path,
+        blocksize => $blocksize,
+        raw => $raw,
+    };
+};
+
+my $parse_target_block = sub {
+    my ($raw) = @_;
+
+    my @lines = $split_lines->($raw);
+    die "malformed target block in $CONFIG_FILE\n" if scalar(@lines) < 2;
+
+    my $header = shift @lines;
+    my $footer = pop @lines;
+    my $preserved = '';
+    my @luns;
+    my %used;
+
+    my $i = 0;
+    while ($i < scalar(@lines)) {
+        my $line = $lines[$i];
+
+        if ($line =~ /^\s*lun\s+(\d+)\s*\{\s*(?:#.*)?$/) {
+            my $lun = int($1);
+            my $depth = $brace_delta->($line);
+            my $block = $line;
+            $used{$lun} = 1;
+            $i++;
+
+            while ($depth > 0) {
+                die "unterminated lun block in $CONFIG_FILE\n" if $i
>= scalar(@lines);
+                $line = $lines[$i];
+                $block .= $line;
+                $depth += $brace_delta->($line);
+                $i++;
+            }
+
+            push @luns, $parse_lun_block->($lun, $block);
+            next;
+        }
+
+        if ($line =~ /^\s*lun\s+(\d+)\b/) {
+            $used{int($1)} = 1;
+        }
+
+        $preserved .= $line;
+        $i++;
+    }
+
+    my $indent = '    ';
+    if ($preserved =~ /^([ \t]+)\S/m) {
+        $indent = $1;
+    } elsif (@luns && $luns[0]->{raw} =~ /^([ \t]+)lun\b/m) {
+        $indent = $1;
+    }
+
+    return {
+        header => $header,
+        footer => $footer,
+        preserved => $preserved,
+        luns => \@luns,
+        used => \%used,
+        indent => $indent,
+    };
+};
+
+my $parse_config = sub {
+    my ($scfg, $config) = @_;
+
+    my @parts;
+    my $text = '';
+    my $selected;
+
+    my @lines = $split_lines->($config);
+    my $i = 0;
+    while ($i < scalar(@lines)) {
+        my $line = $lines[$i];
+
+        if ($line =~ /^\s*target\s+("(?:[^"\\]|\\.)+"|\S+)\s*\{\s*(?:#.*)?$/) {
+            push @parts, { type => 'text', text => $text } if length($text);
+            $text = '';
+
+            my $name = $unquote->($1);
+            my $depth = $brace_delta->($line);
+            my $block = $line;
+            $i++;
+
+            while ($depth > 0) {
+                die "unterminated target block in $CONFIG_FILE\n" if
$i >= scalar(@lines);
+                $line = $lines[$i];
+                $block .= $line;
+                $depth += $brace_delta->($line);
+                $i++;
+            }
+
+            my $part = {
+                type => 'target',
+                name => $name,
+                raw => $block,
+            };
+
+            if ($name eq $scfg->{target}) {
+                die "$scfg->{target}: duplicate target definition in
$CONFIG_FILE\n"
+                    if $selected;
+                $part->{selected} = 1;
+                $selected = $part;
+            }
+
+            push @parts, $part;
+            next;
+        }
+
+        $text .= $line;
+        $i++;
+    }
+
+    push @parts, { type => 'text', text => $text } if length($text);
+
+    die "$scfg->{target}: target not found in $CONFIG_FILE\n" if !$selected;
+
+    $selected->{parsed} = $parse_target_block->($selected->{raw});
+
+    return {
+        parts => \@parts,
+        selected => $selected,
+    };
+};
+
+my $find_lun_by_path = sub {
+    my ($parsed_target, $path) = @_;
+
+    for my $entry (@{ $parsed_target->{luns} }) {
+        next if !defined($entry->{path});
+        return $entry if $entry->{path} eq $path;
+    }
+
+    return undef;
+};
+
+my $allocate_lun_number = sub {
+    my ($parsed_target) = @_;
+
+    for (my $lun = 0; $lun < 65536; $lun++) {
+        return $lun if !$parsed_target->{used}->{$lun};
+    }
+
+    die "no free LUN numbers available for target\n";
+};
+
+my $parse_blocksize = sub {
+    my ($blocksize) = @_;
+
+    return undef if !defined($blocksize);
+    return int($1) if $blocksize =~ /^(\d+)$/;
+
+    if ($blocksize =~ /^(\d+)([KkMmGgTt])$/) {
+        my ($value, $unit) = (int($1), lc($2));
+        my $factor = {
+            k => 1024,
+            m => 1024 * 1024,
+            g => 1024 * 1024 * 1024,
+            t => 1024 * 1024 * 1024 * 1024,
+        }->{$unit};
+        return $value * $factor if $factor;
+    }
+
+    return undef;
+};
+
+my $parse_size = sub {
+    my ($size) = @_;
+
+    return undef if !defined($size);
+
+    if ($size =~ /^(\d+)([KkMmGgTt])$/) {
+        my ($value, $unit) = (int($1), lc($2));
+        my $factor = {
+            k => 1024,
+            m => 1024 * 1024,
+            g => 1024 * 1024 * 1024,
+            t => 1024 * 1024 * 1024 * 1024,
+        }->{$unit};
+
+        return $value * $factor if $factor;
+    }
+
+    return undef;
+};
+
+my $render_lun_block = sub {
+    my ($parsed_target, $entry) = @_;
+
+    return $ensure_trailing_newline->($entry->{raw}) if defined($entry->{raw});
+
+    my $indent = $parsed_target->{indent} || '    ';
+    my $block_indent = $block_indent->($indent);
+    my $raw = "${indent}lun $entry->{lun} {\n";
+
+    $raw .= "${block_indent}blocksize $entry->{blocksize}\n" if
$entry->{blocksize};
+    $raw .= "${block_indent}path " . $quote->($entry->{path}) . "\n";
+    $raw .= "${indent}}\n";
+
+    return $raw;
+};
+
+my $render_target = sub {
+    my ($parsed_target, $luns) = @_;
+
+    my $raw = $parsed_target->{header} . $parsed_target->{preserved};
+    my @entries = sort { $a->{lun} <=> $b->{lun} } @$luns;
+
+    if (@entries) {
+        $raw .= "\n" if $raw !~ /\n[ \t]*\n\z/;
+        for my $entry (@entries) {
+            $raw .= $render_lun_block->($parsed_target, $entry);
+        }
+    }
+
+    $raw .= $parsed_target->{footer};
+
+    return $raw;
+};
+
+my $render_config = sub {
+    my ($parsed, $target_raw) = @_;
+
+    my $config = '';
+    for my $part (@{ $parsed->{parts} }) {
+        if ($part->{type} eq 'text') {
+            $config .= $part->{text};
+        } elsif ($part->{selected}) {
+            $config .= $target_raw;
+        } else {
+            $config .= $part->{raw};
+        }
+    }
+
+    return $config;
+};
+
+my $parse_devlist = sub {
+    my ($text) = @_;
+
+    my @entries;
+    my $current;
+
+    for my $line (split /\n/, $text) {
+        if ($line =~ /^\s*(\d+)\s+\S+\s+(\d+)\s+(\d+)\s+\S+\s+\S+\s*$/) {
+            $current = {
+                lun_id => int($1),
+                size_blocks => int($2),
+                blocksize => int($3),
+            };
+            push @entries, $current;
+        } elsif ($current && $line =~ /^\s+(\w+)=(.*)$/) {
+            $current->{$1} = $2;
+        }
+    }
+
+    return \@entries;
+};
+
+my $find_ctl_lun = sub {
+    my ($scfg, $timeout, $path) = @_;
+
+    my $text = $run_remote_command->($scfg, $timeout, 'ctladm',
'devlist', '-v');
+    for my $entry (@{ $parse_devlist->($text) }) {
+        return $entry if defined($entry->{file}) && $entry->{file} eq $path;
+    }
+
+    return undef;
+};
+
+my $wait_for_ctl_lun = sub {
+    my ($scfg, $timeout, $path, $should_exist) = @_;
+
+    my $max_tries = $normalize_timeout->($timeout);
+    $max_tries = 1 if $max_tries < 1;
+
+    for (my $try = 0; $try < $max_tries; $try++) {
+        my $entry = $find_ctl_lun->($scfg, 10, $path);
+        return $entry if $should_exist && $entry;
+        return 1 if !$should_exist && !$entry;
+        sleep(1) if $try + 1 < $max_tries;
+    }
+
+    return undef;
+};
+
+my $wait_for_ctl_lun_size = sub {
+    my ($scfg, $timeout, $path, $expected_size) = @_;
+
+    my $max_tries = $normalize_timeout->($timeout);
+    $max_tries = 1 if $max_tries < 1;
+
+    for (my $try = 0; $try < $max_tries; $try++) {
+        my $entry = $find_ctl_lun->($scfg, 10, $path);
+        if ($entry && defined($entry->{size_blocks}) &&
defined($entry->{blocksize})) {
+            my $actual_size = $entry->{size_blocks} * $entry->{blocksize};
+            return $entry if $actual_size == $expected_size;
+        }
+
+        sleep(1) if $try + 1 < $max_tries;
+    }
+
+    return undef;
+};
+
+my $write_and_apply_config = sub {
+    my ($scfg, $timeout, $config) = @_;
+
+    my $local_tmp = "/tmp/ctl.conf.$$";
+    my $remote_tmp = "/etc/ctl.conf.tmp.$$";
+    my $remote_backup = "/etc/ctl.conf.bak.$$";
+
+    open(my $fh, '>', $local_tmp) or die "Could not open file '$local_tmp' $!";
+    print $fh $config;
+    close $fh;
+
+    eval {
+        $scp_to_remote->($scfg, $timeout, $local_tmp, $remote_tmp);
+
+        my $remote_q = PVE::Tools::shell_quote($remote_tmp);
+        my $backup_q = PVE::Tools::shell_quote($remote_backup);
+        my $live_q = PVE::Tools::shell_quote($CONFIG_FILE);
+
+        my $script = <<"EOF";
+ctld -t -f $remote_q || exit \$?
+had_live=0
+if [ -f $live_q ]; then
+    cp $live_q $backup_q || exit \$?
+    had_live=1
+fi
+mv $remote_q $live_q || exit \$?
+if service ctld reload; then
+    rm -f $backup_q
+    exit 0
+fi
+rc=\$?
+if [ "\$had_live" -eq 1 ]; then
+    mv $backup_q $live_q || exit \$rc
+else
+    rm -f $live_q
+fi
+service ctld reload >/dev/null 2>&1 || true
+exit \$rc
+EOF
+
+        my $chmod_tmp = 'chmod 600 ' . PVE::Tools::shell_quote($remote_tmp);
+        $run_remote_shell->($scfg, $timeout, $chmod_tmp);
+        $run_remote_shell->($scfg, $timeout, $script);
+    };
+    my $err = $@;
+
+    unlink $local_tmp;
+
+    eval {
+        my $cleanup = 'rm -f '
+            . join(' ', map { PVE::Tools::shell_quote($_) }
$remote_tmp, $remote_backup);
+        $run_remote_shell->($scfg, 10, $cleanup);
+    };
+
+    die $err if $err;
+
+    return;
+};
+
+my $load_current_target = sub {
+    my ($scfg, $timeout) = @_;
+
+    my $config = $read_config->($scfg, $timeout);
+    return $parse_config->($scfg, $config);
+};
+
+my $create_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $path = $params[0];
+
+    my $parsed = $load_current_target->($scfg, $timeout);
+    my $target = $parsed->{selected}->{parsed};
+
+    die "$path: LUN already exists\n" if $find_lun_by_path->($target, $path);
+
+    my $lun = $allocate_lun_number->($target);
+    my @luns = @{ $target->{luns} };
+    push @luns,
+        {
+            lun => $lun,
+            path => $path,
+            blocksize => $parse_blocksize->($scfg->{blocksize}),
+        };
+
+    my $config = $render_config->($parsed, $render_target->($target, \@luns));
+    $write_and_apply_config->($scfg, $timeout, $config);
+
+    die "$path: exported LUN did not appear after ctld reload\n"
+        if !$wait_for_ctl_lun->($scfg, 10, $path, 1);
+
+    return $path;
+};
+
+my $delete_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $path = $params[0];
+
+    my $parsed = $load_current_target->($scfg, $timeout);
+    my $target = $parsed->{selected}->{parsed};
+
+    die "$path: LUN not found\n" if !$find_lun_by_path->($target, $path);
+
+    my @luns = grep { !defined($_->{path}) || $_->{path} ne $path }
@{ $target->{luns} };
+    my $config = $render_config->($parsed, $render_target->($target, \@luns));
+    $write_and_apply_config->($scfg, $timeout, $config);
+
+    die "$path: exported LUN still present after ctld reload\n"
+        if !$wait_for_ctl_lun->($scfg, 10, $path, 0);
+
+    return $path;
+};
+
+my $import_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    return $create_lun->($scfg, $timeout, $method, @params);
+};
+
+my $modify_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my ($size, $path) = @params;
+
+    my $parsed = $load_current_target->($scfg, $timeout);
+    my $target = $parsed->{selected}->{parsed};
+
+    die "$path: LUN not found\n" if !$find_lun_by_path->($target, $path);
+
+    $run_remote_command->($scfg, $timeout, 'service', 'ctld', 'reload');
+
+    my $entry = $wait_for_ctl_lun->($scfg, 10, $path, 1)
+        or die "$path: exported LUN did not reappear after ctld reload\n";
+
+    my $expected_size = $parse_size->($size);
+    return $path if !defined($expected_size);
+
+    my $refreshed = $wait_for_ctl_lun_size->($scfg, 10, $path, $expected_size)
+        or die "$path: exported size mismatch after ctld reload\n";
+
+    return $path;
+};
+
+my $add_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    # ctld exports the target-visible LUN mapping directly from the
inline `lun N { ... }`
+    # entries in /etc/ctl.conf, so create/import already establish the view.
+    return '';
+};
+
+my $list_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $path = $params[0];
+
+    my $parsed = $load_current_target->($scfg, $timeout);
+    my $entry = $find_lun_by_path->($parsed->{selected}->{parsed}, $path);
+
+    return defined($entry) ? $entry->{lun} : undef;
+};
+
+my $list_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $path = $params[0];
+
+    my $parsed = $load_current_target->($scfg, $timeout);
+    my $entry = $find_lun_by_path->($parsed->{selected}->{parsed}, $path);
+
+    return defined($entry) ? $entry->{path} : undef;
+};
+
+my %lun_cmd_map = (
+    create_lu => $create_lun,
+    delete_lu => $delete_lun,
+    import_lu => $import_lun,
+    modify_lu => $modify_lun,
+    add_view => $add_view,
+    list_view => $list_view,
+    list_lu => $list_lun,
+);
+
+sub run_lun_command {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    die "unknown command '$method'\n" if !exists $lun_cmd_map{$method};
+
+    return $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
+}
+
+sub get_base {
+    my ($scfg) = @_;
+    return $scfg->{'zfs-base-path'} || '/dev/zvol';
+}
+
+1;
diff --git a/src/PVE/Storage/LunCmd/Makefile b/src/PVE/Storage/LunCmd/Makefile
index a7209d1..ba9c5e0 100644
--- a/src/PVE/Storage/LunCmd/Makefile
+++ b/src/PVE/Storage/LunCmd/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Comstar.pm Istgt.pm Iet.pm LIO.pm
+SOURCES=Comstar.pm Ctld.pm Istgt.pm Iet.pm LIO.pm

 .PHONY: install
 install:
diff --git a/src/PVE/Storage/ZFSPlugin.pm b/src/PVE/Storage/ZFSPlugin.pm
index 99d8c8f..1155a2c 100644
--- a/src/PVE/Storage/ZFSPlugin.pm
+++ b/src/PVE/Storage/ZFSPlugin.pm
@@ -11,6 +11,7 @@ use PVE::RPCEnvironment;

 use base qw(PVE::Storage::ZFSPoolPlugin);
 use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::Ctld;
 use PVE::Storage::LunCmd::Istgt;
 use PVE::Storage::LunCmd::Iet;
 use PVE::Storage::LunCmd::LIO;
@@ -32,7 +33,7 @@ my $lun_cmds = {
 my $zfs_unknown_scsi_provider = sub {
     my ($provider) = @_;

-    die "$provider: unknown iscsi provider. Available [comstar,
istgt, iet, LIO]";
+    die "$provider: unknown iscsi provider. Available [comstar, ctld,
istgt, iet, LIO]";
 };

 my $zfs_get_base = sub {
@@ -40,6 +41,8 @@ my $zfs_get_base = sub {

     if ($scfg->{iscsiprovider} eq 'comstar') {
         return PVE::Storage::LunCmd::Comstar::get_base($scfg);
+    } elsif ($scfg->{iscsiprovider} eq 'ctld') {
+        return PVE::Storage::LunCmd::Ctld::get_base($scfg);
     } elsif ($scfg->{iscsiprovider} eq 'istgt') {
         return PVE::Storage::LunCmd::Istgt::get_base($scfg);
     } elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -63,6 +66,8 @@ sub zfs_request {
         if ($scfg->{iscsiprovider} eq 'comstar') {
             $msg =
                 PVE::Storage::LunCmd::Comstar::run_lun_command($scfg,
$timeout, $method, @params);
+        } elsif ($scfg->{iscsiprovider} eq 'ctld') {
+            $msg = PVE::Storage::LunCmd::Ctld::run_lun_command($scfg,
$timeout, $method, @params);
         } elsif ($scfg->{iscsiprovider} eq 'istgt') {
             $msg =
PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method,
@params);
         } elsif ($scfg->{iscsiprovider} eq 'iet') {
@@ -243,6 +248,8 @@ sub on_add_hook {
         my $base_path;
         if ($scfg->{iscsiprovider} eq 'comstar') {
             $base_path = PVE::Storage::LunCmd::Comstar::get_base($scfg);
+        } elsif ($scfg->{iscsiprovider} eq 'ctld') {
+            $base_path = PVE::Storage::LunCmd::Ctld::get_base($scfg);
         } elsif ($scfg->{iscsiprovider} eq 'istgt') {
             $base_path = PVE::Storage::LunCmd::Istgt::get_base($scfg);
         } elsif ($scfg->{iscsiprovider} eq 'iet' ||
$scfg->{iscsiprovider} eq 'LIO') {
-- 
2.47.3



^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH manager v2 2/3] add FreeBSD CTL support for ZFS over iSCSI
  2026-04-06  4:38 [PATCH storage/manager/docs v2 0/3] add FreeBSD CTL support for ZFS over iSCSI Микола Микола
  2026-04-06  4:39 ` [PATCH storage v2 1/3] " Микола Микола
@ 2026-04-06  4:40 ` Микола Микола
  2026-04-06  4:41 ` [PATCH docs v2 3/3] " Микола Микола
  2 siblings, 0 replies; 4+ messages in thread
From: Микола Микола @ 2026-04-06  4:40 UTC (permalink / raw)
  To: pve-devel

>From 02d13f40ee513546a76314befe37f8bb90d6eb66 Mon Sep 17 00:00:00 2001
From: mykola2312 <49044616+mykola2312@users.noreply.github.com>
Date: Mon, 6 Apr 2026 04:05:56 +0300
Subject: [PATCH] ui: show CTL for ctld iSCSI provider

---
 www/manager6/form/iScsiProviderSelector.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/www/manager6/form/iScsiProviderSelector.js
b/www/manager6/form/iScsiProviderSelector.js
index e965273a..079dc096 100644
--- a/www/manager6/form/iScsiProviderSelector.js
+++ b/www/manager6/form/iScsiProviderSelector.js
@@ -3,6 +3,7 @@ Ext.define('PVE.form.iScsiProviderSelector', {
     alias: ['widget.pveiScsiProviderSelector'],
     comboItems: [
         ['comstar', 'Comstar'],
+        ['ctld', 'CTL'],
         ['istgt', 'istgt'],
         ['iet', 'IET'],
         ['LIO', 'LIO'],
-- 
2.47.3



^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH docs v2 3/3] add FreeBSD CTL support for ZFS over iSCSI
  2026-04-06  4:38 [PATCH storage/manager/docs v2 0/3] add FreeBSD CTL support for ZFS over iSCSI Микола Микола
  2026-04-06  4:39 ` [PATCH storage v2 1/3] " Микола Микола
  2026-04-06  4:40 ` [PATCH manager v2 2/3] " Микола Микола
@ 2026-04-06  4:41 ` Микола Микола
  2 siblings, 0 replies; 4+ messages in thread
From: Микола Микола @ 2026-04-06  4:41 UTC (permalink / raw)
  To: pve-devel

>From 8d724a09390fc52671c2c1ef83250c90612527a7 Mon Sep 17 00:00:00 2001
From: mykola2312 <49044616+mykola2312@users.noreply.github.com>
Date: Mon, 6 Apr 2026 05:17:45 +0300
Subject: [PATCH] storage: document FreeBSD ctld for ZFS over iSCSI

---
 pve-storage-zfs.adoc | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)

diff --git a/pve-storage-zfs.adoc b/pve-storage-zfs.adoc
index c07f534..49d9737 100644
--- a/pve-storage-zfs.adoc
+++ b/pve-storage-zfs.adoc
@@ -17,6 +17,7 @@ The following iSCSI target implementations are supported:
 * LIO (Linux)
 * IET (Linux)
 * ISTGT (FreeBSD)
+* CTL / `ctld` (FreeBSD)
 * Comstar (Solaris)

 NOTE: This plugin needs a ZFS capable remote storage appliance, you cannot use
@@ -47,7 +48,7 @@ The backend supports the common storage properties
`content`, `nodes`,
 pool::

 The ZFS pool/filesystem on the iSCSI target. All allocations are done
within that
-pool.
+pool or dataset. This can also be a nested dataset path, for example
`zroot/pve`.

 portal::

@@ -55,11 +56,13 @@ iSCSI portal (IP or DNS name with optional port).

 target::

-iSCSI target.
+iSCSI target. When using `ctld`, this target must already exist on the remote
+FreeBSD host.

 iscsiprovider::

-The iSCSI target implementation used on the remote machine
+The iSCSI target implementation used on the remote machine. `ctld` uses the
+native FreeBSD CTL iSCSI target implementation.

 comstar_tg::

@@ -86,6 +89,11 @@ sparse::
 Use ZFS thin-provisioning. A sparse volume is a volume whose
 reservation is not equal to the volume size.

+With FreeBSD `ctld`, configure the target itself on the remote host first,
+including any portal or authentication groups required by your deployment.
+{pve} then manages inline `lun N` entries below the configured target for the
+zvol-backed guest disks.
+

 .Configuration Examples (`/etc/pve/storage.cfg`)
 ----
@@ -107,6 +115,15 @@ zfs: solaris
    portal 192.0.2.112
    content images

+zfs: freebsd-ctl
+   blocksize 4k
+   target iqn.2026-03.example:freebsd.pve
+   pool zroot/pve
+   iscsiprovider ctld
+   portal 192.0.2.113
+   content images
+   sparse 1
+
 zfs: freebsd
    blocksize 4k
    target iqn.2007-09.jp.ne.peach.istgt:tank1
-- 
2.47.3



^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-04-06  4:41 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-06  4:38 [PATCH storage/manager/docs v2 0/3] add FreeBSD CTL support for ZFS over iSCSI Микола Микола
2026-04-06  4:39 ` [PATCH storage v2 1/3] " Микола Микола
2026-04-06  4:40 ` [PATCH manager v2 2/3] " Микола Микола
2026-04-06  4:41 ` [PATCH docs v2 3/3] " Микола Микола

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal