public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Микола Микола" <nikolaytihonov2022@gmail.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH storage v2 1/3] add FreeBSD CTL support for ZFS over iSCSI
Date: Mon, 6 Apr 2026 07:39:28 +0300	[thread overview]
Message-ID: <CADzCCsONcdkz3spXKQS2OFWDOJy31KvYiN7JfjL8sv72+eQitA@mail.gmail.com> (raw)
In-Reply-To: <CADzCCsNfznS8NktTGnpN+5KABLcF9ceBF9-9zdRmhgk=DyULdA@mail.gmail.com>

>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



  reply	other threads:[~2026-04-06  4:39 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-06  4:38 [PATCH storage/manager/docs v2 0/3] " Микола Микола
2026-04-06  4:39 ` Микола Микола [this message]
2026-04-06  4:40 ` [PATCH manager v2 2/3] " Микола Микола
2026-04-06  4:41 ` [PATCH docs v2 3/3] " Микола Микола

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CADzCCsONcdkz3spXKQS2OFWDOJy31KvYiN7JfjL8sv72+eQitA@mail.gmail.com \
    --to=nikolaytihonov2022@gmail.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal