1 package PVE
::API2
::Nodes
::Nodeinfo
;
9 use HTTP
::Status
qw(:constants);
11 use POSIX
qw(LONG_MAX);
12 use Time
::Local
qw(timegm_nocheck);
17 use PVE
::AccessControl
;
18 use PVE
::Cluster
qw(cfs_read_file);
19 use PVE
::DataCenterConfig
;
20 use PVE
::Exception
qw(raise raise_perm_exc raise_param_exc);
23 use PVE
::HA
::Env
::PVE2
;
25 use PVE
::JSONSchema
qw(get_standard_option);
31 use PVE
::RPCEnvironment
;
40 use PVE
::API2
::Capabilities
;
42 use PVE
::API2
::Certificates
;
44 use PVE
::API2
::Firewall
::Host
;
45 use PVE
::API2
::Hardware
;
46 use PVE
::API2
::LXC
::Status
;
48 use PVE
::API2
::Network
;
49 use PVE
::API2
::NodeConfig
;
50 use PVE
::API2
::Qemu
::CPU
;
52 use PVE
::API2
::Replication
;
53 use PVE
::API2
::Services
;
54 use PVE
::API2
::Storage
::Scan
;
55 use PVE
::API2
::Storage
::Status
;
56 use PVE
::API2
::Subscription
;
58 use PVE
::API2
::VZDump
;
62 require PVE
::API2
::Network
::SDN
::Zones
::Status
;
66 use base
qw(PVE::RESTHandler);
68 __PACKAGE__-
>register_method ({
69 subclass
=> "PVE::API2::Qemu",
73 __PACKAGE__-
>register_method ({
74 subclass
=> "PVE::API2::LXC",
78 __PACKAGE__-
>register_method ({
79 subclass
=> "PVE::API2::Ceph",
83 __PACKAGE__-
>register_method ({
84 subclass
=> "PVE::API2::VZDump",
88 __PACKAGE__-
>register_method ({
89 subclass
=> "PVE::API2::Services",
93 __PACKAGE__-
>register_method ({
94 subclass
=> "PVE::API2::Subscription",
95 path
=> 'subscription',
98 __PACKAGE__-
>register_method ({
99 subclass
=> "PVE::API2::Network",
103 __PACKAGE__-
>register_method ({
104 subclass
=> "PVE::API2::Tasks",
108 __PACKAGE__-
>register_method ({
109 subclass
=> "PVE::API2::Storage::Scan",
113 __PACKAGE__-
>register_method ({
114 subclass
=> "PVE::API2::Hardware",
118 __PACKAGE__-
>register_method ({
119 subclass
=> "PVE::API2::Capabilities",
120 path
=> 'capabilities',
123 __PACKAGE__-
>register_method ({
124 subclass
=> "PVE::API2::Storage::Status",
128 __PACKAGE__-
>register_method ({
129 subclass
=> "PVE::API2::Disks",
133 __PACKAGE__-
>register_method ({
134 subclass
=> "PVE::API2::APT",
138 __PACKAGE__-
>register_method ({
139 subclass
=> "PVE::API2::Firewall::Host",
143 __PACKAGE__-
>register_method ({
144 subclass
=> "PVE::API2::Replication",
145 path
=> 'replication',
148 __PACKAGE__-
>register_method ({
149 subclass
=> "PVE::API2::Certificates",
150 path
=> 'certificates',
154 __PACKAGE__-
>register_method ({
155 subclass
=> "PVE::API2::NodeConfig",
160 __PACKAGE__-
>register_method ({
161 subclass
=> "PVE::API2::Network::SDN::Zones::Status",
165 __PACKAGE__-
>register_method ({
169 permissions
=> { user
=> 'all' },
170 description
=> "SDN index.",
172 additionalProperties
=> 0,
174 node
=> get_standard_option
('pve-node'),
183 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
195 __PACKAGE__-
>register_method ({
199 permissions
=> { user
=> 'all' },
200 description
=> "Node index.",
202 additionalProperties
=> 0,
204 node
=> get_standard_option
('pve-node'),
213 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
219 { name
=> 'aplinfo' },
221 { name
=> 'capabilities' },
223 { name
=> 'certificates' },
224 { name
=> 'config' },
227 { name
=> 'firewall' },
229 { name
=> 'journal' },
231 { name
=> 'netstat' },
232 { name
=> 'network' },
234 { name
=> 'replication' },
235 { name
=> 'report' },
236 { name
=> 'rrd' }, # fixme: remove?
237 { name
=> 'rrddata' },# fixme: remove?
239 { name
=> 'services' },
240 { name
=> 'spiceshell' },
241 { name
=> 'startall' },
242 { name
=> 'status' },
243 { name
=> 'stopall' },
244 { name
=> 'storage' },
245 { name
=> 'subscription' },
246 { name
=> 'syslog' },
248 { name
=> 'termproxy' },
250 { name
=> 'version' },
251 { name
=> 'vncshell' },
252 { name
=> 'vzdump' },
253 { name
=> 'wakeonlan' },
256 push @$result, { name
=> 'sdn' } if $have_sdn;
261 __PACKAGE__-
>register_method ({
266 permissions
=> { user
=> 'all' },
267 description
=> "API version details",
269 additionalProperties
=> 0,
271 node
=> get_standard_option
('pve-node'),
279 description
=> 'The current installed pve-manager package version',
283 description
=> 'The current installed Proxmox VE Release',
287 description
=> 'The short git commit hash ID from which this version was build',
292 my ($resp, $param) = @_;
294 return PVE
::pvecfg
::version_info
();
297 __PACKAGE__-
>register_method({
302 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
304 description
=> "Read node status",
307 additionalProperties
=> 0,
309 node
=> get_standard_option
('pve-node'),
326 my ($uptime, $idle) = PVE
::ProcFSTools
::read_proc_uptime
();
327 $res->{uptime
} = $uptime;
329 my ($avg1, $avg5, $avg15) = PVE
::ProcFSTools
::read_loadavg
();
330 $res->{loadavg
} = [ $avg1, $avg5, $avg15];
332 my ($sysname, $nodename, $release, $version, $machine) = POSIX
::uname
();
334 $res->{kversion
} = "$sysname $release $version";
336 $res->{cpuinfo
} = PVE
::ProcFSTools
::read_cpuinfo
();
338 my $stat = PVE
::ProcFSTools
::read_proc_stat
();
339 $res->{cpu
} = $stat->{cpu
};
340 $res->{wait} = $stat->{wait};
342 my $meminfo = PVE
::ProcFSTools
::read_meminfo
();
344 free
=> $meminfo->{memfree
},
345 total
=> $meminfo->{memtotal
},
346 used
=> $meminfo->{memused
},
350 shared
=> $meminfo->{memshared
},
354 free
=> $meminfo->{swapfree
},
355 total
=> $meminfo->{swaptotal
},
356 used
=> $meminfo->{swapused
},
359 $res->{pveversion
} = PVE
::pvecfg
::package() . "/" .
360 PVE
::pvecfg
::version_text
();
362 my $dinfo = df
('/', 1); # output is bytes
365 total
=> $dinfo->{blocks
},
366 avail
=> $dinfo->{bavail
},
367 used
=> $dinfo->{used
},
368 free
=> $dinfo->{blocks
} - $dinfo->{used
},
374 __PACKAGE__-
>register_method({
379 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
381 description
=> "Read tap/vm network device interface counters",
384 additionalProperties
=> 0,
386 node
=> get_standard_option
('pve-node'),
401 my $netdev = PVE
::ProcFSTools
::read_proc_net_dev
();
402 foreach my $dev (sort keys %$netdev) {
403 next if $dev !~ m/^(?:tap|veth)([1-9]\d*)i(\d+)$/;
404 my ($vmid, $netid) = ($1, $2);
409 in => $netdev->{$dev}->{transmit
},
410 out
=> $netdev->{$dev}->{receive
},
417 __PACKAGE__-
>register_method({
422 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
424 description
=> "Execute multiple commands in order.",
426 protected
=> 1, # avoid problems with proxy code
428 additionalProperties
=> 0,
430 node
=> get_standard_option
('pve-node'),
432 description
=> "JSON encoded array of commands.",
447 my $rpcenv = PVE
::RPCEnvironment
::get
();
448 my $user = $rpcenv->get_user();
450 my $commands = eval { decode_json
($param->{commands
}); };
452 die "commands param did not contain valid JSON: $@" if $@;
453 die "commands is not an array" if ref($commands) ne "ARRAY";
455 foreach my $cmd (@$commands) {
457 die "$cmd is not a valid command" if (ref($cmd) ne "HASH" || !$cmd->{path
} || !$cmd->{method});
461 my $path = "nodes/$param->{node}/$cmd->{path}";
464 my ($handler, $info) = PVE
::API2-
>find_handler($cmd->{method}, $path, $uri_param);
465 if (!$handler || !$info) {
466 die "no handler for '$path'\n";
469 foreach my $p (keys %{$cmd->{args
}}) {
470 raise_param_exc
({ $p => "duplicate parameter" }) if defined($uri_param->{$p});
471 $uri_param->{$p} = $cmd->{args
}->{$p};
474 # check access permissions
475 $rpcenv->check_api2_permissions($info->{permissions
}, $user, $uri_param);
479 data
=> $handler->handle($info, $uri_param),
483 my $resp = { status
=> HTTP_INTERNAL_SERVER_ERROR
};
484 if (ref($err) eq "PVE::Exception") {
485 $resp->{status
} = $err->{code
} if $err->{code
};
486 $resp->{errors
} = $err->{errors
} if $err->{errors
};
487 $resp->{message
} = $err->{msg
};
489 $resp->{message
} = $err;
499 __PACKAGE__-
>register_method({
504 check
=> ['perm', '/nodes/{node}', [ 'Sys.PowerMgmt' ]],
507 description
=> "Reboot or shutdown a node.",
510 additionalProperties
=> 0,
512 node
=> get_standard_option
('pve-node'),
514 description
=> "Specify the command.",
516 enum
=> [qw(reboot shutdown)],
520 returns
=> { type
=> "null" },
524 if ($param->{command
} eq 'reboot') {
525 system ("(sleep 2;/sbin/reboot)&");
526 } elsif ($param->{command
} eq 'shutdown') {
527 system ("(sleep 2;/sbin/poweroff)&");
533 __PACKAGE__-
>register_method({
538 check
=> ['perm', '/nodes/{node}', [ 'Sys.PowerMgmt' ]],
541 description
=> "Try to wake a node via 'wake on LAN' network packet.",
543 additionalProperties
=> 0,
545 node
=> get_standard_option
('pve-node', {
546 description
=> 'target node for wake on LAN packet',
548 my $members = PVE
::Cluster
::get_members
();
549 return [ grep { !$members->{$_}->{online
} } keys %$members ];
556 format
=> 'mac-addr',
557 description
=> 'MAC address used to assemble the WoL magic packet.',
562 my $node = $param->{node
};
564 die "'$node' is local node, cannot wake my self!\n"
565 if $node eq 'localhost' || $node eq PVE
::INotify
::nodename
();
567 PVE
::Cluster
::check_node_exists
($node);
569 my $config = PVE
::NodeConfig
::load_config
($node);
570 my $mac_addr = $config->{wakeonlan
};
571 if (!defined($mac_addr)) {
572 die "No wake on LAN MAC address defined for '$node'!\n";
576 my $packet = chr(0xff) x
6 . pack('H*', $mac_addr) x
16;
578 my $addr = gethostbyname('255.255.255.255');
579 my $port = getservbyname('discard', 'udp');
580 my $to = Socket
::pack_sockaddr_in
($port, $addr);
582 socket(my $sock, Socket
::AF_INET
, Socket
::SOCK_DGRAM
, Socket
::IPPROTO_UDP
)
583 || die "Unable to open socket: $!\n";
584 setsockopt($sock, Socket
::SOL_SOCKET
, Socket
::SO_BROADCAST
, 1)
585 || die "Unable to set socket option: $!\n";
587 send($sock, $packet, 0, $to)
588 || die "Unable to send packet: $!\n";
592 return $config->{wakeonlan
};
595 __PACKAGE__-
>register_method({
599 protected
=> 1, # fixme: can we avoid that?
601 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
603 description
=> "Read node RRD statistics (returns PNG)",
605 additionalProperties
=> 0,
607 node
=> get_standard_option
('pve-node'),
609 description
=> "Specify the time frame you are interested in.",
611 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
614 description
=> "The list of datasources you want to display.",
615 type
=> 'string', format
=> 'pve-configid-list',
618 description
=> "The RRD consolidation function",
620 enum
=> [ 'AVERAGE', 'MAX' ],
628 filename
=> { type
=> 'string' },
634 return PVE
::RRD
::create_rrd_graph
(
635 "pve2-node/$param->{node}", $param->{timeframe
},
636 $param->{ds
}, $param->{cf
});
640 __PACKAGE__-
>register_method({
644 protected
=> 1, # fixme: can we avoid that?
646 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
648 description
=> "Read node RRD statistics",
650 additionalProperties
=> 0,
652 node
=> get_standard_option
('pve-node'),
654 description
=> "Specify the time frame you are interested in.",
656 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
659 description
=> "The RRD consolidation function",
661 enum
=> [ 'AVERAGE', 'MAX' ],
676 return PVE
::RRD
::create_rrd_data
(
677 "pve2-node/$param->{node}", $param->{timeframe
}, $param->{cf
});
680 __PACKAGE__-
>register_method({
684 description
=> "Read system log",
687 check
=> ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
691 additionalProperties
=> 0,
693 node
=> get_standard_option
('pve-node'),
706 pattern
=> '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
707 description
=> "Display all log since this date-time string.",
712 pattern
=> '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
713 description
=> "Display all log until this date-time string.",
717 description
=> "Service ID",
730 description
=> "Line number",
734 description
=> "Line text",
743 my $rpcenv = PVE
::RPCEnvironment
::get
();
744 my $user = $rpcenv->get_user();
745 my $node = $param->{node
};
748 if ($param->{service
}) {
749 my $service_aliases = {
750 'postfix' => 'postfix@-',
753 $service = $service_aliases->{$param->{service
}} // $param->{service
};
756 my ($count, $lines) = PVE
::Tools
::dump_journal
($param->{start
}, $param->{limit
},
757 $param->{since
}, $param->{until}, $service);
759 $rpcenv->set_result_attrib('total', $count);
764 __PACKAGE__-
>register_method({
768 description
=> "Read Journal",
771 check
=> ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
775 additionalProperties
=> 0,
777 node
=> get_standard_option
('pve-node'),
781 description
=> "Display all log since this UNIX epoch. Conflicts with 'startcursor'.",
787 description
=> "Display all log until this UNIX epoch. Conflicts with 'endcursor'.",
791 description
=> "Limit to the last X lines. Conflicts with a range.",
797 description
=> "Start after the given Cursor. Conflicts with 'since'",
802 description
=> "End before the given Cursor. Conflicts with 'until'",
817 my $rpcenv = PVE
::RPCEnvironment
::get
();
818 my $user = $rpcenv->get_user();
820 my $cmd = ["/usr/bin/mini-journalreader"];
821 push @$cmd, '-n', $param->{lastentries
} if $param->{lastentries
};
822 push @$cmd, '-b', $param->{since
} if $param->{since
};
823 push @$cmd, '-e', $param->{until} if $param->{until};
824 push @$cmd, '-f', $param->{startcursor
} if $param->{startcursor
};
825 push @$cmd, '-t', $param->{endcursor
} if $param->{endcursor
};
828 my $parser = sub { push @$lines, shift };
830 PVE
::Tools
::run_command
($cmd, outfunc
=> $parser);
837 my $shell_cmd_map = {
839 cmd
=> [ '/bin/login', '-f', 'root' ],
842 cmd
=> [ '/usr/bin/pveupgrade', '--shell' ],
845 cmd
=> [ '/usr/bin/pveceph', 'install' ],
850 sub get_shell_command
{
851 my ($user, $shellcmd, $args) = @_;
854 if ($user eq 'root@pam') {
855 if (defined($shellcmd) && exists($shell_cmd_map->{$shellcmd})) {
856 my $def = $shell_cmd_map->{$shellcmd};
857 $cmd = [ @{$def->{cmd
}} ]; # clone
858 if (defined($args) && $def->{allow_args
}) {
859 push @$cmd, split("\0", $args);
862 $cmd = [ '/bin/login', '-f', 'root' ];
865 # non-root must always login for now, we do not have a superuser role!
866 $cmd = [ '/bin/login' ];
871 my $get_vnc_connection_info = sub {
876 my ($remip, $family);
877 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
878 ($remip, $family) = PVE
::Cluster
::remote_node_ip
($node);
879 $remote_cmd = ['/usr/bin/ssh', '-e', 'none', '-t', $remip , '--'];
881 $family = PVE
::Tools
::get_host_address_family
($node);
883 my $port = PVE
::Tools
::next_vnc_port
($family);
885 return ($port, $remote_cmd);
888 __PACKAGE__-
>register_method ({
894 description
=> "Restricted to users on realm 'pam'",
895 check
=> ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
897 description
=> "Creates a VNC Shell proxy.",
899 additionalProperties
=> 0,
901 node
=> get_standard_option
('pve-node'),
904 description
=> "Run specific command or default to login.",
905 enum
=> [keys %$shell_cmd_map],
911 description
=> "Add parameters to a command. Encoded as null terminated strings.",
919 description
=> "use websocket instead of standard vnc.",
923 description
=> "sets the width of the console in pixels.",
930 description
=> "sets the height of the console in pixels.",
938 additionalProperties
=> 0,
940 user
=> { type
=> 'string' },
941 ticket
=> { type
=> 'string' },
942 cert
=> { type
=> 'string' },
943 port
=> { type
=> 'integer' },
944 upid
=> { type
=> 'string' },
950 my $rpcenv = PVE
::RPCEnvironment
::get
();
951 my ($user, undef, $realm) = PVE
::AccessControl
::verify_username
($rpcenv->get_user());
953 raise_perm_exc
("realm != pam") if $realm ne 'pam';
955 if (defined($param->{cmd
}) && $param->{cmd
} eq 'upgrade' && $user ne 'root@pam') {
956 raise_perm_exc
('user != root@pam');
959 my $node = $param->{node
};
961 my $authpath = "/nodes/$node";
962 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($user, $authpath);
964 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
967 my ($port, $remcmd) = $get_vnc_connection_info->($node);
969 my $shcmd = get_shell_command
($user, $param->{cmd
}, $param->{'cmd-opts'});
973 my $cmd = ['/usr/bin/vncterm',
975 '-timeout', $timeout,
976 '-authpath', $authpath,
977 '-perm', 'Sys.Console',
980 push @$cmd, '-width', $param->{width
} if $param->{width
};
981 push @$cmd, '-height', $param->{height
} if $param->{height
};
983 if ($param->{websocket
}) {
984 $ENV{PVE_VNC_TICKET
} = $ticket; # pass ticket to vncterm
985 push @$cmd, '-notls', '-listen', 'localhost';
988 push @$cmd, '-c', @$remcmd, @$shcmd;
993 syslog
('info', "starting vnc proxy $upid\n");
995 my $cmdstr = join (' ', @$cmd);
996 syslog
('info', "launch command: $cmdstr");
999 foreach my $k (keys %ENV) {
1000 next if $k eq 'PVE_VNC_TICKET';
1001 next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE';
1006 PVE
::Tools
::run_command
($cmd, errmsg
=> "vncterm failed", keeplocale
=> 1);
1009 syslog
('err', $err);
1015 my $upid = $rpcenv->fork_worker('vncshell', "", $user, $realcmd);
1017 PVE
::Tools
::wait_for_vnc_port
($port);
1028 __PACKAGE__-
>register_method ({
1029 name
=> 'termproxy',
1030 path
=> 'termproxy',
1034 description
=> "Restricted to users on realm 'pam'",
1035 check
=> ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1037 description
=> "Creates a VNC Shell proxy.",
1039 additionalProperties
=> 0,
1041 node
=> get_standard_option
('pve-node'),
1044 description
=> "Run specific command or default to login.",
1045 enum
=> [keys %$shell_cmd_map],
1051 description
=> "Add parameters to a command. Encoded as null terminated strings.",
1059 additionalProperties
=> 0,
1061 user
=> { type
=> 'string' },
1062 ticket
=> { type
=> 'string' },
1063 port
=> { type
=> 'integer' },
1064 upid
=> { type
=> 'string' },
1070 my $rpcenv = PVE
::RPCEnvironment
::get
();
1071 my ($user, undef, $realm) = PVE
::AccessControl
::verify_username
($rpcenv->get_user());
1072 raise_perm_exc
("realm $realm != pam") if $realm ne 'pam';
1074 my $node = $param->{node
};
1075 my $authpath = "/nodes/$node";
1076 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($user, $authpath);
1078 my ($port, $remcmd) = $get_vnc_connection_info->($node);
1080 my $shcmd = get_shell_command
($user, $param->{cmd
}, $param->{'cmd-opts'});
1085 syslog
('info', "starting termproxy $upid\n");
1088 '/usr/bin/termproxy',
1090 '--path', $authpath,
1091 '--perm', 'Sys.Console',
1094 push @$cmd, @$remcmd, @$shcmd;
1096 PVE
::Tools
::run_command
($cmd);
1098 my $upid = $rpcenv->fork_worker('vncshell', "", $user, $realcmd);
1100 PVE
::Tools
::wait_for_vnc_port
($port);
1110 __PACKAGE__-
>register_method({
1111 name
=> 'vncwebsocket',
1112 path
=> 'vncwebsocket',
1115 description
=> "Restricted to users on realm 'pam'. You also need to pass a valid ticket (vncticket).",
1116 check
=> ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1118 description
=> "Opens a websocket for VNC traffic.",
1120 additionalProperties
=> 0,
1122 node
=> get_standard_option
('pve-node'),
1124 description
=> "Ticket from previous call to vncproxy.",
1129 description
=> "Port number returned by previous vncproxy call.",
1139 port
=> { type
=> 'string' },
1145 my $rpcenv = PVE
::RPCEnvironment
::get
();
1147 my ($user, undef, $realm) = PVE
::AccessControl
::verify_username
($rpcenv->get_user());
1149 raise_perm_exc
("realm != pam") if $realm ne 'pam';
1151 my $authpath = "/nodes/$param->{node}";
1153 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $user, $authpath);
1155 my $port = $param->{port
};
1157 return { port
=> $port };
1160 __PACKAGE__-
>register_method ({
1161 name
=> 'spiceshell',
1162 path
=> 'spiceshell',
1167 description
=> "Restricted to users on realm 'pam'",
1168 check
=> ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1170 description
=> "Creates a SPICE shell.",
1172 additionalProperties
=> 0,
1174 node
=> get_standard_option
('pve-node'),
1175 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
1178 description
=> "Run specific command or default to login.",
1179 enum
=> [keys %$shell_cmd_map],
1185 description
=> "Add parameters to a command. Encoded as null terminated strings.",
1192 returns
=> get_standard_option
('remote-viewer-config'),
1196 my $rpcenv = PVE
::RPCEnvironment
::get
();
1197 my $authuser = $rpcenv->get_user();
1199 my ($user, undef, $realm) = PVE
::AccessControl
::verify_username
($authuser);
1201 raise_perm_exc
("realm != pam") if $realm ne 'pam';
1203 if (defined($param->{cmd
}) && $param->{cmd
} eq 'upgrade' && $user ne 'root@pam') {
1204 raise_perm_exc
('user != root@pam');
1207 my $node = $param->{node
};
1208 my $proxy = $param->{proxy
};
1210 my $authpath = "/nodes/$node";
1211 my $permissions = 'Sys.Console';
1213 my $shcmd = get_shell_command
($user, $param->{cmd
}, $param->{'cmd-opts'});
1215 my $title = "Shell on '$node'";
1217 return PVE
::API2Tools
::run_spiceterm
($authpath, $permissions, 0, $node, $proxy, $title, $shcmd);
1220 __PACKAGE__-
>register_method({
1225 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1227 description
=> "Read DNS settings.",
1230 additionalProperties
=> 0,
1232 node
=> get_standard_option
('pve-node'),
1237 additionalProperties
=> 0,
1240 description
=> "Search domain for host-name lookup.",
1245 description
=> 'First name server IP address.',
1250 description
=> 'Second name server IP address.',
1255 description
=> 'Third name server IP address.',
1264 my $res = PVE
::INotify
::read_file
('resolvconf');
1269 __PACKAGE__-
>register_method({
1270 name
=> 'update_dns',
1273 description
=> "Write DNS settings.",
1275 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
1280 additionalProperties
=> 0,
1282 node
=> get_standard_option
('pve-node'),
1284 description
=> "Search domain for host-name lookup.",
1288 description
=> 'First name server IP address.',
1289 type
=> 'string', format
=> 'ip',
1293 description
=> 'Second name server IP address.',
1294 type
=> 'string', format
=> 'ip',
1298 description
=> 'Third name server IP address.',
1299 type
=> 'string', format
=> 'ip',
1304 returns
=> { type
=> "null" },
1308 PVE
::INotify
::update_file
('resolvconf', $param);
1313 __PACKAGE__-
>register_method({
1318 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1320 description
=> "Read server time and time zone settings.",
1323 additionalProperties
=> 0,
1325 node
=> get_standard_option
('pve-node'),
1330 additionalProperties
=> 0,
1333 description
=> "Time zone",
1337 description
=> "Seconds since 1970-01-01 00:00:00 UTC.",
1339 minimum
=> 1297163644,
1340 renderer
=> 'timestamp',
1343 description
=> "Seconds since 1970-01-01 00:00:00 (local time)",
1345 minimum
=> 1297163644,
1346 renderer
=> 'timestamp_gmt',
1354 my $ltime = timegm_nocheck
(localtime($ctime));
1356 timezone
=> PVE
::INotify
::read_file
('timezone'),
1358 localtime => $ltime,
1364 __PACKAGE__-
>register_method({
1365 name
=> 'set_timezone',
1368 description
=> "Set time zone.",
1370 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
1375 additionalProperties
=> 0,
1377 node
=> get_standard_option
('pve-node'),
1379 description
=> "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.",
1384 returns
=> { type
=> "null" },
1388 PVE
::INotify
::write_file
('timezone', $param->{timezone
});
1393 __PACKAGE__-
>register_method({
1400 description
=> "Get list of appliances.",
1403 additionalProperties
=> 0,
1405 node
=> get_standard_option
('pve-node'),
1418 my $list = PVE
::APLInfo
::load_data
();
1421 for my $appliance (values %{$list->{all
}}) {
1422 next if $appliance->{'package'} eq 'pve-web-news';
1423 push @$res, $appliance;
1429 __PACKAGE__-
>register_method({
1430 name
=> 'apl_download',
1434 check
=> ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
1436 description
=> "Download appliance templates.",
1440 additionalProperties
=> 0,
1442 node
=> get_standard_option
('pve-node'),
1443 storage
=> get_standard_option
('pve-storage-id', {
1444 description
=> "The storage where the template will be stored",
1445 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
1449 description
=> "The template which will downloaded",
1451 completion
=> \
&complete_templet_repo
,
1455 returns
=> { type
=> "string" },
1459 my $rpcenv = PVE
::RPCEnvironment
::get
();
1460 my $user = $rpcenv->get_user();
1462 my $node = $param->{node
};
1463 my $template = $param->{template
};
1465 my $list = PVE
::APLInfo
::load_data
();
1466 my $appliance = $list->{all
}->{$template};
1467 raise_param_exc
({ template
=> "no such template"}) if !$appliance;
1469 my $cfg = PVE
::Storage
::config
();
1470 my $scfg = PVE
::Storage
::storage_check_enabled
($cfg, $param->{storage
}, $node);
1472 die "unknown template type '$appliance->{type}'\n"
1473 if !($appliance->{type
} eq 'openvz' || $appliance->{type
} eq 'lxc');
1475 die "storage '$param->{storage}' does not support templates\n"
1476 if !$scfg->{content
}->{vztmpl
};
1478 my $tmpldir = PVE
::Storage
::get_vztmpl_dir
($cfg, $param->{storage
});
1481 my $dccfg = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
1483 PVE
::Tools
::download_file_from_url
("$tmpldir/$template", $appliance->{location
}, {
1485 sha512sum
=> $appliance->{sha512sum
},
1486 md5sum
=> $appliance->{md5sum
},
1487 http_proxy
=> $dccfg->{http_proxy
},
1491 my $upid = $rpcenv->fork_worker('download', $template, $user, $worker);
1496 __PACKAGE__-
>register_method({
1501 check
=> ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1504 description
=> "Gather various systems information about a node",
1507 additionalProperties
=> 0,
1509 node
=> get_standard_option
('pve-node'),
1516 return PVE
::Report
::generate
();
1519 # returns a list of VMIDs, those can be filtered by
1520 # * current parent node
1522 # * guest is a template (default: skip)
1523 # * guest is HA manged (default: skip)
1524 my $get_filtered_vmlist = sub {
1525 my ($nodename, $vmfilter, $templates, $ha_managed) = @_;
1527 my $vmlist = PVE
::Cluster
::get_vmlist
();
1529 my $vms_allowed = {};
1530 if (defined($vmfilter)) {
1531 foreach my $vmid (PVE
::Tools
::split_list
($vmfilter)) {
1532 $vms_allowed->{$vmid} = 1;
1537 foreach my $vmid (keys %{$vmlist->{ids
}}) {
1538 next if %$vms_allowed && !$vms_allowed->{$vmid};
1540 my $d = $vmlist->{ids
}->{$vmid};
1541 next if $nodename && $d->{node
} ne $nodename;
1545 if ($d->{type
} eq 'lxc') {
1546 $class = 'PVE::LXC::Config';
1547 } elsif ($d->{type
} eq 'qemu') {
1548 $class = 'PVE::QemuConfig';
1550 die "unknown VM type '$d->{type}'\n";
1553 my $conf = $class->load_config($vmid);
1554 return if !$templates && $class->is_template($conf);
1555 return if !$ha_managed && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
1557 $res->{$vmid}->{conf
} = $conf;
1558 $res->{$vmid}->{type
} = $d->{type
};
1559 $res->{$vmid}->{class} = $class;
1567 # return all VMs which should get started/stopped on power up/down
1568 my $get_start_stop_list = sub {
1569 my ($nodename, $autostart, $vmfilter) = @_;
1571 # do not skip HA vms on force or if a specific VMID set is wanted
1572 my $include_ha_managed = defined($vmfilter) ?
1 : 0;
1574 my $vmlist = &$get_filtered_vmlist($nodename, $vmfilter, undef, $include_ha_managed);
1577 foreach my $vmid (keys %$vmlist) {
1578 my $conf = $vmlist->{$vmid}->{conf
};
1580 next if $autostart && !$conf->{onboot
};
1583 if ($conf->{startup
}) {
1584 $startup = PVE
::JSONSchema
::pve_parse_startup_order
($conf->{startup
});
1587 $startup->{order
} = LONG_MAX
if !defined($startup->{order
});
1589 $resList->{$startup->{order
}}->{$vmid} = $startup;
1590 $resList->{$startup->{order
}}->{$vmid}->{type
} = $vmlist->{$vmid}->{type
};
1596 my $remove_locks_on_startup = sub {
1597 my ($nodename) = @_;
1599 my $vmlist = &$get_filtered_vmlist($nodename, undef, undef, 1);
1601 foreach my $vmid (keys %$vmlist) {
1602 my $conf = $vmlist->{$vmid}->{conf
};
1603 my $class = $vmlist->{$vmid}->{class};
1606 if ($class->has_lock($conf, 'backup')) {
1607 $class->remove_lock($vmid, 'backup');
1608 my $msg = "removed left over backup lock from '$vmid'!";
1609 warn "$msg\n"; # prints to task log
1610 syslog
('warning', $msg);
1616 __PACKAGE__-
>register_method ({
1622 check
=> ['perm', '/', [ 'VM.PowerMgmt' ]],
1625 description
=> "Start all VMs and containers located on this node (by default only those with onboot=1).",
1627 additionalProperties
=> 0,
1629 node
=> get_standard_option
('pve-node'),
1634 description
=> "Issue start command even if virtual guest have 'onboot' not set or set to off.",
1637 description
=> "Only consider guests from this comma separated list of VMIDs.",
1638 type
=> 'string', format
=> 'pve-vmid-list',
1649 my $rpcenv = PVE
::RPCEnvironment
::get
();
1650 my $authuser = $rpcenv->get_user();
1652 my $nodename = $param->{node
};
1653 $nodename = PVE
::INotify
::nodename
() if $nodename eq 'localhost';
1655 my $force = $param->{force
};
1659 $rpcenv->{type
} = 'priv'; # to start tasks in background
1661 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
1662 print "waiting for quorum ...\n";
1665 } while (!PVE
::Cluster
::check_cfs_quorum
(1));
1666 print "got quorum\n";
1669 eval { # remove backup locks, but avoid running into a scheduled backup job
1670 PVE
::Tools
::lock_file
('/var/run/vzdump.lock', 10, $remove_locks_on_startup, $nodename);
1673 my $autostart = $force ?
undef : 1;
1674 my $startList = &$get_start_stop_list($nodename, $autostart, $param->{vms
});
1676 # Note: use numeric sorting with <=>
1677 foreach my $order (sort {$a <=> $b} keys %$startList) {
1678 my $vmlist = $startList->{$order};
1680 foreach my $vmid (sort {$a <=> $b} keys %$vmlist) {
1681 my $d = $vmlist->{$vmid};
1683 PVE
::Cluster
::check_cfs_quorum
(); # abort when we loose quorum
1686 my $default_delay = 0;
1690 if ($d->{type
} eq 'lxc') {
1692 return if PVE
::LXC
::check_running
($vmid);
1693 print STDERR
"Starting CT $vmid\n";
1694 $upid = PVE
::API2
::LXC
::Status-
>vm_start({node
=> $nodename, vmid
=> $vmid });
1695 } elsif ($d->{type
} eq 'qemu') {
1697 $default_delay = 3; # to reduce load
1698 return if PVE
::QemuServer
::check_running
($vmid, 1);
1699 print STDERR
"Starting VM $vmid\n";
1700 $upid = PVE
::API2
::Qemu-
>vm_start({node
=> $nodename, vmid
=> $vmid });
1702 die "unknown VM type '$d->{type}'\n";
1705 my $res = PVE
::Tools
::upid_decode
($upid);
1706 while (PVE
::ProcFSTools
::check_process_running
($res->{pid
})) {
1710 my $status = PVE
::Tools
::upid_read_status
($upid);
1711 if ($status eq 'OK') {
1712 # use default delay to reduce load
1713 my $delay = defined($d->{up
}) ?
int($d->{up
}) : $default_delay;
1715 print STDERR
"Waiting for $delay seconds (startup delay)\n" if $d->{up
};
1716 for (my $i = 0; $i < $delay; $i++) {
1721 print STDERR
"Starting $typeText $vmid failed: $status\n";
1730 return $rpcenv->fork_worker('startall', undef, $authuser, $code);
1733 my $create_stop_worker = sub {
1734 my ($nodename, $type, $vmid, $down_timeout) = @_;
1737 if ($type eq 'lxc') {
1738 return if !PVE
::LXC
::check_running
($vmid);
1739 my $timeout = defined($down_timeout) ?
int($down_timeout) : 60;
1740 print STDERR
"Stopping CT $vmid (timeout = $timeout seconds)\n";
1741 $upid = PVE
::API2
::LXC
::Status-
>vm_shutdown({node
=> $nodename, vmid
=> $vmid,
1742 timeout
=> $timeout, forceStop
=> 1 });
1743 } elsif ($type eq 'qemu') {
1744 return if !PVE
::QemuServer
::check_running
($vmid, 1);
1745 my $timeout = defined($down_timeout) ?
int($down_timeout) : 60*3;
1746 print STDERR
"Stopping VM $vmid (timeout = $timeout seconds)\n";
1747 $upid = PVE
::API2
::Qemu-
>vm_shutdown({node
=> $nodename, vmid
=> $vmid,
1748 timeout
=> $timeout, forceStop
=> 1 });
1750 die "unknown VM type '$type'\n";
1756 __PACKAGE__-
>register_method ({
1762 check
=> ['perm', '/', [ 'VM.PowerMgmt' ]],
1765 description
=> "Stop all VMs and Containers.",
1767 additionalProperties
=> 0,
1769 node
=> get_standard_option
('pve-node'),
1771 description
=> "Only consider Guests with these IDs.",
1772 type
=> 'string', format
=> 'pve-vmid-list',
1783 my $rpcenv = PVE
::RPCEnvironment
::get
();
1784 my $authuser = $rpcenv->get_user();
1786 my $nodename = $param->{node
};
1787 $nodename = PVE
::INotify
::nodename
() if $nodename eq 'localhost';
1791 $rpcenv->{type
} = 'priv'; # to start tasks in background
1793 my $stopList = &$get_start_stop_list($nodename, undef, $param->{vms
});
1795 my $cpuinfo = PVE
::ProcFSTools
::read_cpuinfo
();
1796 my $datacenterconfig = cfs_read_file
('datacenter.cfg');
1797 # if not set by user spawn max cpu count number of workers
1798 my $maxWorkers = $datacenterconfig->{max_workers
} || $cpuinfo->{cpus
};
1800 foreach my $order (sort {$b <=> $a} keys %$stopList) {
1801 my $vmlist = $stopList->{$order};
1804 my $finish_worker = sub {
1806 my $d = $workers->{$pid};
1808 delete $workers->{$pid};
1810 syslog
('info', "end task $d->{upid}");
1813 foreach my $vmid (sort {$b <=> $a} keys %$vmlist) {
1814 my $d = $vmlist->{$vmid};
1816 eval { $upid = &$create_stop_worker($nodename, $d->{type
}, $vmid, $d->{down
}); };
1820 my $res = PVE
::Tools
::upid_decode
($upid, 1);
1823 my $pid = $res->{pid
};
1825 $workers->{$pid} = { type
=> $d->{type
}, upid
=> $upid, vmid
=> $vmid };
1826 while (scalar(keys %$workers) >= $maxWorkers) {
1827 foreach my $p (keys %$workers) {
1828 if (!PVE
::ProcFSTools
::check_process_running
($p)) {
1829 &$finish_worker($p);
1835 while (scalar(keys %$workers)) {
1836 foreach my $p (keys %$workers) {
1837 if (!PVE
::ProcFSTools
::check_process_running
($p)) {
1838 &$finish_worker($p);
1845 syslog
('info', "all VMs and CTs stopped");
1850 return $rpcenv->fork_worker('stopall', undef, $authuser, $code);
1853 my $create_migrate_worker = sub {
1854 my ($nodename, $type, $vmid, $target, $with_local_disks) = @_;
1857 if ($type eq 'lxc') {
1858 my $online = PVE
::LXC
::check_running
($vmid) ?
1 : 0;
1859 print STDERR
"Migrating CT $vmid\n";
1860 $upid = PVE
::API2
::LXC-
>migrate_vm({node
=> $nodename, vmid
=> $vmid, target
=> $target,
1861 restart
=> $online });
1862 } elsif ($type eq 'qemu') {
1863 print STDERR
"Check VM $vmid: ";
1865 my $online = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
1866 my $preconditions = PVE
::API2
::Qemu-
>migrate_vm_precondition({node
=> $nodename, vmid
=> $vmid, target
=> $target});
1867 my $invalidConditions = '';
1868 if ($online && !$with_local_disks && scalar @{$preconditions->{local_disks
}}) {
1869 $invalidConditions .= "\n Has local disks: " .
1870 join(', ', map { $_->{volid
} } @{$preconditions->{local_disks
}});
1873 if (@{$preconditions->{local_resources
}}) {
1874 $invalidConditions .= "\n Has local resources: " . join(', ', @{$preconditions->{local_resources
}});
1877 if ($invalidConditions && $invalidConditions ne '') {
1878 print STDERR
"skip VM $vmid - precondition check failed:";
1879 die "$invalidConditions\n";
1881 print STDERR
"precondition check passed\n";
1882 print STDERR
"Migrating VM $vmid\n";
1890 $params->{'with-local-disks'} = $with_local_disks if defined($with_local_disks);
1892 $upid = PVE
::API2
::Qemu-
>migrate_vm($params);
1894 die "unknown VM type '$type'\n";
1897 my $res = PVE
::Tools
::upid_decode
($upid);
1902 __PACKAGE__-
>register_method ({
1903 name
=> 'migrateall',
1904 path
=> 'migrateall',
1909 check
=> ['perm', '/', [ 'VM.Migrate' ]],
1911 description
=> "Migrate all VMs and Containers.",
1913 additionalProperties
=> 0,
1915 node
=> get_standard_option
('pve-node'),
1916 target
=> get_standard_option
('pve-node', { description
=> "Target node." }),
1918 description
=> "Maximal number of parallel migration job." .
1919 " If not set use 'max_workers' from datacenter.cfg," .
1920 " one of both must be set!",
1926 description
=> "Only consider Guests with these IDs.",
1927 type
=> 'string', format
=> 'pve-vmid-list',
1930 "with-local-disks" => {
1932 description
=> "Enable live storage migration for local disk",
1943 my $rpcenv = PVE
::RPCEnvironment
::get
();
1944 my $authuser = $rpcenv->get_user();
1946 my $nodename = $param->{node
};
1947 $nodename = PVE
::INotify
::nodename
() if $nodename eq 'localhost';
1949 my $target = $param->{target
};
1950 my $with_local_disks = $param->{'with-local-disks'};
1951 raise_param_exc
({ target
=> "target is local node."}) if $target eq $nodename;
1953 PVE
::Cluster
::check_cfs_quorum
();
1955 PVE
::Cluster
::check_node_exists
($target);
1957 my $datacenterconfig = cfs_read_file
('datacenter.cfg');
1958 # prefer parameter over datacenter cfg settings
1959 my $maxWorkers = $param->{maxworkers
} || $datacenterconfig->{max_workers
} ||
1960 die "either 'maxworkers' parameter or max_workers in datacenter.cfg must be set!\n";
1963 $rpcenv->{type
} = 'priv'; # to start tasks in background
1965 my $vmlist = &$get_filtered_vmlist($nodename, $param->{vms
}, 1, 1);
1966 if (!scalar(keys %$vmlist)) {
1967 warn "no virtual guests matched, nothing to do..\n";
1972 my $workers_started = 0;
1973 foreach my $vmid (sort keys %$vmlist) {
1974 my $d = $vmlist->{$vmid};
1976 eval { $pid = &$create_migrate_worker($nodename, $d->{type
}, $vmid, $target, $with_local_disks); };
1981 $workers->{$pid} = 1;
1982 while (scalar(keys %$workers) >= $maxWorkers) {
1983 foreach my $p (keys %$workers) {
1984 if (!PVE
::ProcFSTools
::check_process_running
($p)) {
1985 delete $workers->{$p};
1991 while (scalar(keys %$workers)) {
1992 foreach my $p (keys %$workers) {
1993 # FIXME: what about PID re-use ?!?!
1994 if (!PVE
::ProcFSTools
::check_process_running
($p)) {
1995 delete $workers->{$p};
2000 if ($workers_started <= 0) {
2001 die "no migrations worker started...\n";
2003 print STDERR
"All jobs finished, used $workers_started workers in total.\n";
2007 return $rpcenv->fork_worker('migrateall', undef, $authuser, $code);
2011 __PACKAGE__-
>register_method ({
2012 name
=> 'get_etc_hosts',
2018 check
=> ['perm', '/', [ 'Sys.Audit' ]],
2020 description
=> "Get the content of /etc/hosts.",
2022 additionalProperties
=> 0,
2024 node
=> get_standard_option
('pve-node'),
2030 digest
=> get_standard_option
('pve-config-digest'),
2033 description
=> 'The content of /etc/hosts.'
2040 return PVE
::INotify
::read_file
('etchosts');
2044 __PACKAGE__-
>register_method ({
2045 name
=> 'write_etc_hosts',
2051 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
2053 description
=> "Write /etc/hosts.",
2055 additionalProperties
=> 0,
2057 node
=> get_standard_option
('pve-node'),
2058 digest
=> get_standard_option
('pve-config-digest'),
2061 description
=> 'The target content of /etc/hosts.'
2071 PVE
::Tools
::lock_file
('/var/lock/pve-etchosts.lck', undef, sub{
2072 if ($param->{digest
}) {
2073 my $hosts = PVE
::INotify
::read_file
('etchosts');
2074 PVE
::Tools
::assert_if_modified
($hosts->{digest
}, $param->{digest
});
2076 PVE
::INotify
::write_file
('etchosts', $param->{data
});
2083 # bash completion helper
2085 sub complete_templet_repo
{
2086 my ($cmdname, $pname, $cvalue) = @_;
2088 my $repo = PVE
::APLInfo
::load_data
();
2090 foreach my $templ (keys %{$repo->{all
}}) {
2091 next if $templ !~ m/^$cvalue/;
2098 package PVE
::API2
::Nodes
;
2103 use PVE
::SafeSyslog
;
2105 use PVE
::RESTHandler
;
2106 use PVE
::RPCEnvironment
;
2108 use PVE
::JSONSchema
qw(get_standard_option);
2110 use base
qw(PVE::RESTHandler);
2112 __PACKAGE__-
>register_method ({
2113 subclass
=> "PVE::API2::Nodes::Nodeinfo",
2117 __PACKAGE__-
>register_method ({
2121 permissions
=> { user
=> 'all' },
2122 description
=> "Cluster node index.",
2124 additionalProperties
=> 0,
2132 node
=> get_standard_option
('pve-node'),
2134 description
=> "Node status.",
2136 enum
=> ['unknown', 'online', 'offline'],
2139 description
=> "CPU utilization.",
2142 renderer
=> 'fraction_as_percentage',
2145 description
=> "Number of available CPUs.",
2150 description
=> "Used memory in bytes.",
2153 renderer
=> 'bytes',
2156 description
=> "Number of available memory in bytes.",
2159 renderer
=> 'bytes',
2162 description
=> "Support level.",
2167 description
=> "Node uptime in seconds.",
2170 renderer
=> 'duration',
2172 ssl_fingerprint
=> {
2173 description
=> "The SSL fingerprint for the node certificate.",
2179 links
=> [ { rel
=> 'child', href
=> "{node}" } ],
2184 my $rpcenv = PVE
::RPCEnvironment
::get
();
2185 my $authuser = $rpcenv->get_user();
2187 my $clinfo = PVE
::Cluster
::get_clinfo
();
2190 my $nodelist = PVE
::Cluster
::get_nodelist
();
2191 my $members = PVE
::Cluster
::get_members
();
2192 my $rrd = PVE
::Cluster
::rrd_dump
();
2194 foreach my $node (@$nodelist) {
2195 my $can_audit = $rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Audit' ], 1);
2196 my $entry = PVE
::API2Tools
::extract_node_stats
($node, $members, $rrd, !$can_audit);
2198 $entry->{ssl_fingerprint
} = eval { PVE
::Cluster
::get_node_fingerprint
($node) };