]>
git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/LunCmd/LIO.pm
2fd3181d576a0152153bd22ab52ed7fa9f0d87fe
1 package PVE
::Storage
::LunCmd
::LIO
;
3 # lightly based on code from Iet.pm
6 # -----------------------------------------------------------------
7 # Copyright (c) 2018 BestSolution.at EDV Systemhaus GmbH
10 # This software is released under the terms of the
12 # "GNU Affero General Public License"
14 # and may only be distributed and used under the terms of the
15 # mentioned license. You should have received a copy of the license
16 # along with this software product, if not you can download it from
17 # https://www.gnu.org/licenses/agpl-3.0.en.html
19 # Author: udo.rader@bestsolution.at
20 # -----------------------------------------------------------------
24 use PVE
::Tools
qw(run_command);
30 # config file location differs from distro to distro
32 '/etc/rtslib-fb-target/saveconfig.json', # Debian 9.x et al
33 '/etc/target/saveconfig.json' , # ArchLinux, CentOS
35 my $BACKSTORE = '/backstores/block';
38 my $SETTINGS_TIMESTAMP = 0;
39 my $SETTINGS_MAXAGE = 15; # in seconds
41 my @ssh_opts = ('-o', 'BatchMode=yes');
42 my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
43 my $id_rsa_path = '/etc/pve/priv/zfs';
44 my $targetcli = '/usr/bin/targetcli';
46 my $execute_remote_command = sub {
47 my ($scfg, $timeout, $remote_command, @params) = @_;
55 $timeout = 10 if !$timeout;
57 my $output = sub { $msg .= "$_[0]\n" };
58 my $errfunc = sub { $err .= "$_[0]\n" };
60 $target = 'root@' . $scfg->{portal
};
61 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $remote_command, @params];
64 run_command
($cmd, outfunc
=> $output, errfunc
=> $errfunc, timeout
=> $timeout);
81 # fetch targetcli configuration from the portal
82 my $read_config = sub {
83 my ($scfg, $timeout) = @_;
91 $timeout = 10 if !$timeout;
93 my $output = sub { $msg .= "$_[0]\n" };
94 my $errfunc = sub { $err .= "$_[0]\n" };
96 $target = 'root@' . $scfg->{portal
};
98 foreach my $oneFile (@CONFIG_FILES) {
99 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $oneFile];
101 run_command
($cmd, outfunc
=> $output, errfunc
=> $errfunc, timeout
=> $timeout);
104 die $err if ($err !~ /No such file or directory/);
106 return $msg if $msg ne '';
109 die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq '';
114 my $get_config = sub {
118 my $config = $read_config->($scfg, undef);
119 die "Missing config file" unless $config;
124 # Return settings of a specific target
125 my $get_target_settings = sub {
128 my $id = "$scfg->{portal}.$scfg->{target}";
129 return undef if !$SETTINGS;
130 return $SETTINGS->{$id};
133 # fetches and parses targetcli config from the portal
136 my $tpg = $scfg->{lio_tpg
} || die "Target Portal Group not set, aborting!\n";
139 if ($tpg =~ /^tpg(\d+)$/) {
142 die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n";
147 my $config = $get_config->($scfg);
148 my $jsonconfig = JSON-
>new->utf8->decode($config);
151 foreach my $target (@{$jsonconfig->{targets
}}) {
152 # only interested in iSCSI targets
153 next if !($target->{fabric
} eq 'iscsi' && $target->{wwn
} eq $scfg->{target
});
155 foreach my $tpg (@{$target->{tpgs
}}) {
156 if ($tpg->{tag
} == $tpg_tag) {
157 my $id = "$scfg->{portal}.$scfg->{target}";
158 $SETTINGS->{$id} = $tpg;
165 # seriously unhappy if the target server lacks iSCSI target configuration ...
167 die "target portal group tpg$tpg_tag not found!\n";
171 # removes the given lu_name from the local list of luns
172 my $free_lu_name = sub {
173 my ($scfg, $lu_name) = @_;
176 my $target = $get_target_settings->($scfg);
177 foreach my $lun (@{$target->{luns
}}) {
178 if ($lun->{storage_object
} ne "$BACKSTORE/$lu_name") {
183 $target->{luns
} = $new;
186 # locally registers a new lun
187 my $register_lun = sub {
188 my ($scfg, $idx, $volname) = @_;
192 storage_object
=> "$BACKSTORE/$volname",
195 my $target = $get_target_settings->($scfg);
196 push @{$target->{luns
}}, $conf;
201 # extracts the ZFS volume name from a device path
202 my $extract_volname = sub {
203 my ($scfg, $lunpath) = @_;
207 if ($lunpath =~ /^$base\/$scfg
->{pool
}\
/([\w\-]+)$/) {
214 # retrieves the LUN index for a particular object
215 my $list_view = sub {
216 my ($scfg, $timeout, $method, @params) = @_;
219 my $object = $params[0];
220 my $volname = $extract_volname->($scfg, $object);
221 my $target = $get_target_settings->($scfg);
223 return undef if !defined($volname); # nothing to search for..
225 foreach my $lun (@{$target->{luns
}}) {
226 if ($lun->{storage_object
} eq "$BACKSTORE/$volname") {
227 return $lun->{index};
234 # determines, if the given object exists on the portal
236 my ($scfg, $timeout, $method, @params) = @_;
239 my $object = $params[0];
240 my $volname = $extract_volname->($scfg, $params[0]);
241 my $target = $get_target_settings->($scfg);
243 foreach my $lun (@{$target->{luns
}}) {
244 if ($lun->{storage_object
} eq "$BACKSTORE/$volname") {
252 # adds a new LUN to the target
253 my $create_lun = sub {
254 my ($scfg, $timeout, $method, @params) = @_;
256 if ($list_lun->($scfg, $timeout, $method, @params)) {
257 die "$params[0]: LUN already exists!";
260 my $device = $params[0];
261 my $volname = $extract_volname->($scfg, $device);
262 my $tpg = $scfg->{lio_tpg
} || die "Target Portal Group not set, aborting!\n";
264 # step 1: create backstore for device
265 my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
266 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
267 die $res->{msg
} if !$res->{result
};
269 # step 2: enable unmap support on the backstore
270 @cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' );
271 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
272 die $res->{msg
} if !$res->{result
};
274 # step 3: register lun with target
275 # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
276 @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
277 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
278 die $res->{msg
} if !$res->{result
};
280 # targetcli responds with "Created LUN 99"
281 # not calculating the index ourselves, because the index at the portal might have
282 # changed without our knowledge, so relying on the number that targetcli returns
284 if ($res->{msg
} =~ /LUN (\d+)/) {
287 die "unable to determine new LUN index: $res->{msg}";
290 $register_lun->($scfg, $lun_idx, $volname);
292 # step 3: unfortunately, targetcli doesn't always save changes, no matter
293 # if auto_save_on_exit is true or not. So saving to be safe ...
294 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
299 my $delete_lun = sub {
300 my ($scfg, $timeout, $method, @params) = @_;
301 my $res = {msg
=> undef};
303 my $tpg = $scfg->{lio_tpg
} || die "Target Portal Group not set, aborting!\n";
305 my $path = $params[0];
306 my $volname = $extract_volname->($scfg, $params[0]);
307 my $target = $get_target_settings->($scfg);
309 foreach my $lun (@{$target->{luns
}}) {
310 next if $lun->{storage_object
} ne "$BACKSTORE/$volname";
312 # step 1: delete the lun
313 my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
314 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
317 } unless $res->{result
};
319 # step 2: delete the backstore
320 @cliparams = ($BACKSTORE, 'delete', $volname);
321 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
324 } unless $res->{result
};
326 # step 3: save to be safe ...
327 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
329 # update interal cache
330 $free_lu_name->($scfg, $volname);
338 my $import_lun = sub {
339 my ($scfg, $timeout, $method, @params) = @_;
341 return $create_lun->($scfg, $timeout, $method, @params);
344 # needed for example when the underlying ZFS volume has been resized
345 my $modify_lun = sub {
346 my ($scfg, $timeout, $method, @params) = @_;
347 # Nothing to do on volume modification for LIO
352 my ($scfg, $timeout, $method, @params) = @_;
358 create_lu
=> $create_lun,
359 delete_lu
=> $delete_lun,
360 import_lu
=> $import_lun,
361 modify_lu
=> $modify_lun,
362 add_view
=> $add_view,
363 list_view
=> $list_view,
364 list_lu
=> $list_lun,
367 sub run_lun_command
{
368 my ($scfg, $timeout, $method, @params) = @_;
370 # fetch configuration from target if we haven't yet or if it is stale
371 my $timediff = time - $SETTINGS_TIMESTAMP;
372 my $target = $get_target_settings->($scfg);
373 if (!$target || $timediff > $SETTINGS_MAXAGE) {
374 $SETTINGS_TIMESTAMP = time;
378 die "unknown command '$method'" unless exists $lun_cmd_map{$method};
379 my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);