]>
Commit | Line | Data |
---|---|---|
46c6107e UR |
1 | package 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 | ||
22 | use strict; | |
23 | use warnings; | |
24 | use PVE::Tools qw(run_command); | |
25 | use JSON; | |
26 | ||
27 | sub get_base; | |
28 | ||
29 | # targetcli constants | |
30 | # config file location differs from distro to distro | |
31 | my @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 | ); | |
35 | my $BACKSTORE = '/backstores/block'; | |
36 | ||
37 | my $SETTINGS = undef; | |
38 | my $SETTINGS_TIMESTAMP = 0; | |
39 | my $SETTINGS_MAXAGE = 15; # in seconds | |
40 | ||
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'; | |
45 | ||
46 | my $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 | |
82 | my $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 | ||
114 | my $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 | ||
609f8ec6 DB |
124 | # Return settings of a specific target |
125 | my $get_target_settings = sub { | |
126 | my ($scfg) = @_; | |
127 | ||
128 | my $id = "$scfg->{portal}.$scfg->{target}"; | |
129 | return undef if !$SETTINGS; | |
130 | return $SETTINGS->{$id}; | |
131 | }; | |
132 | ||
46c6107e UR |
133 | # fetches and parses targetcli config from the portal |
134 | my $parser = sub { | |
135 | my ($scfg) = @_; | |
136 | my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n"; | |
137 | my $tpg_tag; | |
138 | ||
139 | if ($tpg =~ /^tpg(\d+)$/) { | |
140 | $tpg_tag = $1; | |
141 | } else { | |
142 | die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n"; | |
143 | } | |
144 | ||
46c6107e UR |
145 | my $config = $get_config->($scfg); |
146 | my $jsonconfig = JSON->new->utf8->decode($config); | |
147 | ||
148 | my $haveTarget = 0; | |
f15ac9b5 | 149 | foreach my $target (@{$jsonconfig->{targets}}) { |
46c6107e | 150 | # only interested in iSCSI targets |
f15ac9b5 TL |
151 | next if !($target->{fabric} eq 'iscsi' && $target->{wwn} eq $scfg->{target}); |
152 | # find correct TPG | |
153 | foreach my $tpg (@{$target->{tpgs}}) { | |
154 | if ($tpg->{tag} == $tpg_tag) { | |
d4abdf4e SI |
155 | my $res = []; |
156 | foreach my $lun (@{$tpg->{luns}}) { | |
157 | my ($idx, $storage_object); | |
158 | if ($lun->{index} =~ /^(\d+)$/) { | |
159 | $idx = $1; | |
160 | } | |
161 | if ($lun->{storage_object} =~ m|^($BACKSTORE/.*)$|) { | |
162 | $storage_object = $1; | |
163 | } | |
164 | die "Invalid lun definition in config!\n" | |
165 | if !(defined($idx) && defined($storage_object)); | |
166 | push @$res, { index => $idx, storage_object => $storage_object }; | |
167 | } | |
168 | ||
609f8ec6 | 169 | my $id = "$scfg->{portal}.$scfg->{target}"; |
d4abdf4e | 170 | $SETTINGS->{$id}->{luns} = $res; |
f15ac9b5 TL |
171 | $haveTarget = 1; |
172 | last; | |
46c6107e UR |
173 | } |
174 | } | |
175 | } | |
176 | ||
177 | # seriously unhappy if the target server lacks iSCSI target configuration ... | |
178 | if (!$haveTarget) { | |
179 | die "target portal group tpg$tpg_tag not found!\n"; | |
180 | } | |
181 | }; | |
182 | ||
eb89269f DB |
183 | # Get prefix for backstores |
184 | my $get_backstore_prefix = sub { | |
185 | my ($scfg) = @_; | |
186 | my $pool = $scfg->{pool}; | |
187 | $pool =~ s/\//-/g; | |
188 | return $pool . '-'; | |
189 | }; | |
190 | ||
46c6107e UR |
191 | # removes the given lu_name from the local list of luns |
192 | my $free_lu_name = sub { | |
609f8ec6 | 193 | my ($scfg, $lu_name) = @_; |
46c6107e | 194 | |
f15ac9b5 | 195 | my $new = []; |
609f8ec6 DB |
196 | my $target = $get_target_settings->($scfg); |
197 | foreach my $lun (@{$target->{luns}}) { | |
ccdf8ddb TL |
198 | if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") { |
199 | push @$new, $lun; | |
200 | } | |
46c6107e UR |
201 | } |
202 | ||
609f8ec6 | 203 | $target->{luns} = $new; |
46c6107e UR |
204 | }; |
205 | ||
d9254744 | 206 | # locally registers a new lun |
46c6107e UR |
207 | my $register_lun = sub { |
208 | my ($scfg, $idx, $volname) = @_; | |
209 | ||
210 | my $conf = { | |
ccdf8ddb TL |
211 | index => $idx, |
212 | storage_object => "$BACKSTORE/$volname", | |
213 | is_new => 1, | |
46c6107e | 214 | }; |
609f8ec6 DB |
215 | my $target = $get_target_settings->($scfg); |
216 | push @{$target->{luns}}, $conf; | |
46c6107e UR |
217 | |
218 | return $conf; | |
219 | }; | |
220 | ||
221 | # extracts the ZFS volume name from a device path | |
222 | my $extract_volname = sub { | |
223 | my ($scfg, $lunpath) = @_; | |
224 | my $volname = undef; | |
225 | ||
226 | my $base = get_base; | |
227 | if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) { | |
228 | $volname = $1; | |
eb89269f DB |
229 | my $prefix = $get_backstore_prefix->($scfg); |
230 | my $target = $get_target_settings->($scfg); | |
231 | foreach my $lun (@{$target->{luns}}) { | |
232 | # If we have a lun with the pool prefix matching this vol, then return this one | |
233 | # like pool-pve-vm-100-disk-0 | |
234 | # Else, just fallback to the old name scheme which is vm-100-disk-0 | |
235 | if ($lun->{storage_object} =~ /^$BACKSTORE\/($prefix$volname)$/) { | |
236 | return $1; | |
237 | } | |
238 | } | |
46c6107e UR |
239 | } |
240 | ||
241 | return $volname; | |
242 | }; | |
243 | ||
244 | # retrieves the LUN index for a particular object | |
245 | my $list_view = sub { | |
246 | my ($scfg, $timeout, $method, @params) = @_; | |
247 | my $lun = undef; | |
248 | ||
249 | my $object = $params[0]; | |
2dbca26d | 250 | my $volname = $extract_volname->($scfg, $object); |
609f8ec6 | 251 | my $target = $get_target_settings->($scfg); |
46c6107e | 252 | |
61137a54 TL |
253 | return undef if !defined($volname); # nothing to search for.. |
254 | ||
609f8ec6 | 255 | foreach my $lun (@{$target->{luns}}) { |
ccdf8ddb TL |
256 | if ($lun->{storage_object} eq "$BACKSTORE/$volname") { |
257 | return $lun->{index}; | |
258 | } | |
46c6107e UR |
259 | } |
260 | ||
261 | return $lun; | |
262 | }; | |
263 | ||
264 | # determines, if the given object exists on the portal | |
265 | my $list_lun = sub { | |
266 | my ($scfg, $timeout, $method, @params) = @_; | |
46c6107e UR |
267 | |
268 | my $object = $params[0]; | |
9258d945 | 269 | my $volname = $extract_volname->($scfg, $object); |
609f8ec6 | 270 | my $target = $get_target_settings->($scfg); |
46c6107e | 271 | |
609f8ec6 | 272 | foreach my $lun (@{$target->{luns}}) { |
ccdf8ddb TL |
273 | if ($lun->{storage_object} eq "$BACKSTORE/$volname") { |
274 | return $object; | |
275 | } | |
46c6107e UR |
276 | } |
277 | ||
6726a2e0 | 278 | return undef; |
46c6107e UR |
279 | }; |
280 | ||
281 | # adds a new LUN to the target | |
282 | my $create_lun = sub { | |
283 | my ($scfg, $timeout, $method, @params) = @_; | |
284 | ||
285 | if ($list_lun->($scfg, $timeout, $method, @params)) { | |
ccdf8ddb | 286 | die "$params[0]: LUN already exists!"; |
46c6107e UR |
287 | } |
288 | ||
289 | my $device = $params[0]; | |
290 | my $volname = $extract_volname->($scfg, $device); | |
eb89269f DB |
291 | # Here we create a new device, so we didn't get the volname prefixed with the pool name |
292 | # as extract_volname couldn't find a matching vol yet | |
293 | $volname = $get_backstore_prefix->($scfg) . $volname; | |
46c6107e UR |
294 | my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n"; |
295 | ||
296 | # step 1: create backstore for device | |
297 | my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" ); | |
298 | my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams); | |
299 | die $res->{msg} if !$res->{result}; | |
300 | ||
b7c8738f DB |
301 | # step 2: enable unmap support on the backstore |
302 | @cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' ); | |
303 | $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams); | |
304 | die $res->{msg} if !$res->{result}; | |
305 | ||
306 | # step 3: register lun with target | |
46c6107e UR |
307 | # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar |
308 | @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" ); | |
309 | $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams); | |
310 | die $res->{msg} if !$res->{result}; | |
311 | ||
312 | # targetcli responds with "Created LUN 99" | |
313 | # not calculating the index ourselves, because the index at the portal might have | |
314 | # changed without our knowledge, so relying on the number that targetcli returns | |
315 | my $lun_idx; | |
316 | if ($res->{msg} =~ /LUN (\d+)/) { | |
ccdf8ddb | 317 | $lun_idx = $1; |
46c6107e | 318 | } else { |
ccdf8ddb | 319 | die "unable to determine new LUN index: $res->{msg}"; |
46c6107e UR |
320 | } |
321 | ||
322 | $register_lun->($scfg, $lun_idx, $volname); | |
323 | ||
d9254744 | 324 | # step 3: unfortunately, targetcli doesn't always save changes, no matter |
46c6107e UR |
325 | # if auto_save_on_exit is true or not. So saving to be safe ... |
326 | $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig'); | |
327 | ||
328 | return $res->{msg}; | |
329 | }; | |
330 | ||
331 | my $delete_lun = sub { | |
332 | my ($scfg, $timeout, $method, @params) = @_; | |
333 | my $res = {msg => undef}; | |
334 | ||
335 | my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n"; | |
336 | ||
337 | my $path = $params[0]; | |
9258d945 | 338 | my $volname = $extract_volname->($scfg, $path); |
609f8ec6 | 339 | my $target = $get_target_settings->($scfg); |
46c6107e | 340 | |
609f8ec6 | 341 | foreach my $lun (@{$target->{luns}}) { |
f15ac9b5 TL |
342 | next if $lun->{storage_object} ne "$BACKSTORE/$volname"; |
343 | ||
344 | # step 1: delete the lun | |
345 | my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" ); | |
346 | my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams); | |
347 | do { | |
348 | die $res->{msg}; | |
349 | } unless $res->{result}; | |
350 | ||
351 | # step 2: delete the backstore | |
352 | @cliparams = ($BACKSTORE, 'delete', $volname); | |
353 | $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams); | |
354 | do { | |
355 | die $res->{msg}; | |
356 | } unless $res->{result}; | |
357 | ||
358 | # step 3: save to be safe ... | |
359 | $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig'); | |
360 | ||
ffc31266 | 361 | # update internal cache |
609f8ec6 | 362 | $free_lu_name->($scfg, $volname); |
f15ac9b5 TL |
363 | |
364 | last; | |
46c6107e UR |
365 | } |
366 | ||
367 | return $res->{msg}; | |
368 | }; | |
369 | ||
370 | my $import_lun = sub { | |
371 | my ($scfg, $timeout, $method, @params) = @_; | |
372 | ||
373 | return $create_lun->($scfg, $timeout, $method, @params); | |
374 | }; | |
375 | ||
376 | # needed for example when the underlying ZFS volume has been resized | |
377 | my $modify_lun = sub { | |
378 | my ($scfg, $timeout, $method, @params) = @_; | |
525ed353 DB |
379 | # Nothing to do on volume modification for LIO |
380 | return undef; | |
46c6107e UR |
381 | }; |
382 | ||
383 | my $add_view = sub { | |
384 | my ($scfg, $timeout, $method, @params) = @_; | |
385 | ||
386 | return ''; | |
387 | }; | |
388 | ||
389 | my %lun_cmd_map = ( | |
390 | create_lu => $create_lun, | |
391 | delete_lu => $delete_lun, | |
392 | import_lu => $import_lun, | |
393 | modify_lu => $modify_lun, | |
394 | add_view => $add_view, | |
395 | list_view => $list_view, | |
396 | list_lu => $list_lun, | |
397 | ); | |
398 | ||
399 | sub run_lun_command { | |
400 | my ($scfg, $timeout, $method, @params) = @_; | |
401 | ||
f15ac9b5 | 402 | # fetch configuration from target if we haven't yet or if it is stale |
46c6107e | 403 | my $timediff = time - $SETTINGS_TIMESTAMP; |
609f8ec6 DB |
404 | my $target = $get_target_settings->($scfg); |
405 | if (!$target || $timediff > $SETTINGS_MAXAGE) { | |
ccdf8ddb TL |
406 | $SETTINGS_TIMESTAMP = time; |
407 | $parser->($scfg); | |
46c6107e UR |
408 | } |
409 | ||
410 | die "unknown command '$method'" unless exists $lun_cmd_map{$method}; | |
411 | my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params); | |
412 | ||
413 | return $msg; | |
414 | } | |
415 | ||
416 | sub get_base { | |
417 | return '/dev'; | |
418 | } | |
419 | ||
420 | 1; |