From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 2F1151FF136 for ; Mon, 06 Apr 2026 06:39:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7E2781FBEA; Mon, 6 Apr 2026 06:40:15 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; t=1775450380; cv=none; d=google.com; s=arc-20240605; b=Al6ijKA53micwjUEAXBflgn8hnm/HXRixVG1z5y6QLQQCeeahXbj133vF/ndIuxaRj qfMZrGs1SJ/2RacyEzDTi6xOTTJWqvlln+04Snd0hwTxX6z88c8OZIWc4txtH2JtORJJ Nt08Ut6X3Oa0WL7HD813QOLoW33vBvxeA2k3OOPenLwfYeBVop/VbVlo+sGnVt8rdj2G g76k+nD7EsC8Mm6hFFl3aPkTm/QGzQ2R5ZXlKpHtpraNAuvKN1e0dd2l7ik5cD4wGxeY Q/Ewg5jAku+FRj5PxD+/i9JlvT2a5FsPP8G1mgFgHaPfi9NkkrAcFNGacL2BGsB9B+yX RS1g== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :dkim-signature; bh=Sahazz67kC/KdTcY+8UKhsAtt0UbOec4jEdKbCBux6w=; fh=qUdnlOw4HMavIAa4vJfUpEe/QUA6LBLHlg5B+iT7exQ=; b=JYyy3PXLnFGCcr341TMuXVK4FbQziVHQ+WEYsvPWLfqZCnmlxXeWBNQaLj98+AAQuv iddEq32MX8jEBgA/q3bwk/l0zEUzjvDTu3b10zpxQGq0LJe6gbufuBnB8TKtFToCwKqF hWOXQlVPXssriR+8gK72BNTerJwR1yH0kkNvUsvyVzDqYkM3vH6ul0LL5HosQAwWjkuO K2o+p5cBCLYin5pMifDgLePBimrermV80Zmvswr6VPCpCEqPPh+nDh0Cp+EZGh6AKFkU plcV7h0oekRWRLzeGrxC+bDdLb3p4VEK0f1ZCq9wo7MOHSj4glsl7QlKyeXrsn3KoLSY rw8g==; darn=lists.proxmox.com ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1775450380; x=1776055180; darn=lists.proxmox.com; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=Sahazz67kC/KdTcY+8UKhsAtt0UbOec4jEdKbCBux6w=; b=GLZDO76ABlswQfMDxtCctVLU/o4iYWOH3ZbiMIm+QMHxzNkVq2PrsrnzkLrPmLnDCf ocdowJ1imr1NWE8GbgSl7WNLaTXhE5dA65UvFRYfk4p0yHyBQ0ulh6zLSrciWFItO0do vWnUCxOASZTaVh/NGBkqk6w7oh51HT2jCNFZgbtzdOpRxw7oeLLtGjqfRvaxMYh+9/D6 6DXPMQobGenYhfzrCCJg6LHzxyv7J4MhBFQdkSdfX3R5ZuqExT0k2U+va0MPX+ExwHPf sZ4q6lE42uSmnogjvoPJw1xOPQqEABqYEXCTyCYbD1cVdNaXjWPKk4cwSxWOYzhu9gWv pK+w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1775450380; x=1776055180; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=Sahazz67kC/KdTcY+8UKhsAtt0UbOec4jEdKbCBux6w=; b=kr9Zmh/8z3FAPPLQfW1ILbgNLTufPOFU1PmIwBGwVpy2wtw6asUOfbxj2B1YdyMw2G 37D6cYQVGTi/vLZBJaiP82Sd3tk4LABS4hJrzbYwFG8/hAc8mGFls4t65v9sjQdGOm+8 PFSNlpz4ryjNYKSInPvFs32D+1BnjHh0LdZ7AWBewq89lk9Xw7iba3u1PO633oOb72F1 ua5TPXVoLKPm9hw+F10Wylz1wKM7OD/bugIe27ktUN5DP/XABekEpWOyJ4ASsKnPxCGx dJT+JAGoTBHZtWrie9JcfY45Co5AfKVZvmMUJX+kjmjrjT/DS5LVY8AbyoQyDLRbIvuv CRMA== X-Gm-Message-State: AOJu0Yx548CWr8AEKYNiB4Iu3xsN4pgCMY3THXhIkkMOf8SeSyZqHeO2 zVsRAENz5kj9zL+S2/STWrieOHkCsVy293x7mic2bKAGJTaSaD7fXbMTWrsBH0/RRbDTDO6kZYw sE74LRN2wKIyVmEj/NcB3xZ7mG/wopN+aScIi X-Gm-Gg: AeBDieuMjmQ+3yWXq5Pgab14UuEvRBn+oGAQIZ/pYrT00D2y5n6xQqVOwlcR0Ae8KpR hB4NzOTBRTxXJkSw85iiXWE15NnzWt56vR6LaejKnrGjJ3P/7X9FHxLTEcJ8Sw2IjQP5eYOJ73B yk3cT9pOTnRyxy7Y+qIbXMJqbSuyjvt3AFlHq4dRNhAqFw9hxmT1CrQFCeexUf+U79/WPYXujr3 XnO7v91oQkrs2HgDN9VYVsLlHi57W0GHT99scKZI+fsxgk59EojuJqUZH8BBI2mRua1cJy6NcYF RFNb+gA= X-Received: by 2002:a05:690c:e3ee:b0:79b:dafd:d4c with SMTP id 00721157ae682-7a4d39cfa31mr115886537b3.13.1775450379746; Sun, 05 Apr 2026 21:39:39 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: From: =?UTF-8?B?0JzQuNC60L7Qu9CwINCc0LjQutC+0LvQsA==?= Date: Mon, 6 Apr 2026 07:39:28 +0300 X-Gm-Features: AQROBzDn13KD5bXukiLngHSRqcIcjVHRR-MiwLos2YD9uQlBzKXzHKhsOuc23Wk Message-ID: Subject: [PATCH storage v2 1/3] add FreeBSD CTL support for ZFS over iSCSI To: pve-devel@lists.proxmox.com Content-Type: text/plain; charset="UTF-8" X-SPAM-LEVEL: Spam detection results: 0 AWL 0.001 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy FREEMAIL_ENVFROM_END_DIGIT 1 Envelope-from freemail username ends in digit FREEMAIL_FROM 0.001 Sender email is commonly abused enduser mail provider RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [istgt.pm,iet.pm,ctld.pm,comstar.pm,lio.pm,zfsplugin.pm] Message-ID-Hash: RP7ZX3G4KBHHYXOVCX2JBZBWOHVA6PZS X-Message-ID-Hash: RP7ZX3G4KBHHYXOVCX2JBZBWOHVA6PZS X-MailFrom: nikolaytihonov2022@gmail.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: >>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