906 lines
34 KiB
Perl
906 lines
34 KiB
Perl
package PVE::Storage::LunCmd::FreeNAS;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
our $VERSION = '2.4.0';
|
|
use Data::Dumper;
|
|
use PVE::SafeSyslog;
|
|
use IO::Socket::SSL;
|
|
|
|
use LWP::UserAgent;
|
|
use HTTP::Request;
|
|
use REST::Client;
|
|
use MIME::Base64;
|
|
use JSON;
|
|
|
|
# Global variable definitions
|
|
my $MAX_LUNS = 255; # Max LUNS per target on the iSCSI server
|
|
my $freenas_server_list = {}; # Per-host state: {client, product_name, api_version, dev_prefix, runaway_count}
|
|
my $freenas_rest_connection = undef; # REST::Client for the current active host
|
|
my $freenas_global_config_list = {}; # Per-host iSCSI global config cache
|
|
my $freenas_global_config = undef; # Pointer to entry in $freenas_global_config_list
|
|
my $dev_prefix = "";
|
|
my $product_name = undef;
|
|
|
|
# FreeNAS API definitions
|
|
my $freenas_api_version = "v1.0"; # Default to v1.0 of the API's
|
|
my $freenas_api_methods = undef; # API Methods Nested HASH Ref
|
|
my $freenas_api_variables = undef; # API Variable Nested HASH Ref
|
|
my $truenas_version = undef;
|
|
my $truenas_release_type = "Production";
|
|
|
|
# FreeNAS/TrueNAS (CORE) API Versioning HashRef Matrix
|
|
my $freenas_api_version_matrix = {
|
|
"v1.0" => {
|
|
"methods" => {
|
|
"config" => {
|
|
"resource" => "/api/v1.0/services/iscsi/globalconfiguration/",
|
|
},
|
|
"target" => {
|
|
"resource" => "/api/v1.0/services/iscsi/target/",
|
|
},
|
|
"extent" => {
|
|
"resource" => "/api/v1.0/services/iscsi/extent/",
|
|
"post_body" => {
|
|
"iscsi_target_extent_type" => "Disk",
|
|
"iscsi_target_extent_name" => "\$name",
|
|
"iscsi_target_extent_disk" => "\$device",
|
|
},
|
|
},
|
|
"targetextent" => {
|
|
"resource" => "/api/v1.0/services/iscsi/targettoextent/",
|
|
"post_body" => {
|
|
"iscsi_target" => "\$target_id",
|
|
"iscsi_extent" => "\$extent_id",
|
|
"iscsi_lunid" => "\$lun_id",
|
|
},
|
|
},
|
|
},
|
|
"variables" => {
|
|
"basename" => "iscsi_basename",
|
|
"lunid" => "iscsi_lunid",
|
|
"extentid" => "iscsi_extent",
|
|
"targetid" => "iscsi_target",
|
|
"extentpath" => "iscsi_target_extent_path",
|
|
"extentnaa" => "iscsi_target_extent_naa",
|
|
"targetname" => "iscsi_target_name",
|
|
}
|
|
},
|
|
"v2.0" => {
|
|
"methods" => {
|
|
"config" => {
|
|
"resource" => "/api/v2.0/iscsi/global",
|
|
},
|
|
"target" => {
|
|
"resource" => "/api/v2.0/iscsi/target/",
|
|
},
|
|
"extent" => {
|
|
"resource" => "/api/v2.0/iscsi/extent/",
|
|
"delete_body" => {
|
|
"remove" => \1,
|
|
"force" => \1,
|
|
},
|
|
"post_body" => {
|
|
"type" => "DISK",
|
|
"name" => "\$name",
|
|
"disk" => "\$device",
|
|
},
|
|
},
|
|
"targetextent" => {
|
|
"resource" => "/api/v2.0/iscsi/targetextent/",
|
|
"delete_body" => {
|
|
"force" => \1,
|
|
},
|
|
"post_body" => {
|
|
"target" => "\$target_id",
|
|
"extent" => "\$extent_id",
|
|
"lunid" => "\$lun_id",
|
|
},
|
|
},
|
|
},
|
|
"variables" => {
|
|
"basename" => "basename",
|
|
"lunid" => "lunid",
|
|
"extentid" => "extent",
|
|
"targetid" => "target",
|
|
"extentpath" => "path",
|
|
"extentnaa" => "naa",
|
|
"targetname" => "name",
|
|
},
|
|
},
|
|
};
|
|
|
|
|
|
#
|
|
#
|
|
#
|
|
sub get_base {
|
|
return '/dev/zvol';
|
|
}
|
|
|
|
|
|
#
|
|
# Subroutine called from ZFSPlugin.pm
|
|
#
|
|
sub run_lun_command {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
|
|
syslog("info",(caller(0))[3] . " : $method(@params)");
|
|
|
|
if (defined($scfg->{'truenas_token_auth'}) && $scfg->{'truenas_token_auth'}) {
|
|
if (!defined($scfg->{'truenas_secret'})) {
|
|
die "Undefined `truenas_secret` variable.";
|
|
}
|
|
} elsif (!defined($scfg->{'freenas_user'}) || !defined($scfg->{'freenas_password'})) {
|
|
die "Undefined `freenas_user` and/or `freenas_password` variables.";
|
|
}
|
|
if (!defined $freenas_server_list->{defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal}}) {
|
|
freenas_api_check($scfg);
|
|
}
|
|
|
|
if($method eq "create_lu") {
|
|
return run_create_lu($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "delete_lu") {
|
|
return run_delete_lu($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "import_lu") {
|
|
return run_create_lu($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "modify_lu") {
|
|
return run_modify_lu($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "add_view") {
|
|
return run_add_view($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "list_view") {
|
|
return run_list_view($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "list_extent") {
|
|
return run_list_extent($scfg, $timeout, $method, @params);
|
|
}
|
|
if($method eq "list_lu") {
|
|
return run_list_lu($scfg, $timeout, $method, "name", @params);
|
|
}
|
|
|
|
syslog("error",(caller(0))[3] . " : unknown method $method");
|
|
return;
|
|
}
|
|
|
|
#
|
|
#
|
|
#
|
|
sub run_add_view {
|
|
return '';
|
|
}
|
|
|
|
#
|
|
# a modify_lu occur by example on a zvol resize. we just need to destroy and recreate the lun with the same zvol.
|
|
# Be careful, the first param is the new size of the zvol, we must shift params
|
|
#
|
|
sub run_modify_lu {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
shift(@params);
|
|
run_delete_lu($scfg, $timeout, $method, @params);
|
|
return run_create_lu($scfg, $timeout, $method, @params);
|
|
}
|
|
|
|
#
|
|
#
|
|
#
|
|
sub run_list_view {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
return run_list_lu($scfg, $timeout, $method, "lun-id", @params);
|
|
}
|
|
|
|
#
|
|
#
|
|
# Optimized
|
|
sub run_list_lu {
|
|
my ($scfg, $timeout, $method, $result_value_type, @params) = @_;
|
|
my $object = $params[0];
|
|
my $result = undef;
|
|
my $luns = freenas_list_lu($scfg);
|
|
syslog("info", (caller(0))[3] . " : called with (method: '$method'; result_value_type: '$result_value_type'; param[0]: '$object')");
|
|
|
|
$object =~ s/^\Q$dev_prefix//;
|
|
syslog("info", (caller(0))[3] . " : TrueNAS object to find: '$object'");
|
|
if (defined($luns->{$object})) {
|
|
my $lu_object = $luns->{$object};
|
|
$result = $result_value_type eq "lun-id" ? $lu_object->{$freenas_api_variables->{'lunid'}} : $dev_prefix . $lu_object->{$freenas_api_variables->{'extentpath'}};
|
|
syslog("info",(caller(0))[3] . " '$object' with key '$result_value_type' found with value: '$result'");
|
|
} else {
|
|
syslog("info", (caller(0))[3] . " '$object' with key '$result_value_type' was not found");
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
#
|
|
#
|
|
# Optimzed
|
|
sub run_list_extent {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
my $object = $params[0];
|
|
syslog("info", (caller(0))[3] . " : called with (method: '$method'; params[0]: '$object')");
|
|
my $result = undef;
|
|
my $luns = freenas_list_lu($scfg);
|
|
|
|
$object =~ s/^\Q$dev_prefix//;
|
|
syslog("info", (caller(0))[3] . " TrueNAS object to find: '$object'");
|
|
if (defined($luns->{$object})) {
|
|
my $lu_object = $luns->{$object};
|
|
$result = $lu_object->{$freenas_api_variables->{'extentnaa'}};
|
|
syslog("info",(caller(0))[3] . " '$object' with key '$freenas_api_variables->{'extentnaa'}' found with value: '$result'");
|
|
} else {
|
|
syslog("info",(caller(0))[3] . " '$object' with key '$freenas_api_variables->{'extentnaa'}' was not found");
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
#
|
|
#
|
|
#
|
|
sub run_create_lu {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
my $lun_path = $params[0];
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (method=$method; param[0]=$lun_path)");
|
|
|
|
my $lun_id = freenas_get_first_available_lunid($scfg);
|
|
|
|
die "Maximum number of LUNs per target is $MAX_LUNS" if scalar $lun_id >= $MAX_LUNS;
|
|
die "$params[0]: LUN $lun_path exists" if defined(run_list_lu($scfg, $timeout, $method, "name", @params));
|
|
|
|
my $target_id = freenas_get_targetid($scfg);
|
|
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
|
|
|
|
my ($extent_id, $link_id);
|
|
eval {
|
|
my $extent = freenas_iscsi_create_extent($scfg, $lun_path);
|
|
die "Unable to create extent for $lun_path" if !defined($extent);
|
|
$extent_id = $extent->{'id'};
|
|
|
|
my $link = freenas_iscsi_create_target_to_extent($scfg, $target_id, $extent_id, $lun_id);
|
|
die "Unable to link extent to target for $lun_path" if !defined($link);
|
|
$link_id = $link->{'id'};
|
|
|
|
# Set FREENAS_TEST_ROLLBACK=1 in pvedaemon environment to exercise
|
|
# rollback without a real failure (#239)
|
|
die "TEST: forced rollback for #239 verification" if $ENV{FREENAS_TEST_ROLLBACK};
|
|
};
|
|
if (my $err = $@) {
|
|
syslog("err", (caller(0))[3] . " : rolling back after error for $lun_path: $err");
|
|
freenas_iscsi_remove_target_to_extent($scfg, $link_id) if defined $link_id;
|
|
freenas_iscsi_remove_extent($scfg, $extent_id) if defined $extent_id;
|
|
freenas_delete_zvol($scfg, $lun_path) if defined $extent_id;
|
|
die "Unable to create lun $lun_path (rolled back): $err";
|
|
}
|
|
|
|
syslog("info", (caller(0))[3] . "(lun_path=$lun_path, lun_id=$lun_id) : successful");
|
|
return "";
|
|
}
|
|
|
|
#
|
|
#
|
|
# Optimzied
|
|
sub run_delete_lu {
|
|
my ($scfg, $timeout, $method, @params) = @_;
|
|
my $lun_path = $params[0];
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (method: '$method'; param[0]: '$lun_path')");
|
|
|
|
my $luns = freenas_list_lu($scfg);
|
|
my $lun = undef;
|
|
my $link = undef;
|
|
$lun_path =~ s/^\Q$dev_prefix//;
|
|
|
|
if (defined($luns->{$lun_path})) {
|
|
$lun = $luns->{$lun_path};
|
|
syslog("info",(caller(0))[3] . " lun: '$lun_path' found");
|
|
} else {
|
|
die "Unable to find the lun $lun_path for $scfg->{target}";
|
|
}
|
|
|
|
my $target_id = freenas_get_targetid($scfg);
|
|
die "Unable to find the target id for $scfg->{target}" if !defined($target_id);
|
|
|
|
# find the target to extent
|
|
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
|
|
|
|
syslog("info", (caller(0))[3] . " : searching for 'targetextent' with (target_id=$target_id; lun_id=$lun->{$freenas_api_variables->{'lunid'}}; extent_id=$lun->{id})");
|
|
foreach my $item (@$target2extents) {
|
|
if($item->{$freenas_api_variables->{'targetid'}} == $target_id &&
|
|
$item->{$freenas_api_variables->{'lunid'}} == $lun->{$freenas_api_variables->{'lunid'}} &&
|
|
$item->{$freenas_api_variables->{'extentid'}} == $lun->{'id'}) {
|
|
$link = $item;
|
|
syslog("info", (caller(0))[3] . " : found 'targetextent'(target_id=$item->{$freenas_api_variables->{'targetid'}}; lun_id=$item->{$freenas_api_variables->{'lunid'}}; extent_id=$item->{$freenas_api_variables->{'extentid'}})");
|
|
last;
|
|
}
|
|
}
|
|
die "Unable to find the link for the lun $lun_path for $scfg->{target}" if !defined($link);
|
|
|
|
# Remove the extent
|
|
my $remove_extent = freenas_iscsi_remove_extent($scfg, $lun->{'id'});
|
|
|
|
# Remove the link
|
|
my $remove_link = freenas_iscsi_remove_target_to_extent($scfg, $link->{'id'});
|
|
|
|
# NOTE: zvol deletion is intentionally left to ZFSPlugin's SSH path (zfs_delete_zvol).
|
|
# Deleting it here caused a double-delete: SSH would fail, ZFSPlugin's error recovery
|
|
# would call run_create_lu on a missing zvol, producing a false "Unable to create lun"
|
|
# error in the task log (#240). freenas_delete_zvol belongs only in run_create_lu's
|
|
# rollback path (#239).
|
|
|
|
if($remove_link == 1 && $remove_extent == 1) {
|
|
syslog("info", (caller(0))[3] . "(lun_path=$lun_path) : successful");
|
|
} else {
|
|
die "Unable to delete lun $lun_path";
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
sub freenas_api_connect {
|
|
my ($scfg, $ping) = @_;
|
|
$ping //= '/api/v1.0/system/version/';
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
my $scheme = $scfg->{freenas_use_ssl} ? "https" : "http";
|
|
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
|
|
|
|
if (!defined $freenas_server_list->{$apihost} || !defined $freenas_server_list->{$apihost}{client}) {
|
|
$freenas_server_list->{$apihost} = {
|
|
client => REST::Client->new(),
|
|
product_name => undef,
|
|
api_version => 'v1.0',
|
|
dev_prefix => '',
|
|
runaway_count => 0,
|
|
};
|
|
}
|
|
my $host = $freenas_server_list->{$apihost};
|
|
$host->{client}->setHost($scheme . '://' . $apihost);
|
|
$host->{client}->addHeader('Content-Type', 'application/json');
|
|
if (defined($scfg->{'truenas_token_auth'}) && $scfg->{'truenas_token_auth'}) {
|
|
syslog("info", (caller(0))[3] . " : Authentication using Bearer Token Auth");
|
|
if (!$scfg->{freenas_use_ssl}) {
|
|
syslog("warn", (caller(0))[3] . " : Bearer Token used without SSL — TrueNAS 25.04+ revokes API keys sent over plain HTTP; enable 'Use SSL' in storage config");
|
|
}
|
|
$host->{client}->addHeader('Authorization', 'Bearer ' . $scfg->{truenas_secret});
|
|
} else {
|
|
syslog("info", (caller(0))[3] . " : Authentication using Basic Auth");
|
|
$host->{client}->addHeader('Authorization', 'Basic ' . encode_base64($scfg->{freenas_user} . ':' . $scfg->{freenas_password}));
|
|
}
|
|
# If using SSL, don't verify SSL certs
|
|
if ($scfg->{freenas_use_ssl}) {
|
|
$host->{client}->getUseragent()->ssl_opts(verify_hostname => 0);
|
|
$host->{client}->getUseragent()->ssl_opts(SSL_verify_mode => SSL_VERIFY_NONE);
|
|
}
|
|
# Check if the APIs are accessible via the selected host and scheme
|
|
my $api_response = $host->{client}->request('GET', $ping);
|
|
my $code = $api_response->responseCode();
|
|
my $type = $api_response->responseHeader('Content-Type');
|
|
syslog("info", (caller(0))[3] . " : REST connection header Content-Type:'" . $type . "'");
|
|
|
|
# Make sure we are not recursion calling.
|
|
if ($host->{runaway_count} > 2) {
|
|
freenas_api_log_error($host->{client});
|
|
die "Loop recursion prevention";
|
|
# Successful connection
|
|
} elsif ($code == 200 && ($type =~ /^text\/plain/ || $type =~ /^application\/json/)) {
|
|
syslog("info", (caller(0))[3] . " : REST connection successful to '" . $apihost . "' using the '" . $scheme . "' protocol");
|
|
$host->{runaway_count} = 0;
|
|
# A 302 or 200 (wrong content-type) means v1.0 is redirecting — upgrade to v2.0.
|
|
# TrueNAS 25.04+ removed v1.0 entirely and returns 404 instead of 302; treat
|
|
# that the same way so long as we are still probing the v1.0 endpoint.
|
|
} elsif ($code == 302 || $code == 200 || ($code == 404 && $ping =~ /v1\.0/)) {
|
|
syslog("info", (caller(0))[3] . " : v1.0 API unavailable (HTTP $code) — upgrading to v2.0");
|
|
$host->{runaway_count}++;
|
|
$ping =~ s/v1\.0/v2\.0/;
|
|
freenas_api_connect($scfg, $ping);
|
|
# A 307 from FreeNAS means redirect http to https.
|
|
} elsif ($code == 307) {
|
|
syslog("info", (caller(0))[3] . " : Redirecting to HTTPS protocol");
|
|
$host->{runaway_count}++;
|
|
$scfg->{freenas_use_ssl} = 1;
|
|
freenas_api_connect($scfg, $ping);
|
|
# For now, any other code we fail.
|
|
} else {
|
|
freenas_api_log_error($host->{client});
|
|
die "Unable to connect to the FreeNAS API service at '" . $apihost . "' using the '" . $scheme . "' protocol";
|
|
}
|
|
$freenas_rest_connection = $host->{client};
|
|
return;
|
|
}
|
|
|
|
#
|
|
# Check to see what FreeNAS version we are running and set
|
|
# the FreeNAS.pm to use the correct API version of FreeNAS
|
|
#
|
|
sub freenas_api_check {
|
|
my ($scfg, $timeout) = @_;
|
|
my $result = {};
|
|
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
if (!defined $freenas_server_list->{$apihost}{client}) {
|
|
freenas_api_connect($scfg);
|
|
$result = $freenas_rest_connection->responseContent();
|
|
$result =~ s/"//g;
|
|
syslog("info", (caller(0))[3] . " : successful : Server version: " . $result);
|
|
if ($result =~ /^(TrueNAS|FreeNAS)-(\d+)\.(\d+)\-U(\d+)(?(?=\.)\.(\d+))$/) {
|
|
$product_name = $1;
|
|
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $4 || 0, $5 || 0);
|
|
} elsif ($result =~ /^(TrueNAS)-(\d+)\.(\d+)(?(?=\-U\d+)-U(\d+)|-\w+)(?(?=\.).(\d+))$/) {
|
|
$product_name = $1;
|
|
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $4 || 0, $6 || 0);
|
|
$truenas_release_type = $5 || "Production";
|
|
} elsif ($result =~ /^(TrueNAS-SCALE)-(\d+)\.(\d+)(?(?=\-)-(\w+))\.(\d+)(?(?=\.)\.(\d+))(?(?=\-)-(\d+))$/) {
|
|
$product_name = $1;
|
|
$truenas_version = sprintf("%02d%02d%02d%02d", $2, $3 || 0, $5 || 0, $7 || 0);
|
|
$truenas_release_type = $4 || "Production";
|
|
} else {
|
|
$product_name = "Unknown";
|
|
$truenas_release_type = "Unknown";
|
|
syslog("error", (caller(0))[3] . " : Could not parse the version of TrueNAS.");
|
|
}
|
|
syslog("info", (caller(0))[3] . " : ". $product_name . " Unformatted Version: " . $truenas_version);
|
|
if ($truenas_version >= 11030100) {
|
|
$freenas_api_version = "v2.0";
|
|
$dev_prefix = "/dev/";
|
|
}
|
|
if ($truenas_release_type ne "Production") {
|
|
syslog("warn", (caller(0))[3] . " : The '" . $product_name . "' release type of '" . $truenas_release_type . "' may not worked due to unsupported changes.");
|
|
}
|
|
# Save per-host metadata so subsequent calls restore state correctly
|
|
$freenas_server_list->{$apihost}{product_name} = $product_name;
|
|
$freenas_server_list->{$apihost}{api_version} = $freenas_api_version;
|
|
$freenas_server_list->{$apihost}{dev_prefix} = $dev_prefix;
|
|
} else {
|
|
syslog("info", (caller(0))[3] . " : REST Client already initialized");
|
|
}
|
|
syslog("info", (caller(0))[3] . " : Using " . $product_name . " API version " . $freenas_api_version);
|
|
$freenas_api_methods = $freenas_api_version_matrix->{$freenas_api_version}->{'methods'};
|
|
$freenas_api_variables = $freenas_api_version_matrix->{$freenas_api_version}->{'variables'};
|
|
$freenas_global_config = $freenas_global_config_list->{$apihost} = (!defined($freenas_global_config_list->{$apihost})) ? freenas_iscsi_get_globalconfiguration($scfg) : $freenas_global_config_list->{$apihost};
|
|
return;
|
|
}
|
|
|
|
|
|
#
|
|
### FREENAS API CALLING ROUTINE ###
|
|
#
|
|
sub freenas_api_call {
|
|
my ($scfg, $method, $path, $data) = @_;
|
|
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
|
|
|
|
syslog("info", (caller(0))[3] . " : called for host '" . $apihost . "'");
|
|
|
|
$method = uc($method);
|
|
if ($method !~ /^(?:GET|DELETE|POST)$/) {
|
|
syslog("info", (caller(0))[3] . " : Invalid HTTP RESTful service method '$method'");
|
|
die "Invalid HTTP RESTful service method '$method' used.";
|
|
}
|
|
|
|
if (!defined $freenas_server_list->{$apihost}{client}) {
|
|
freenas_api_check($scfg);
|
|
}
|
|
my $host = $freenas_server_list->{$apihost};
|
|
$freenas_rest_connection = $host->{client};
|
|
$freenas_global_config = $freenas_global_config_list->{$apihost};
|
|
$product_name = $host->{product_name};
|
|
$dev_prefix = $host->{dev_prefix};
|
|
$freenas_api_methods = $freenas_api_version_matrix->{$host->{api_version}}{methods};
|
|
$freenas_api_variables = $freenas_api_version_matrix->{$host->{api_version}}{variables};
|
|
my $json_data = (defined $data) ? encode_json($data) : undef;
|
|
$freenas_rest_connection->request($method, $path, $json_data);
|
|
syslog("info", (caller(0))[3] . " : successful");
|
|
return;
|
|
}
|
|
|
|
#
|
|
# Writes the Response and Content to SysLog
|
|
#
|
|
sub freenas_api_log_error {
|
|
my ($rest_connection) = @_;
|
|
my $connection = ((defined $rest_connection) ? $rest_connection : $freenas_rest_connection);
|
|
syslog("info","[ERROR]FreeNAS::API::" . (caller(1))[3] . " : Response code: " . $connection->responseCode());
|
|
syslog("info","[ERROR]FreeNAS::API::" . (caller(1))[3] . " : Response content: " . $connection->responseContent());
|
|
return 1;
|
|
}
|
|
|
|
#
|
|
#
|
|
#
|
|
sub freenas_iscsi_get_globalconfiguration {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'config'}->{'resource'}, $freenas_api_methods->{'config'}->{'get'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", (caller(0))[3] . " : target_basename=" . $result->{$freenas_api_variables->{'basename'}});
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Returns a list of all extents.
|
|
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-extent-
|
|
#
|
|
sub freenas_iscsi_get_extent {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'extent'}->{'resource'} . "?limit=0", $freenas_api_methods->{'extent'}->{'get'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", (caller(0))[3] . " : successful");
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Create an extent on FreeNas
|
|
# http://api.freenas.org/resources/iscsi/index.html#create-resource
|
|
# Parameters:
|
|
# - target config (scfg)
|
|
# - lun_path
|
|
#
|
|
sub freenas_iscsi_create_extent {
|
|
my ($scfg, $lun_path) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (lun_path=$lun_path)");
|
|
|
|
my $name = $lun_path;
|
|
$name =~ s/^.*\///; # all from last /
|
|
|
|
my $pool = $scfg->{'pool'};
|
|
# If TrueNAS-SCALE the slashes (/) need to be converted to dashes (-)
|
|
if ($product_name eq "TrueNAS-SCALE") {
|
|
$pool =~ s/\//-/g;
|
|
syslog("info", (caller(0))[3] . " : TrueNAS-SCALE slash to dash conversion '" . $pool ."'");
|
|
}
|
|
$name = $pool . ($product_name eq "TrueNAS-SCALE" ? '-' : '/') . $name;
|
|
syslog("info", (caller(0))[3] . " : " . $product_name . " extent '". $name . "'");
|
|
|
|
my $device = $lun_path;
|
|
$device =~ s/^\/dev\///; # strip /dev/
|
|
|
|
my %extent_vars = ('$name' => $name, '$device' => $device);
|
|
my $post_body = {};
|
|
while ((my $key, my $value) = each %{$freenas_api_methods->{'extent'}->{'post_body'}}) {
|
|
$post_body->{$key} = exists $extent_vars{$value} ? $extent_vars{$value} : $value;
|
|
}
|
|
|
|
freenas_api_call($scfg, 'POST', $freenas_api_methods->{'extent'}->{'resource'}, $post_body);
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200 || $code == 201) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", "FreeNAS::API::create_extent(lun_path=" . $result->{$freenas_api_variables->{'extentpath'}} . ") : successful");
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Explicitly delete the underlying ZFS zvol via the pool/dataset API.
|
|
# The extent DELETE with remove:true is unreliable on TrueNAS SCALE 24.10+
|
|
# (extent config is removed but the zvol is left behind). This function
|
|
# is a belt-and-suspenders call after every extent removal.
|
|
# Only runs on v2.0; v1.0 hosts are expected to handle it via remove:true.
|
|
# Parameters:
|
|
# - scfg
|
|
# - lun_path (either /dev/zvol/tank/... or zvol/tank/... form)
|
|
#
|
|
sub freenas_delete_zvol {
|
|
my ($scfg, $lun_path) = @_;
|
|
|
|
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
|
|
my $api_version = ($freenas_server_list->{$apihost}{api_version} // $freenas_api_version) // '';
|
|
return 1 unless $api_version eq 'v2.0';
|
|
|
|
my $dataset = $lun_path;
|
|
$dataset =~ s{^/dev/zvol/}{}; # strip /dev/zvol/ prefix (run_create_lu form)
|
|
$dataset =~ s{^zvol/}{}; # strip zvol/ prefix (run_delete_lu form after $dev_prefix strip)
|
|
$dataset =~ s{/}{%2F}g; # URL-encode path separators for REST endpoint
|
|
|
|
syslog("info", (caller(0))[3] . " : explicitly deleting zvol dataset '$dataset'");
|
|
freenas_api_call($scfg, 'DELETE', "/api/v2.0/pool/dataset/id/$dataset/", {"recursive" => \0});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200 || $code == 204) {
|
|
syslog("info", (caller(0))[3] . " : zvol '$dataset' deleted successfully");
|
|
return 1;
|
|
} else {
|
|
syslog("err", (caller(0))[3] . " : WARNING: could not delete zvol '$dataset' (HTTP $code) — manual cleanup may be needed");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# Parse a blocksize string (e.g. "8k", "16k", "8192") to bytes.
|
|
sub freenas_parse_blocksize {
|
|
my ($val) = @_;
|
|
return 0 unless defined $val && $val ne '';
|
|
return $1 * 1024 if $val =~ /^(\d+)\s*[kK]$/;
|
|
return $1 * 1024 * 1024 if $val =~ /^(\d+)\s*[mM]$/;
|
|
return $val + 0;
|
|
}
|
|
|
|
# Returns the recommended ZFS volblocksize for this TrueNAS host based on its
|
|
# product type. TrueNAS SCALE ships a newer ZFS that requires >= 16k; CORE is
|
|
# fine with 8k. Returns undef if the API is unreachable so callers can fall
|
|
# back to the user-configured value or the ZFS default.
|
|
sub freenas_get_recommended_blocksize {
|
|
my ($scfg) = @_;
|
|
|
|
my $apihost = defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal};
|
|
|
|
# freenas_api_check (not freenas_api_connect) must be used here because
|
|
# product_name is only parsed and cached by freenas_api_check.
|
|
# freenas_api_connect only sets up the HTTP client.
|
|
eval { freenas_api_check($scfg) };
|
|
if ($@) {
|
|
syslog("warn", (caller(0))[3] . " : unable to connect to TrueNAS API for blocksize detection: $@");
|
|
return;
|
|
}
|
|
|
|
my $product = $freenas_server_list->{$apihost}{product_name} // '';
|
|
if ($product =~ /SCALE/i) {
|
|
syslog("info", (caller(0))[3] . " : detected TrueNAS SCALE — recommending blocksize 16384");
|
|
return 16384;
|
|
}
|
|
|
|
syslog("info", (caller(0))[3] . " : detected '$product' — recommending blocksize 8192");
|
|
return 8192;
|
|
}
|
|
|
|
#
|
|
# Remove an extent by it's id
|
|
# http://api.freenas.org/resources/iscsi/index.html#delete-resource
|
|
# Parameters:
|
|
# - scfg
|
|
# - extent_id
|
|
#
|
|
sub freenas_iscsi_remove_extent {
|
|
my ($scfg, $extent_id) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (extent_id=$extent_id)");
|
|
freenas_api_call($scfg, 'DELETE', $freenas_api_methods->{'extent'}->{'resource'} . (($freenas_api_version eq "v2.0") ? "id/" : "") . "$extent_id/", $freenas_api_methods->{'extent'}->{'delete_body'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200 || $code == 204) {
|
|
syslog("info", (caller(0))[3] . "(extent_id=$extent_id) : successful");
|
|
return 1;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Returns a list of all targets
|
|
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-target-
|
|
#
|
|
sub freenas_iscsi_get_target {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'target'}->{'resource'} . "?limit=0", $freenas_api_methods->{'target'}->{'get'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", (caller(0))[3] . " : successful");
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Returns a list of associated extents to targets
|
|
# http://api.freenas.org/resources/iscsi/index.html#get--api-v1.0-services-iscsi-targettoextent-
|
|
#
|
|
sub freenas_iscsi_get_target_to_extent {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
freenas_api_call($scfg, 'GET', $freenas_api_methods->{'targetextent'}->{'resource'} . "?limit=0", $freenas_api_methods->{'targetextent'}->{'get'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", (caller(0))[3] . " : successful");
|
|
# If 'iscsi_lunid' is undef then it is set to 'Auto' in FreeNAS
|
|
# which should be '0' in our eyes.
|
|
# This gave Proxmox 5.x and FreeNAS 11.1 a few issues.
|
|
foreach my $item (@$result) {
|
|
if (!defined($item->{$freenas_api_variables->{'lunid'}})) {
|
|
$item->{$freenas_api_variables->{'lunid'}} = 0;
|
|
syslog("info", (caller(0))[3] . " : change undef iscsi_lunid to 0");
|
|
}
|
|
}
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Associate a FreeNas extent to a FreeNas Target
|
|
# http://api.freenas.org/resources/iscsi/index.html#post--api-v1.0-services-iscsi-targettoextent-
|
|
# Parameters:
|
|
# - target config (scfg)
|
|
# - FreeNas Target ID
|
|
# - FreeNas Extent ID
|
|
# - Lun ID
|
|
#
|
|
sub freenas_iscsi_create_target_to_extent {
|
|
my ($scfg, $target_id, $extent_id, $lun_id) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (target_id=$target_id, extent_id=$extent_id, lun_id=$lun_id)");
|
|
|
|
my %tte_vars = ('$target_id' => $target_id, '$extent_id' => $extent_id, '$lun_id' => $lun_id);
|
|
my $post_body = {};
|
|
while ((my $key, my $value) = each %{$freenas_api_methods->{'targetextent'}->{'post_body'}}) {
|
|
$post_body->{$key} = exists $tte_vars{$value} ? $tte_vars{$value} : $value;
|
|
}
|
|
|
|
freenas_api_call($scfg, 'POST', $freenas_api_methods->{'targetextent'}->{'resource'}, $post_body);
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200 || $code == 201) {
|
|
my $result = decode_json($freenas_rest_connection->responseContent());
|
|
syslog("info", (caller(0))[3] . "(target_id=$target_id, extent_id=$extent_id, lun_id=$lun_id) : successful");
|
|
return $result;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Remove a Target to extent by it's id
|
|
# http://api.freenas.org/resources/iscsi/index.html#delete--api-v1.0-services-iscsi-targettoextent-(int-id)-
|
|
# Parameters:
|
|
# - scfg
|
|
# - link_id
|
|
#
|
|
sub freenas_iscsi_remove_target_to_extent {
|
|
my ($scfg, $link_id) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called with (link_id=$link_id)");
|
|
|
|
if ($freenas_api_version eq "v2.0") {
|
|
syslog("info", (caller(0))[3] . "(link_id=$link_id) : V2.0 API's so NOT Needed...successful");
|
|
return 1;
|
|
}
|
|
|
|
freenas_api_call($scfg, 'DELETE', $freenas_api_methods->{'targetextent'}->{'resource'} . (($freenas_api_version eq "v2.0") ? "id/" : "") . "$link_id/", $freenas_api_methods->{'targetextent'}->{'delete_body'});
|
|
my $code = $freenas_rest_connection->responseCode();
|
|
if ($code == 200 || $code == 204) {
|
|
syslog("info", (caller(0))[3] . "(link_id=$link_id) : successful");
|
|
return 1;
|
|
} else {
|
|
freenas_api_log_error();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Returns all luns associated to the current target defined by $scfg->{target}
|
|
# This method returns an array reference like "freenas_iscsi_get_extent" do
|
|
# but with an additionnal hash entry "iscsi_lunid" retrieved from "freenas_iscsi_get_target_to_extent"
|
|
#
|
|
sub freenas_list_lu {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
my $targets = freenas_iscsi_get_target($scfg);
|
|
my $target_id = freenas_get_targetid($scfg);
|
|
|
|
my %lun_hash;
|
|
my $iscsi_lunid = undef;
|
|
|
|
if(defined($target_id)) {
|
|
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
|
|
my $extents = freenas_iscsi_get_extent($scfg);
|
|
|
|
foreach my $item (@$target2extents) {
|
|
if($item->{$freenas_api_variables->{'targetid'}} == $target_id) {
|
|
foreach my $node (@$extents) {
|
|
if($node->{'id'} == $item->{$freenas_api_variables->{'extentid'}}) {
|
|
if ($item->{$freenas_api_variables->{'lunid'}} =~ /(\d+)/) {
|
|
if (defined($node)) {
|
|
$node->{$freenas_api_variables->{'lunid'}} .= "$1";
|
|
$lun_hash{$node->{$freenas_api_variables->{'extentpath'}}} = $node;
|
|
}
|
|
last;
|
|
} else {
|
|
syslog("warn", (caller(0))[3] . " : iscsi_lunid did not pass tainted testing");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
syslog("info", (caller(0))[3] . " : successful");
|
|
return \%lun_hash;
|
|
}
|
|
|
|
#
|
|
# Returns the first available "lunid" (in all targets namespaces)
|
|
#
|
|
sub freenas_get_first_available_lunid {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
my $target_id = freenas_get_targetid($scfg);
|
|
my $target2extents = freenas_iscsi_get_target_to_extent($scfg);
|
|
my @luns = ();
|
|
|
|
foreach my $item (@$target2extents) {
|
|
push(@luns, $item->{$freenas_api_variables->{'lunid'}}) if ($item->{$freenas_api_variables->{'targetid'}} == $target_id);
|
|
}
|
|
|
|
my @sorted_luns = sort {$a <=> $b} @luns;
|
|
my $lun_id = 0;
|
|
|
|
# find the first hole, if not, give the +1 of the last lun
|
|
foreach my $lun (@sorted_luns) {
|
|
last if $lun != $lun_id;
|
|
$lun_id = $lun_id + 1;
|
|
}
|
|
|
|
syslog("info", (caller(0))[3] . " : $lun_id");
|
|
return $lun_id;
|
|
}
|
|
|
|
#
|
|
# Returns the target id on FreeNas of the currently configured target of this PVE storage
|
|
#
|
|
sub freenas_get_targetid {
|
|
my ($scfg) = @_;
|
|
|
|
syslog("info", (caller(0))[3] . " : called");
|
|
|
|
my $targets = freenas_iscsi_get_target($scfg);
|
|
my $target_id = undef;
|
|
|
|
foreach my $target (@$targets) {
|
|
my $iqn = $freenas_global_config->{$freenas_api_variables->{'basename'}} . ':' . $target->{$freenas_api_variables->{'targetname'}};
|
|
if($iqn eq $scfg->{target}) {
|
|
$target_id = $target->{'id'};
|
|
last;
|
|
}
|
|
}
|
|
syslog("info", (caller(0))[3] . " : successful : $target_id");
|
|
return $target_id;
|
|
}
|
|
|
|
|
|
1;
|