* [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.