]>
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 | ||
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 { | |
ccdf8ddb | 71 | run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout); |
46c6107e UR |
72 | }; |
73 | if ($@) { | |
ccdf8ddb TL |
74 | $res = { |
75 | result => 0, | |
76 | msg => $err, | |
77 | } | |
46c6107e | 78 | } else { |
ccdf8ddb TL |
79 | $res = { |
80 | result => 1, | |
81 | msg => $msg, | |
82 | } | |
46c6107e UR |
83 | } |
84 | ||
85 | return $res; | |
86 | }; | |
87 | ||
88 | # fetch targetcli configuration from the portal | |
89 | my $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) { | |
ccdf8ddb TL |
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 ''; | |
46c6107e UR |
121 | } |
122 | ||
123 | die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq ''; | |
124 | ||
125 | return $msg; | |
126 | }; | |
127 | ||
128 | my $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 | |
139 | my $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 | |
177 | my $free_lu_name = sub { | |
178 | my ($lu_name) = @_; | |
179 | my $new; | |
180 | ||
181 | foreach my $lun (@{$SETTINGS->{target}->{luns}}) { | |
ccdf8ddb TL |
182 | if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") { |
183 | push @$new, $lun; | |
184 | } | |
46c6107e UR |
185 | } |
186 | ||
187 | $SETTINGS->{target}->{luns} = $new; | |
188 | }; | |
189 | ||
d9254744 | 190 | # locally registers a new lun |
46c6107e UR |
191 | my $register_lun = sub { |
192 | my ($scfg, $idx, $volname) = @_; | |
193 | ||
194 | my $conf = { | |
ccdf8ddb TL |
195 | index => $idx, |
196 | storage_object => "$BACKSTORE/$volname", | |
197 | is_new => 1, | |
46c6107e UR |
198 | }; |
199 | push @{$SETTINGS->{target}->{luns}}, $conf; | |
200 | ||
201 | return $conf; | |
202 | }; | |
203 | ||
204 | # extracts the ZFS volume name from a device path | |
205 | my $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 | |
218 | my $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}}) { | |
ccdf8ddb TL |
226 | if ($lun->{storage_object} eq "$BACKSTORE/$volname") { |
227 | return $lun->{index}; | |
228 | } | |
46c6107e UR |
229 | } |
230 | ||
231 | return $lun; | |
232 | }; | |
233 | ||
234 | # determines, if the given object exists on the portal | |
235 | my $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}}) { | |
ccdf8ddb TL |
243 | if ($lun->{storage_object} eq "$BACKSTORE/$volname") { |
244 | return $object; | |
245 | } | |
46c6107e UR |
246 | } |
247 | ||
248 | return $name; | |
249 | }; | |
250 | ||
251 | # adds a new LUN to the target | |
252 | my $create_lun = sub { | |
253 | my ($scfg, $timeout, $method, @params) = @_; | |
254 | ||
255 | if ($list_lun->($scfg, $timeout, $method, @params)) { | |
ccdf8ddb | 256 | die "$params[0]: LUN already exists!"; |
46c6107e UR |
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+)/) { | |
ccdf8ddb | 279 | $lun_idx = $1; |
46c6107e | 280 | } else { |
ccdf8ddb | 281 | die "unable to determine new LUN index: $res->{msg}"; |
46c6107e UR |
282 | } |
283 | ||
284 | $register_lun->($scfg, $lun_idx, $volname); | |
285 | ||
d9254744 | 286 | # step 3: unfortunately, targetcli doesn't always save changes, no matter |
46c6107e UR |
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 | ||
293 | my $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 | ||
331 | my $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 | |
338 | my $modify_lun = sub { | |
339 | my ($scfg, $timeout, $method, @params) = @_; | |
340 | my $msg; | |
341 | ||
342 | $msg = $delete_lun->($scfg, $timeout, $method, @params); | |
343 | if ($msg) { | |
ccdf8ddb | 344 | $msg = $create_lun->($scfg, $timeout, $method, @params); |
46c6107e UR |
345 | } |
346 | ||
347 | return $msg; | |
348 | }; | |
349 | ||
350 | my $add_view = sub { | |
351 | my ($scfg, $timeout, $method, @params) = @_; | |
352 | ||
353 | return ''; | |
354 | }; | |
355 | ||
356 | my %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 | ||
366 | sub 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; | |
ccdf8ddb TL |
372 | if (!$SETTINGS || $timediff > $SETTINGS_MAXAGE) { |
373 | $SETTINGS_TIMESTAMP = time; | |
374 | $parser->($scfg); | |
46c6107e UR |
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 | ||
383 | sub get_base { | |
384 | return '/dev'; | |
385 | } | |
386 | ||
387 | 1; |