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