]> git.proxmox.com Git - pmg-api.git/blame - PMG/API2/Nodes.pm
node: journal: minor code cleanup
[pmg-api.git] / PMG / API2 / Nodes.pm
CommitLineData
1360e6f0
DM
1package PMG::API2::NodeInfo;
2
3use strict;
4use warnings;
d934c136 5use Time::Local qw(timegm_nocheck);
c1f5acda 6use Filesys::Df;
ba11e2d3 7use Data::Dumper;
d934c136 8
1360e6f0
DM
9use PVE::INotify;
10use PVE::RESTHandler;
11use PVE::JSONSchema qw(get_standard_option);
9d82c6bc 12use PMG::RESTEnvironment;
1360e6f0 13use PVE::SafeSyslog;
c1f5acda 14use PVE::ProcFSTools;
1360e6f0 15
c1f5acda 16use PMG::pmgcfg;
1360e6f0 17use PMG::Ticket;
3d8cdfee 18use PMG::Report;
87952a8d 19use PMG::API2::Subscription;
cc115a48 20use PMG::API2::APT;
fa04fcb4 21use PMG::API2::Tasks;
163fb58f 22use PMG::API2::Services;
2551682a 23use PMG::API2::Network;
d17c5265 24use PMG::API2::ClamAV;
b00d2d73 25use PMG::API2::SpamAssassin;
70805f7d 26use PMG::API2::Postfix;
202c952a 27use PMG::API2::MailTracker;
464f7494 28use PMG::API2::Backup;
1360e6f0
DM
29
30use base qw(PVE::RESTHandler);
31
70805f7d
DM
32__PACKAGE__->register_method ({
33 subclass => "PMG::API2::Postfix",
34 path => 'postfix',
35});
36
d17c5265
DM
37__PACKAGE__->register_method ({
38 subclass => "PMG::API2::ClamAV",
39 path => 'clamav',
40});
41
b00d2d73
DC
42__PACKAGE__->register_method ({
43 subclass => "PMG::API2::SpamAssassin",
44 path => 'spamassassin',
45});
46
2551682a
DM
47__PACKAGE__->register_method ({
48 subclass => "PMG::API2::Network",
49 path => 'network',
50});
51
fa04fcb4
DM
52__PACKAGE__->register_method ({
53 subclass => "PMG::API2::Tasks",
54 path => 'tasks',
55});
56
163fb58f
DM
57__PACKAGE__->register_method ({
58 subclass => "PMG::API2::Services",
59 path => 'services',
60});
61
87952a8d
DM
62__PACKAGE__->register_method ({
63 subclass => "PMG::API2::Subscription",
64 path => 'subscription',
65});
66
cc115a48
DM
67__PACKAGE__->register_method ({
68 subclass => "PMG::API2::APT",
69 path => 'apt',
70});
71
202c952a
DM
72__PACKAGE__->register_method ({
73 subclass => "PMG::API2::MailTracker",
74 path => 'tracker',
75});
76
464f7494
DM
77__PACKAGE__->register_method ({
78 subclass => "PMG::API2::Backup",
79 path => 'backup',
80});
81
1360e6f0
DM
82__PACKAGE__->register_method ({
83 name => 'index',
84 path => '',
85 method => 'GET',
86 permissions => { user => 'all' },
87 description => "Node index.",
88 parameters => {
89 additionalProperties => 0,
90 properties => {
91 node => get_standard_option('pve-node'),
92 },
93 },
94 returns => {
95 type => 'array',
96 items => {
97 type => "object",
98 properties => {},
99 },
100 links => [ { rel => 'child', href => "{name}" } ],
101 },
102 code => sub {
103 my ($param) = @_;
104
105 my $result = [
cc115a48 106 { name => 'apt' },
464f7494 107 { name => 'backup' },
d17c5265 108 { name => 'clamav' },
b00d2d73 109 { name => 'spamassassin' },
70805f7d 110 { name => 'postfix' },
163fb58f 111 { name => 'services' },
7e4d6e51 112 { name => 'syslog' },
8b4a5410 113 { name => 'journal' },
fa04fcb4 114 { name => 'tasks' },
202c952a 115 { name => 'tracker' },
d934c136 116 { name => 'time' },
3d8cdfee 117 { name => 'report' },
c1f5acda 118 { name => 'status' },
87952a8d 119 { name => 'subscription' },
6466cf6d 120 { name => 'termproxy' },
229e2e68 121 { name => 'rrddata' },
1360e6f0
DM
122 ];
123
124 return $result;
125 }});
126
3d8cdfee
DC
127__PACKAGE__->register_method({
128 name => 'report',
129 path => 'report',
130 method => 'GET',
131 protected => 1,
132 proxyto => 'node',
133 permissions => { check => [ 'admin', 'audit' ] },
134 description => "Gather various system information about a node",
135 parameters => {
136 additionalProperties => 0,
137 properties => {
138 node => get_standard_option('pve-node'),
139 },
140 },
141 returns => {
142 type => 'string',
143 },
144 code => sub {
145 return PMG::Report::generate();
146 }});
147
065b2986
DM
148__PACKAGE__->register_method({
149 name => 'rrddata',
150 path => 'rrddata',
151 method => 'GET',
152 protected => 1, # fixme: can we avoid that?
153 proxyto => 'node',
127154c3 154 permissions => { check => [ 'admin', 'audit' ] },
065b2986
DM
155 description => "Read node RRD statistics",
156 parameters => {
157 additionalProperties => 0,
158 properties => {
159 node => get_standard_option('pve-node'),
160 timeframe => {
161 description => "Specify the time frame you are interested in.",
162 type => 'string',
163 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
164 },
165 cf => {
166 description => "The RRD consolidation function",
167 type => 'string',
168 enum => [ 'AVERAGE', 'MAX' ],
169 optional => 1,
170 },
171 },
172 },
173 returns => {
174 type => "array",
175 items => {
176 type => "object",
177 properties => {},
178 },
179 },
180 code => sub {
181 my ($param) = @_;
182
183 return PMG::Utils::create_rrd_data(
184 "pmg-node-v1.rrd", $param->{timeframe}, $param->{cf});
185 }});
186
187
7e4d6e51
DM
188__PACKAGE__->register_method({
189 name => 'syslog',
190 path => 'syslog',
191 method => 'GET',
192 description => "Read system log",
193 proxyto => 'node',
194 protected => 1,
da5777d7 195 permissions => { check => [ 'admin', 'audit' ] },
7e4d6e51
DM
196 parameters => {
197 additionalProperties => 0,
198 properties => {
199 node => get_standard_option('pve-node'),
200 start => {
201 type => 'integer',
202 minimum => 0,
203 optional => 1,
204 },
205 limit => {
206 type => 'integer',
207 minimum => 0,
208 optional => 1,
209 },
210 since => {
cf49e998 211 type => 'string',
7e4d6e51
DM
212 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
213 description => "Display all log since this date-time string.",
214 optional => 1,
215 },
216 'until' => {
cf49e998 217 type => 'string',
7e4d6e51
DM
218 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
219 description => "Display all log until this date-time string.",
220 optional => 1,
221 },
cf49e998
DM
222 service => {
223 description => "Service ID",
224 type => 'string',
225 maxLength => 128,
226 optional => 1,
227 },
7e4d6e51
DM
228 },
229 },
230 returns => {
231 type => 'array',
232 items => {
233 type => "object",
234 properties => {
235 n => {
236 description=> "Line number",
237 type=> 'integer',
238 },
239 t => {
240 description=> "Line text",
241 type => 'string',
242 }
243 }
244 }
245 },
246 code => sub {
247 my ($param) = @_;
248
f1c29260 249 my $restenv = PMG::RESTEnvironment->get();
7e4d6e51 250
cf49e998 251 my $service = $param->{service};
ef07e4d0
DM
252 $service = PMG::Utils::lookup_real_service_name($service)
253 if $service;
cf49e998
DM
254
255 my ($count, $lines) = PVE::Tools::dump_journal(
256 $param->{start}, $param->{limit},
257 $param->{since}, $param->{'until'}, $service);
7e4d6e51
DM
258
259 $restenv->set_result_attrib('total', $count);
260
261 return $lines;
262 }});
263
c2e3529a
DC
264__PACKAGE__->register_method({
265 name => 'journal',
266 path => 'journal',
267 method => 'GET',
268 description => "Read Journal",
269 proxyto => 'node',
270 permissions => { check => [ 'admin', 'audit' ] },
271 protected => 1,
272 parameters => {
273 additionalProperties => 0,
274 properties => {
275 node => get_standard_option('pve-node'),
276 since => {
13232555
TL
277 description => "Display all log since this UNIX epoch. Conflicts with 'startcursor'.",
278 type => 'integer',
279 minimum => 0,
c2e3529a
DC
280 optional => 1,
281 },
282 until => {
13232555
TL
283 description => "Display all log until this UNIX epoch. Conflicts with 'endcursor'.",
284 type => 'integer',
285 minimum => 0,
c2e3529a
DC
286 optional => 1,
287 },
288 lastentries => {
13232555 289 description => "Limit to the last X lines. Conflicts with a range.",
c2e3529a 290 type => 'integer',
13232555 291 minimum => 0,
c2e3529a
DC
292 optional => 1,
293 },
294 startcursor => {
13232555 295 description => "Start after the given Cursor. Conflicts with 'since'.",
c2e3529a
DC
296 type => 'string',
297 optional => 1,
298 },
299 endcursor => {
13232555 300 description => "End before the given Cursor. Conflicts with 'until'.",
c2e3529a
DC
301 type => 'string',
302 optional => 1,
303 },
304 },
305 },
306 returns => {
307 type => 'array',
308 items => {
309 type => "string",
310 }
311 },
312 code => sub {
313 my ($param) = @_;
314
c2e3529a
DC
315 my $cmd = ["/usr/bin/mini-journalreader"];
316 push @$cmd, '-n', $param->{lastentries} if $param->{lastentries};
317 push @$cmd, '-b', $param->{since} if $param->{since};
318 push @$cmd, '-e', $param->{until} if $param->{until};
319 push @$cmd, '-f', $param->{startcursor} if $param->{startcursor};
320 push @$cmd, '-t', $param->{endcursor} if $param->{endcursor};
321
80b9242d
TL
322 my $lines = [];
323 my $parser = sub { push @$lines, shift };
324
c2e3529a
DC
325 PVE::Tools::run_command($cmd, outfunc => $parser);
326
327 return $lines;
328 }});
329
6466cf6d 330
1360e6f0 331__PACKAGE__->register_method ({
6466cf6d
DC
332 name => 'termproxy',
333 path => 'termproxy',
1360e6f0 334 method => 'POST',
aafcc40e 335 permissions => { check => [ 'admin' ] },
fa04fcb4 336 protected => 1,
6466cf6d 337 description => "Creates a Terminal proxy.",
1360e6f0
DM
338 parameters => {
339 additionalProperties => 0,
340 properties => {
341 node => get_standard_option('pve-node'),
5e6e07df
DM
342 upgrade => {
343 type => 'boolean',
344 description => "Run 'apt-get dist-upgrade' instead of normal shell.",
345 optional => 1,
346 default => 0,
347 },
1360e6f0
DM
348 },
349 },
350 returns => {
351 additionalProperties => 0,
352 properties => {
353 user => { type => 'string' },
354 ticket => { type => 'string' },
355 port => { type => 'integer' },
356 upid => { type => 'string' },
357 },
358 },
359 code => sub {
360 my ($param) = @_;
361
362 my $node = $param->{node};
363
5e6e07df 364 if ($node ne PVE::INotify::nodename()) {
6466cf6d 365 die "termproxy to remote node not implemented";
5e6e07df
DM
366 }
367
1360e6f0
DM
368 my $authpath = "/nodes/$node";
369
9d82c6bc 370 my $restenv = PMG::RESTEnvironment->get();
1360e6f0
DM
371 my $user = $restenv->get_user();
372
5e6e07df
DM
373 raise_perm_exc('user != root@pam') if $param->{upgrade} && $user ne 'root@pam';
374
1360e6f0
DM
375 my $ticket = PMG::Ticket::assemble_vnc_ticket($user, $authpath);
376
377 my $family = PVE::Tools::get_host_address_family($node);
378 my $port = PVE::Tools::next_vnc_port($family);
379
8e6893c6
DM
380 my $shcmd;
381
382 if ($user eq 'root@pam') {
5e6e07df
DM
383 if ($param->{upgrade}) {
384 my $upgradecmd = "pmgupgrade --shell";
385 # $upgradecmd = PVE::Tools::shellquote($upgradecmd) if $remip;
386 $shcmd = [ '/bin/bash', '-c', $upgradecmd ];
387 } else {
388 $shcmd = [ '/bin/login', '-f', 'root' ];
389 }
8e6893c6
DM
390 } else {
391 $shcmd = [ '/bin/login' ];
392 }
393
6466cf6d
DC
394 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
395 '--', @$shcmd];
1360e6f0
DM
396
397 my $realcmd = sub {
398 my $upid = shift;
399
6466cf6d 400 syslog ('info', "starting termproxy $upid\n");
1360e6f0
DM
401
402 my $cmdstr = join (' ', @$cmd);
403 syslog ('info', "launch command: $cmdstr");
404
6466cf6d 405 PVE::Tools::run_command($cmd);
1360e6f0
DM
406
407 return;
408 };
409
6466cf6d 410 my $upid = $restenv->fork_worker('termproxy', "", $user, $realcmd);
1360e6f0
DM
411
412 PVE::Tools::wait_for_vnc_port($port);
413
414 return {
415 user => $user,
416 ticket => $ticket,
417 port => $port,
418 upid => $upid,
419 };
420 }});
421
422__PACKAGE__->register_method({
423 name => 'vncwebsocket',
424 path => 'vncwebsocket',
425 method => 'GET',
aafcc40e 426 permissions => { check => [ 'admin' ] },
1360e6f0
DM
427 description => "Opens a weksocket for VNC traffic.",
428 parameters => {
429 additionalProperties => 0,
430 properties => {
431 node => get_standard_option('pve-node'),
432 vncticket => {
433 description => "Ticket from previous call to vncproxy.",
434 type => 'string',
435 maxLength => 512,
436 },
437 port => {
438 description => "Port number returned by previous vncproxy call.",
439 type => 'integer',
440 minimum => 5900,
441 maximum => 5999,
442 },
443 },
444 },
445 returns => {
446 type => "object",
447 properties => {
448 port => { type => 'string' },
449 },
450 },
451 code => sub {
452 my ($param) = @_;
453
454 my $authpath = "/nodes/$param->{node}";
455
9d82c6bc 456 my $restenv = PMG::RESTEnvironment->get();
1360e6f0
DM
457 my $user = $restenv->get_user();
458
459 PMG::Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
460
461 my $port = $param->{port};
462
463 return { port => $port };
464 }});
465
bce9f371
DM
466__PACKAGE__->register_method({
467 name => 'dns',
468 path => 'dns',
469 method => 'GET',
470 description => "Read DNS settings.",
471 proxyto => 'node',
aea3488a 472 permissions => { check => [ 'admin', 'audit' ] },
bce9f371
DM
473 parameters => {
474 additionalProperties => 0,
475 properties => {
476 node => get_standard_option('pve-node'),
477 },
478 },
479 returns => {
480 type => "object",
481 additionalProperties => 0,
482 properties => {
483 search => {
484 description => "Search domain for host-name lookup.",
485 type => 'string',
486 optional => 1,
487 },
488 dns1 => {
489 description => 'First name server IP address.',
490 type => 'string',
491 optional => 1,
492 },
493 dns2 => {
494 description => 'Second name server IP address.',
495 type => 'string',
496 optional => 1,
497 },
498 dns3 => {
499 description => 'Third name server IP address.',
500 type => 'string',
501 optional => 1,
502 },
503 },
504 },
505 code => sub {
506 my ($param) = @_;
507
508 my $res = PVE::INotify::read_file('resolvconf');
509
510 return $res;
511 }});
512
513__PACKAGE__->register_method({
514 name => 'update_dns',
515 path => 'dns',
516 method => 'PUT',
517 description => "Write DNS settings.",
518 proxyto => 'node',
519 protected => 1,
520 parameters => {
521 additionalProperties => 0,
522 properties => {
523 node => get_standard_option('pve-node'),
524 search => {
525 description => "Search domain for host-name lookup.",
526 type => 'string',
527 },
528 dns1 => {
529 description => 'First name server IP address.',
530 type => 'string', format => 'ip',
531 optional => 1,
532 },
533 dns2 => {
534 description => 'Second name server IP address.',
535 type => 'string', format => 'ip',
536 optional => 1,
537 },
538 dns3 => {
539 description => 'Third name server IP address.',
540 type => 'string', format => 'ip',
541 optional => 1,
542 },
543 },
544 },
545 returns => { type => "null" },
546 code => sub {
547 my ($param) = @_;
548
549 PVE::INotify::update_file('resolvconf', $param);
550
551 return undef;
552 }});
553
554
d934c136
DM
555__PACKAGE__->register_method({
556 name => 'time',
557 path => 'time',
558 method => 'GET',
559 description => "Read server time and time zone settings.",
560 proxyto => 'node',
aea3488a 561 permissions => { check => [ 'admin', 'audit' ] },
d934c136
DM
562 parameters => {
563 additionalProperties => 0,
564 properties => {
565 node => get_standard_option('pve-node'),
566 },
567 },
568 returns => {
569 type => "object",
570 additionalProperties => 0,
571 properties => {
572 timezone => {
573 description => "Time zone",
574 type => 'string',
575 },
576 time => {
577 description => "Seconds since 1970-01-01 00:00:00 UTC.",
578 type => 'integer',
579 minimum => 1297163644,
580 },
581 localtime => {
582 description => "Seconds since 1970-01-01 00:00:00 (local time)",
583 type => 'integer',
584 minimum => 1297163644,
585 },
586 },
587 },
588 code => sub {
589 my ($param) = @_;
590
591 my $ctime = time();
592 my $ltime = timegm_nocheck(localtime($ctime));
593 my $res = {
594 timezone => PVE::INotify::read_file('timezone'),
595 time => time(),
596 localtime => $ltime,
597 };
598
599 return $res;
600 }});
601
602__PACKAGE__->register_method({
603 name => 'set_timezone',
604 path => 'time',
605 method => 'PUT',
606 description => "Set time zone.",
607 proxyto => 'node',
608 protected => 1,
609 parameters => {
610 additionalProperties => 0,
611 properties => {
612 node => get_standard_option('pve-node'),
613 timezone => {
614 description => "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.",
615 type => 'string',
616 },
617 },
618 },
619 returns => { type => "null" },
620 code => sub {
621 my ($param) = @_;
622
623 PVE::INotify::write_file('timezone', $param->{timezone});
624
625 return undef;
626 }});
627
c1f5acda
DM
628__PACKAGE__->register_method({
629 name => 'status',
630 path => 'status',
631 method => 'GET',
632 description => "Read server status. This is used by the cluster manager to test the node health.",
633 proxyto => 'node',
e4842eec 634 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
c1f5acda
DM
635 protected => 1,
636 parameters => {
637 additionalProperties => 0,
638 properties => {
639 node => get_standard_option('pve-node'),
640 },
641 },
642 returns => {
643 type => "object",
644 additionalProperties => 1,
645 properties => {
646 time => {
647 description => "Seconds since 1970-01-01 00:00:00 UTC.",
648 type => 'integer',
649 minimum => 1297163644,
650 },
651 uptime => {
652 description => "The uptime of the system in seconds.",
653 type => 'integer',
654 minimum => 0,
655 },
656 insync => {
657 description => "Database is synced with other nodes.",
658 type => 'boolean',
659 },
660 },
661 },
662 code => sub {
663 my ($param) = @_;
664
665 my $restenv = PMG::RESTEnvironment->get();
666 my $cinfo = $restenv->{cinfo};
667
668 my $ctime = time();
669
670 my $res = { time => $ctime, insync => 1 };
671
672 my $si = PMG::DBTools::cluster_sync_status($cinfo);
673 foreach my $cid (keys %$si) {
674 my $lastsync = $si->{$cid};
675 my $sdiff = $ctime - $lastsync;
676 $sdiff = 0 if $sdiff < 0;
677 $res->{insync} = 0 if $sdiff > (60*3);
678 }
679
680 my ($uptime, $idle) = PVE::ProcFSTools::read_proc_uptime();
681 $res->{uptime} = $uptime;
682
683 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
684 $res->{loadavg} = [ $avg1, $avg5, $avg15];
685
686 my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
687
688 $res->{kversion} = "$sysname $release $version";
689
690 $res->{cpuinfo} = PVE::ProcFSTools::read_cpuinfo();
691
692 my $stat = PVE::ProcFSTools::read_proc_stat();
693 $res->{cpu} = $stat->{cpu};
694 $res->{wait} = $stat->{wait};
695
696 my $meminfo = PVE::ProcFSTools::read_meminfo();
697 $res->{memory} = {
698 free => $meminfo->{memfree},
699 total => $meminfo->{memtotal},
700 used => $meminfo->{memused},
701 };
702
703 $res->{swap} = {
704 free => $meminfo->{swapfree},
705 total => $meminfo->{swaptotal},
706 used => $meminfo->{swapused},
707 };
708
709 $res->{pmgversion} = PMG::pmgcfg::package() . "/" .
710 PMG::pmgcfg::version_text();
711
712 my $dinfo = df('/', 1); # output is bytes
713
714 $res->{rootfs} = {
715 total => $dinfo->{blocks},
716 avail => $dinfo->{bavail},
717 used => $dinfo->{used},
d9f4add4 718 free => $dinfo->{blocks} - $dinfo->{used},
c1f5acda
DM
719 };
720
b4df3454
DM
721 if (my $subinfo = PVE::INotify::read_file('subscription')) {
722 if (my $level = $subinfo->{level}) {
723 $res->{level} = $level;
724 }
725 }
726
c1f5acda
DM
727 return $res;
728 }});
729
0a327ff4
DM
730__PACKAGE__->register_method({
731 name => 'node_cmd',
732 path => 'status',
733 method => 'POST',
aafcc40e 734 permissions => { check => [ 'admin' ] },
0a327ff4
DM
735 protected => 1,
736 description => "Reboot or shutdown a node.",
737 proxyto => 'node',
738 parameters => {
739 additionalProperties => 0,
740 properties => {
741 node => get_standard_option('pve-node'),
742 command => {
743 description => "Specify the command.",
744 type => 'string',
745 enum => [qw(reboot shutdown)],
746 },
747 },
748 },
749 returns => { type => "null" },
750 code => sub {
751 my ($param) = @_;
752
753 if ($param->{command} eq 'reboot') {
754 system ("(sleep 2;/sbin/reboot)&");
755 } elsif ($param->{command} eq 'shutdown') {
756 system ("(sleep 2;/sbin/poweroff)&");
757 }
758
759 return undef;
760 }});
1360e6f0
DM
761
762package PMG::API2::Nodes;
763
764use strict;
765use warnings;
766
767use PVE::RESTHandler;
768use PVE::JSONSchema qw(get_standard_option);
769
be512f56
DM
770use PMG::RESTEnvironment;
771
1360e6f0
DM
772use base qw(PVE::RESTHandler);
773
774__PACKAGE__->register_method ({
f6dcba61 775 subclass => "PMG::API2::NodeInfo",
1360e6f0
DM
776 path => '{node}',
777});
778
779__PACKAGE__->register_method ({
780 name => 'index',
781 path => '',
782 method => 'GET',
783 permissions => { user => 'all' },
784 description => "Cluster node index.",
785 parameters => {
786 additionalProperties => 0,
787 properties => {},
788 },
789 returns => {
790 type => 'array',
791 items => {
792 type => "object",
793 properties => {},
794 },
795 links => [ { rel => 'child', href => "{node}" } ],
796 },
797 code => sub {
798 my ($param) = @_;
799
800 my $nodename = PVE::INotify::nodename();
be512f56
DM
801
802 my $res = [ { node => $nodename } ];
803
804 my $done = {};
805
806 $done->{$nodename} = 1;
807
808 my $restenv = PMG::RESTEnvironment->get();
809 my $cinfo = $restenv->{cinfo};
810
811 foreach my $ni (values %{$cinfo->{ids}}) {
812 push @$res, { node => $ni->{name} } if !$done->{$ni->{name}};
813 $done->{$ni->{name}} = 1;
814 }
1360e6f0
DM
815
816 return $res;
817 }});
818
819
8201;