bump version to 7.0-13
[pve-storage.git] / PVE / Storage / LunCmd / LIO.pm
1 package PVE::Storage::LunCmd::LIO;
2
3 # lightly based on code from Iet.pm
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"
13 #
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
18 #
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 = (
32 '/etc/rtslib-fb-target/saveconfig.json', # Debian 9.x et al
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
57 my $output = sub { $msg .= "$_[0]\n" };
58 my $errfunc = sub { $err .= "$_[0]\n" };
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 {
64 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
65 };
66 if ($@) {
67 $res = {
68 result => 0,
69 msg => $err,
70 }
71 } else {
72 $res = {
73 result => 1,
74 msg => $msg,
75 }
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
93 my $output = sub { $msg .= "$_[0]\n" };
94 my $errfunc = sub { $err .= "$_[0]\n" };
95
96 $target = 'root@' . $scfg->{portal};
97
98 foreach my $oneFile (@CONFIG_FILES) {
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 '';
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
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
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
145 my $base = get_base;
146
147 my $config = $get_config->($scfg);
148 my $jsonconfig = JSON->new->utf8->decode($config);
149
150 my $haveTarget = 0;
151 foreach my $target (@{$jsonconfig->{targets}}) {
152 # only interested in iSCSI targets
153 next if !($target->{fabric} eq 'iscsi' && $target->{wwn} eq $scfg->{target});
154 # find correct TPG
155 foreach my $tpg (@{$target->{tpgs}}) {
156 if ($tpg->{tag} == $tpg_tag) {
157 my $id = "$scfg->{portal}.$scfg->{target}";
158 $SETTINGS->{$id} = $tpg;
159 $haveTarget = 1;
160 last;
161 }
162 }
163 }
164
165 # seriously unhappy if the target server lacks iSCSI target configuration ...
166 if (!$haveTarget) {
167 die "target portal group tpg$tpg_tag not found!\n";
168 }
169 };
170
171 # Get prefix for backstores
172 my $get_backstore_prefix = sub {
173 my ($scfg) = @_;
174 my $pool = $scfg->{pool};
175 $pool =~ s/\//-/g;
176 return $pool . '-';
177 };
178
179 # removes the given lu_name from the local list of luns
180 my $free_lu_name = sub {
181 my ($scfg, $lu_name) = @_;
182
183 my $new = [];
184 my $target = $get_target_settings->($scfg);
185 foreach my $lun (@{$target->{luns}}) {
186 if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") {
187 push @$new, $lun;
188 }
189 }
190
191 $target->{luns} = $new;
192 };
193
194 # locally registers a new lun
195 my $register_lun = sub {
196 my ($scfg, $idx, $volname) = @_;
197
198 my $conf = {
199 index => $idx,
200 storage_object => "$BACKSTORE/$volname",
201 is_new => 1,
202 };
203 my $target = $get_target_settings->($scfg);
204 push @{$target->{luns}}, $conf;
205
206 return $conf;
207 };
208
209 # extracts the ZFS volume name from a device path
210 my $extract_volname = sub {
211 my ($scfg, $lunpath) = @_;
212 my $volname = undef;
213
214 my $base = get_base;
215 if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
216 $volname = $1;
217 my $prefix = $get_backstore_prefix->($scfg);
218 my $target = $get_target_settings->($scfg);
219 foreach my $lun (@{$target->{luns}}) {
220 # If we have a lun with the pool prefix matching this vol, then return this one
221 # like pool-pve-vm-100-disk-0
222 # Else, just fallback to the old name scheme which is vm-100-disk-0
223 if ($lun->{storage_object} =~ /^$BACKSTORE\/($prefix$volname)$/) {
224 return $1;
225 }
226 }
227 }
228
229 return $volname;
230 };
231
232 # retrieves the LUN index for a particular object
233 my $list_view = sub {
234 my ($scfg, $timeout, $method, @params) = @_;
235 my $lun = undef;
236
237 my $object = $params[0];
238 my $volname = $extract_volname->($scfg, $object);
239 my $target = $get_target_settings->($scfg);
240
241 return undef if !defined($volname); # nothing to search for..
242
243 foreach my $lun (@{$target->{luns}}) {
244 if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
245 return $lun->{index};
246 }
247 }
248
249 return $lun;
250 };
251
252 # determines, if the given object exists on the portal
253 my $list_lun = sub {
254 my ($scfg, $timeout, $method, @params) = @_;
255 my $name = undef;
256
257 my $object = $params[0];
258 my $volname = $extract_volname->($scfg, $object);
259 my $target = $get_target_settings->($scfg);
260
261 foreach my $lun (@{$target->{luns}}) {
262 if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
263 return $object;
264 }
265 }
266
267 return $name;
268 };
269
270 # adds a new LUN to the target
271 my $create_lun = sub {
272 my ($scfg, $timeout, $method, @params) = @_;
273
274 if ($list_lun->($scfg, $timeout, $method, @params)) {
275 die "$params[0]: LUN already exists!";
276 }
277
278 my $device = $params[0];
279 my $volname = $extract_volname->($scfg, $device);
280 # Here we create a new device, so we didn't get the volname prefixed with the pool name
281 # as extract_volname couldn't find a matching vol yet
282 $volname = $get_backstore_prefix->($scfg) . $volname;
283 my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
284
285 # step 1: create backstore for device
286 my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
287 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
288 die $res->{msg} if !$res->{result};
289
290 # step 2: enable unmap support on the backstore
291 @cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' );
292 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
293 die $res->{msg} if !$res->{result};
294
295 # step 3: register lun with target
296 # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
297 @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
298 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
299 die $res->{msg} if !$res->{result};
300
301 # targetcli responds with "Created LUN 99"
302 # not calculating the index ourselves, because the index at the portal might have
303 # changed without our knowledge, so relying on the number that targetcli returns
304 my $lun_idx;
305 if ($res->{msg} =~ /LUN (\d+)/) {
306 $lun_idx = $1;
307 } else {
308 die "unable to determine new LUN index: $res->{msg}";
309 }
310
311 $register_lun->($scfg, $lun_idx, $volname);
312
313 # step 3: unfortunately, targetcli doesn't always save changes, no matter
314 # if auto_save_on_exit is true or not. So saving to be safe ...
315 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
316
317 return $res->{msg};
318 };
319
320 my $delete_lun = sub {
321 my ($scfg, $timeout, $method, @params) = @_;
322 my $res = {msg => undef};
323
324 my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
325
326 my $path = $params[0];
327 my $volname = $extract_volname->($scfg, $path);
328 my $target = $get_target_settings->($scfg);
329
330 foreach my $lun (@{$target->{luns}}) {
331 next if $lun->{storage_object} ne "$BACKSTORE/$volname";
332
333 # step 1: delete the lun
334 my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
335 my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
336 do {
337 die $res->{msg};
338 } unless $res->{result};
339
340 # step 2: delete the backstore
341 @cliparams = ($BACKSTORE, 'delete', $volname);
342 $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
343 do {
344 die $res->{msg};
345 } unless $res->{result};
346
347 # step 3: save to be safe ...
348 $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
349
350 # update interal cache
351 $free_lu_name->($scfg, $volname);
352
353 last;
354 }
355
356 return $res->{msg};
357 };
358
359 my $import_lun = sub {
360 my ($scfg, $timeout, $method, @params) = @_;
361
362 return $create_lun->($scfg, $timeout, $method, @params);
363 };
364
365 # needed for example when the underlying ZFS volume has been resized
366 my $modify_lun = sub {
367 my ($scfg, $timeout, $method, @params) = @_;
368 # Nothing to do on volume modification for LIO
369 return undef;
370 };
371
372 my $add_view = sub {
373 my ($scfg, $timeout, $method, @params) = @_;
374
375 return '';
376 };
377
378 my %lun_cmd_map = (
379 create_lu => $create_lun,
380 delete_lu => $delete_lun,
381 import_lu => $import_lun,
382 modify_lu => $modify_lun,
383 add_view => $add_view,
384 list_view => $list_view,
385 list_lu => $list_lun,
386 );
387
388 sub run_lun_command {
389 my ($scfg, $timeout, $method, @params) = @_;
390
391 # fetch configuration from target if we haven't yet or if it is stale
392 my $timediff = time - $SETTINGS_TIMESTAMP;
393 my $target = $get_target_settings->($scfg);
394 if (!$target || $timediff > $SETTINGS_MAXAGE) {
395 $SETTINGS_TIMESTAMP = time;
396 $parser->($scfg);
397 }
398
399 die "unknown command '$method'" unless exists $lun_cmd_map{$method};
400 my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
401
402 return $msg;
403 }
404
405 sub get_base {
406 return '/dev';
407 }
408
409 1;