]> git.proxmox.com Git - pve-storage.git/blame - PVE/Storage/LunCmd/LIO.pm
LIO: followup: shorter stderr/out logging
[pve-storage.git] / PVE / Storage / LunCmd / LIO.pm
CommitLineData
46c6107e
UR
1package PVE::Storage::LunCmd::LIO;
2
d9254744 3# lightly based on code from Iet.pm
46c6107e
UR
4#
5# additional changes:
6# -----------------------------------------------------------------
7# Copyright (c) 2018 BestSolution.at EDV Systemhaus GmbH
8# All Rights Reserved.
9#
10# This software is released under the terms of the
11#
12# "GNU Affero General Public License"
d9254744 13#
46c6107e
UR
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
d9254744 18#
46c6107e
UR
19# Author: udo.rader@bestsolution.at
20# -----------------------------------------------------------------
21
22use strict;
23use warnings;
24use PVE::Tools qw(run_command);
25use JSON;
26
27sub get_base;
28
29# targetcli constants
30# config file location differs from distro to distro
31my @CONFIG_FILES = (
d9254744 32 '/etc/rtslib-fb-target/saveconfig.json', # Debian 9.x et al
46c6107e
UR
33 '/etc/target/saveconfig.json' , # ArchLinux, CentOS
34);
35my $BACKSTORE = '/backstores/block';
36
37my $SETTINGS = undef;
38my $SETTINGS_TIMESTAMP = 0;
39my $SETTINGS_MAXAGE = 15; # in seconds
40
41my @ssh_opts = ('-o', 'BatchMode=yes');
42my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
43my $id_rsa_path = '/etc/pve/priv/zfs';
44my $targetcli = '/usr/bin/targetcli';
45
46my $execute_remote_command = sub {
47 my ($scfg, $timeout, $remote_command, @params) = @_;
48
49 my $msg = '';
50 my $err = undef;
51 my $target;
52 my $cmd;
53 my $res = ();
54
55 $timeout = 10 if !$timeout;
56
ff69c660
TL
57 my $output = sub { $msg .= "$_[0]\n" };
58 my $errfunc = sub { $err .= "$_[0]\n" };
46c6107e
UR
59
60 $target = 'root@' . $scfg->{portal};
61 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $remote_command, @params];
62
63 eval {
ccdf8ddb 64 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
46c6107e
UR
65 };
66 if ($@) {
ccdf8ddb
TL
67 $res = {
68 result => 0,
69 msg => $err,
70 }
46c6107e 71 } else {
ccdf8ddb
TL
72 $res = {
73 result => 1,
74 msg => $msg,
75 }
46c6107e
UR
76 }
77
78 return $res;
79};
80
81# fetch targetcli configuration from the portal
82my $read_config = sub {
83 my ($scfg, $timeout) = @_;
84
85 my $msg = '';
86 my $err = undef;
87 my $luncmd = 'cat';
88 my $target;
89 my $retry = 1;
90
91 $timeout = 10 if !$timeout;
92
ff69c660
TL
93 my $output = sub { $msg .= "$_[0]\n" };
94 my $errfunc = sub { $err .= "$_[0]\n" };
46c6107e
UR
95
96 $target = 'root@' . $scfg->{portal};
97
98 foreach my $oneFile (@CONFIG_FILES) {
ccdf8ddb
TL
99 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $oneFile];
100 eval {
101 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
102 };
103 if ($@) {
104 die $err if ($err !~ /No such file or directory/);
105 }
106 return $msg if $msg ne '';
46c6107e
UR
107 }
108
109 die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq '';
110
111 return $msg;
112};
113
114my $get_config = sub {
115 my ($scfg) = @_;
116 my @conf = undef;
117
118 my $config = $read_config->($scfg, undef);
119 die "Missing config file" unless $config;
120
121 return $config;
122};
123
124# fetches and parses targetcli config from the portal
125my $parser = sub {
126 my ($scfg) = @_;
127 my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
128 my $tpg_tag;
129
130 if ($tpg =~ /^tpg(\d+)$/) {
131 $tpg_tag = $1;
132 } else {
133 die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n";
134 }
135
136 my $base = get_base;
137
138 my $config = $get_config->($scfg);
139 my $jsonconfig = JSON->new->utf8->decode($config);
140
141 my $haveTarget = 0;
142 foreach my $oneTarget (@{$jsonconfig->{targets}}) {
143 # only interested in iSCSI targets
144 if ($oneTarget->{fabric} eq 'iscsi' && $oneTarget->{wwn} eq $scfg->{target}) {
145 # find correct TPG
146 foreach my $oneTpg (@{$oneTarget->{tpgs}}) {
147 if ($oneTpg->{tag} == $tpg_tag) {
148 $SETTINGS->{target} = $oneTpg;
149 $haveTarget = 1;
150 last;
151 }
152 }
153 }
154 }
155
156 # seriously unhappy if the target server lacks iSCSI target configuration ...
157 if (!$haveTarget) {
158 die "target portal group tpg$tpg_tag not found!\n";
159 }
160};
161
162# removes the given lu_name from the local list of luns
163my $free_lu_name = sub {
164 my ($lu_name) = @_;
165 my $new;
166
167 foreach my $lun (@{$SETTINGS->{target}->{luns}}) {
ccdf8ddb
TL
168 if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") {
169 push @$new, $lun;
170 }
46c6107e
UR
171 }
172
173 $SETTINGS->{target}->{luns} = $new;
174};
175
d9254744 176# locally registers a new lun
46c6107e
UR
177my $register_lun = sub {
178 my ($scfg, $idx, $volname) = @_;
179
180 my $conf = {
ccdf8ddb
TL
181 index => $idx,
182 storage_object => "$BACKSTORE/$volname",
183 is_new => 1,
46c6107e
UR
184 };
185 push @{$SETTINGS->{target}->{luns}}, $conf;
186
187 return $conf;
188};
189
190# extracts the ZFS volume name from a device path
191my $extract_volname = sub {
192 my ($scfg, $lunpath) = @_;
193 my $volname = undef;
194
195 my $base = get_base;
196 if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
197 $volname = $1;
198 }
199
200 return $volname;
201};
202
203# retrieves the LUN index for a particular object
204my $list_view = sub {
205 my ($scfg, $timeout, $method, @params) = @_;
206 my $lun = undef;
207
208 my $object = $params[0];
209 my $volname = $extract_volname->($scfg, $params[0]);
210
211 foreach my $lun (@{$SETTINGS->{target}->{luns}}) {
ccdf8ddb
TL
212 if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
213 return $lun->{index};
214 }
46c6107e
UR
215 }
216
217 return $lun;
218};
219
220# determines, if the given object exists on the portal
221my $list_lun = sub {
222 my ($scfg, $timeout, $method, @params) = @_;
223 my $name = undef;
224
225 my $object = $params[0];
226 my $volname = $extract_volname->($scfg, $params[0]);
227
228 foreach my $lun (@{$SETTINGS->{target}->{luns}}) {
ccdf8ddb
TL
229 if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
230 return $object;
231 }
46c6107e
UR
232 }
233
234 return $name;
235};
236
237# adds a new LUN to the target
238my $create_lun = sub {
239 my ($scfg, $timeout, $method, @params) = @_;
240
241 if ($list_lun->($scfg, $timeout, $method, @params)) {
ccdf8ddb 242 die "$params[0]: LUN already exists!";
46c6107e
UR
243 }
244
245 my $device = $params[0];
246 my $volname = $extract_volname->($scfg, $device);
247 my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
248
249 # step 1: create backstore for device
250 my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
251 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
252 die $res->{msg} if !$res->{result};
253
254 # step 2: register lun with target
255 # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
256 @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
257 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
258 die $res->{msg} if !$res->{result};
259
260 # targetcli responds with "Created LUN 99"
261 # not calculating the index ourselves, because the index at the portal might have
262 # changed without our knowledge, so relying on the number that targetcli returns
263 my $lun_idx;
264 if ($res->{msg} =~ /LUN (\d+)/) {
ccdf8ddb 265 $lun_idx = $1;
46c6107e 266 } else {
ccdf8ddb 267 die "unable to determine new LUN index: $res->{msg}";
46c6107e
UR
268 }
269
270 $register_lun->($scfg, $lun_idx, $volname);
271
d9254744 272 # step 3: unfortunately, targetcli doesn't always save changes, no matter
46c6107e
UR
273 # if auto_save_on_exit is true or not. So saving to be safe ...
274 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
275
276 return $res->{msg};
277};
278
279my $delete_lun = sub {
280 my ($scfg, $timeout, $method, @params) = @_;
281 my $res = {msg => undef};
282
283 my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
284
285 my $path = $params[0];
286 my $volname = $extract_volname->($scfg, $params[0]);
287
288 foreach my $lun (@{$SETTINGS->{target}->{luns}}) {
289 if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
290 # step 1: delete the lun
291 my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
292 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
293 do {
294 die $res->{msg};
295 } unless $res->{result};
296
297 # step 2: delete the backstore
298 @cliparams = ($BACKSTORE, 'delete', $volname);
299 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
300 do {
301 die $res->{msg};
302 } unless $res->{result};
303
304 # step 3: save to be safe ...
305 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
306
307 # update interal cache
308 $free_lu_name->($volname);
309
310 last;
311 }
312 }
313
314 return $res->{msg};
315};
316
317my $import_lun = sub {
318 my ($scfg, $timeout, $method, @params) = @_;
319
320 return $create_lun->($scfg, $timeout, $method, @params);
321};
322
323# needed for example when the underlying ZFS volume has been resized
324my $modify_lun = sub {
325 my ($scfg, $timeout, $method, @params) = @_;
326 my $msg;
327
328 $msg = $delete_lun->($scfg, $timeout, $method, @params);
329 if ($msg) {
ccdf8ddb 330 $msg = $create_lun->($scfg, $timeout, $method, @params);
46c6107e
UR
331 }
332
333 return $msg;
334};
335
336my $add_view = sub {
337 my ($scfg, $timeout, $method, @params) = @_;
338
339 return '';
340};
341
342my %lun_cmd_map = (
343 create_lu => $create_lun,
344 delete_lu => $delete_lun,
345 import_lu => $import_lun,
346 modify_lu => $modify_lun,
347 add_view => $add_view,
348 list_view => $list_view,
349 list_lu => $list_lun,
350);
351
352sub run_lun_command {
353 my ($scfg, $timeout, $method, @params) = @_;
354
355 # fetch configuration from target if we haven't yet
356 # or if our configuration is stale
357 my $timediff = time - $SETTINGS_TIMESTAMP;
ccdf8ddb
TL
358 if (!$SETTINGS || $timediff > $SETTINGS_MAXAGE) {
359 $SETTINGS_TIMESTAMP = time;
360 $parser->($scfg);
46c6107e
UR
361 }
362
363 die "unknown command '$method'" unless exists $lun_cmd_map{$method};
364 my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
365
366 return $msg;
367}
368
369sub get_base {
370 return '/dev';
371}
372
3731;