]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Cluster.pm
ab5b28a191c008fc8187018395b8ae50071fbddb
[pve-manager.git] / PVE / API2 / Cluster.pm
1 package PVE::API2::Cluster;
2
3 use strict;
4 use warnings;
5
6 use JSON;
7
8 use PVE::API2Tools;
9 use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file);
10 use PVE::DataCenterConfig;
11 use PVE::Exception qw(raise_param_exc);
12 use PVE::Firewall;
13 use PVE::HA::Config;
14 use PVE::HA::Env::PVE2;
15 use PVE::INotify;
16 use PVE::JSONSchema qw(get_standard_option);
17 use PVE::RESTHandler;
18 use PVE::RPCEnvironment;
19 use PVE::SafeSyslog;
20 use PVE::Storage;
21 use PVE::Tools qw(extract_param);
22
23 use PVE::API2::ACMEAccount;
24 use PVE::API2::ACMEPlugin;
25 use PVE::API2::Backup;
26 use PVE::API2::BackupInfo;
27 use PVE::API2::Cluster::Ceph;
28 use PVE::API2::Cluster::MetricServer;
29 use PVE::API2::ClusterConfig;
30 use PVE::API2::Firewall::Cluster;
31 use PVE::API2::HAConfig;
32 use PVE::API2::ReplicationConfig;
33
34 my $have_sdn;
35 eval {
36 require PVE::API2::Network::SDN;
37 $have_sdn = 1;
38 };
39
40 use base qw(PVE::RESTHandler);
41
42 __PACKAGE__->register_method ({
43 subclass => "PVE::API2::ReplicationConfig",
44 path => 'replication',
45 });
46
47 __PACKAGE__->register_method ({
48 subclass => "PVE::API2::Cluster::MetricServer",
49 path => 'metrics',
50 });
51
52 __PACKAGE__->register_method ({
53 subclass => "PVE::API2::ClusterConfig",
54 path => 'config',
55 });
56
57 __PACKAGE__->register_method ({
58 subclass => "PVE::API2::Firewall::Cluster",
59 path => 'firewall',
60 });
61
62 __PACKAGE__->register_method ({
63 subclass => "PVE::API2::Backup",
64 path => 'backup',
65 });
66
67 __PACKAGE__->register_method ({
68 subclass => "PVE::API2::BackupInfo",
69 path => 'backupinfo',
70 });
71
72 __PACKAGE__->register_method ({
73 subclass => "PVE::API2::HAConfig",
74 path => 'ha',
75 });
76
77 __PACKAGE__->register_method ({
78 subclass => "PVE::API2::ACMEAccount",
79 path => 'acme',
80 });
81
82 __PACKAGE__->register_method ({
83 subclass => "PVE::API2::Cluster::Ceph",
84 path => 'ceph',
85 });
86
87 if ($have_sdn) {
88 __PACKAGE__->register_method ({
89 subclass => "PVE::API2::Network::SDN",
90 path => 'sdn',
91 });
92 }
93
94 my $dc_schema = PVE::DataCenterConfig::get_datacenter_schema();
95 my $dc_properties = {
96 delete => {
97 type => 'string', format => 'pve-configid-list',
98 description => "A list of settings you want to delete.",
99 optional => 1,
100 }
101 };
102 foreach my $opt (keys %{$dc_schema->{properties}}) {
103 $dc_properties->{$opt} = $dc_schema->{properties}->{$opt};
104 }
105
106 __PACKAGE__->register_method ({
107 name => 'index',
108 path => '',
109 method => 'GET',
110 description => "Cluster index.",
111 permissions => { user => 'all' },
112 parameters => {
113 additionalProperties => 0,
114 properties => {},
115 },
116 returns => {
117 type => 'array',
118 items => {
119 type => "object",
120 properties => {},
121 },
122 links => [ { rel => 'child', href => "{name}" } ],
123 },
124 code => sub {
125 my ($param) = @_;
126
127 my $result = [
128 { name => 'log' },
129 { name => 'options' },
130 { name => 'resources' },
131 { name => 'replication' },
132 { name => 'tasks' },
133 { name => 'backup' },
134 { name => 'ha' },
135 { name => 'status' },
136 { name => 'nextid' },
137 { name => 'firewall' },
138 { name => 'config' },
139 { name => 'acme' },
140 { name => 'ceph' },
141 { name => 'metrics' },
142 ];
143
144 if ($have_sdn) {
145 push(@{$result}, { name => 'sdn' });
146 }
147
148 return $result;
149 }});
150
151 __PACKAGE__->register_method({
152 name => 'log',
153 path => 'log',
154 method => 'GET',
155 description => "Read cluster log",
156 permissions => { user => 'all' },
157 parameters => {
158 additionalProperties => 0,
159 properties => {
160 max => {
161 type => 'integer',
162 description => "Maximum number of entries.",
163 optional => 1,
164 minimum => 1,
165 }
166 },
167 },
168 returns => {
169 type => 'array',
170 items => {
171 type => "object",
172 properties => {},
173 },
174 },
175 code => sub {
176 my ($param) = @_;
177
178 my $rpcenv = PVE::RPCEnvironment::get();
179
180 my $max = $param->{max} || 0;
181 my $user = $rpcenv->get_user();
182
183 my $admin = $rpcenv->check($user, "/", [ 'Sys.Syslog' ], 1);
184
185 my $loguser = $admin ? '' : $user;
186
187 my $res = decode_json(PVE::Cluster::get_cluster_log($loguser, $max));
188
189 foreach my $entry (@{$res->{data}}) {
190 $entry->{id} = "$entry->{uid}:$entry->{node}";
191 }
192
193 return $res->{data};
194 }});
195
196 __PACKAGE__->register_method({
197 name => 'resources',
198 path => 'resources',
199 method => 'GET',
200 description => "Resources index (cluster wide).",
201 permissions => { user => 'all' },
202 parameters => {
203 additionalProperties => 0,
204 properties => {
205 type => {
206 type => 'string',
207 optional => 1,
208 enum => ['vm', 'storage', 'node', 'sdn'],
209 },
210 },
211 },
212 returns => {
213 type => 'array',
214 items => {
215 type => "object",
216 properties => {
217 id => { type => 'string' },
218 type => {
219 description => "Resource type.",
220 type => 'string',
221 enum => ['node', 'storage', 'pool', 'qemu', 'lxc', 'openvz', 'sdn'],
222 },
223 status => {
224 description => "Resource type dependent status.",
225 type => 'string',
226 optional => 1,
227 },
228 node => get_standard_option('pve-node', {
229 description => "The cluster node name (when type in node,storage,qemu,lxc).",
230 optional => 1,
231 }),
232 storage => get_standard_option('pve-storage-id', {
233 description => "The storage identifier (when type == storage).",
234 optional => 1,
235 }),
236 pool => {
237 description => "The pool name (when type in pool,qemu,lxc).",
238 type => 'string',
239 optional => 1,
240 },
241 cpu => {
242 description => "CPU utilization (when type in node,qemu,lxc).",
243 type => 'number',
244 optional => 1,
245 renderer => 'fraction_as_percentage',
246 },
247 maxcpu => {
248 description => "Number of available CPUs (when type in node,qemu,lxc).",
249 type => 'number',
250 optional => 1,
251 },
252 mem => {
253 description => "Used memory in bytes (when type in node,qemu,lxc).",
254 type => 'string',
255 optional => 1,
256 renderer => 'bytes',
257 },
258 maxmem => {
259 description => "Number of available memory in bytes (when type in node,qemu,lxc).",
260 type => 'integer',
261 optional => 1,
262 renderer => 'bytes',
263 },
264 level => {
265 description => "Support level (when type == node).",
266 type => 'string',
267 optional => 1,
268 },
269 uptime => {
270 description => "Node uptime in seconds (when type in node,qemu,lxc).",
271 type => 'integer',
272 optional => 1,
273 renderer => 'duration',
274 },
275 hastate => {
276 description => "HA service status (for HA managed VMs).",
277 type => 'string',
278 optional => 1,
279 },
280 disk => {
281 description => "Used disk space in bytes (when type in storage), used root image spave for VMs (type in qemu,lxc).",
282 type => 'string',
283 optional => 1,
284 renderer => 'bytes',
285 },
286 maxdisk => {
287 description => "Storage size in bytes (when type in storage), root image size for VMs (type in qemu,lxc).",
288 type => 'integer',
289 optional => 1,
290 renderer => 'bytes',
291 },
292 content => {
293 description => "Allowed storage content types (when type == storage).",
294 type => 'string',
295 format => 'pve-storage-content-list',
296 optional => 1,
297 },
298 plugintype => {
299 description => "More specific type, if available.",
300 type => 'string',
301 optional => 1,
302 },
303 },
304 },
305 },
306 code => sub {
307 my ($param) = @_;
308
309 my $rpcenv = PVE::RPCEnvironment::get();
310 my $authuser = $rpcenv->get_user();
311 my $usercfg = $rpcenv->{user_cfg};
312
313 my $res = [];
314
315 my $nodelist = PVE::Cluster::get_nodelist();
316 my $members = PVE::Cluster::get_members();
317
318 my $rrd = PVE::Cluster::rrd_dump();
319
320 my $vmlist = PVE::Cluster::get_vmlist() || {};
321 my $idlist = $vmlist->{ids} || {};
322
323 my $hastatus = PVE::HA::Config::read_manager_status();
324 my $haresources = PVE::HA::Config::read_resources_config();
325 my $hatypemap = {
326 'qemu' => 'vm',
327 'lxc' => 'ct'
328 };
329
330 my $pooldata = {};
331 if (!$param->{type} || $param->{type} eq 'pool') {
332 for my $pool (sort keys %{$usercfg->{pools}}) {
333 my $d = $usercfg->{pools}->{$pool};
334
335 next if !$rpcenv->check($authuser, "/pool/$pool", [ 'Pool.Allocate' ], 1);
336
337 my $entry = {
338 id => "/pool/$pool",
339 pool => $pool,
340 type => 'pool',
341 };
342
343 $pooldata->{$pool} = $entry;
344
345 push @$res, $entry;
346 }
347 }
348
349 # we try to generate 'numbers' by using "$X + 0"
350 if (!$param->{type} || $param->{type} eq 'vm') {
351 my $locked_vms = PVE::Cluster::get_guest_config_property('lock');
352
353 for my $vmid (sort keys %$idlist) {
354
355 my $data = $idlist->{$vmid};
356 my $entry = PVE::API2Tools::extract_vm_stats($vmid, $data, $rrd);
357
358 if (my $pool = $usercfg->{vms}->{$vmid}) {
359 $entry->{pool} = $pool;
360 if (my $pe = $pooldata->{$pool}) {
361 if ($entry->{uptime}) {
362 $pe->{uptime} = $entry->{uptime} if !$pe->{uptime} || $entry->{uptime} > $pe->{uptime};
363 $pe->{mem} = 0 if !$pe->{mem};
364 $pe->{mem} += $entry->{mem};
365 $pe->{maxmem} = 0 if !$pe->{maxmem};
366 $pe->{maxmem} += $entry->{maxmem};
367 $pe->{cpu} = 0 if !$pe->{cpu};
368 $pe->{maxcpu} = 0 if !$pe->{maxcpu};
369 # explanation:
370 # we do not know how much cpus there are in the cluster at this moment
371 # so we calculate the current % of the cpu
372 # but we had already the old cpu % before this vm, so:
373 # new% = (old%*oldmax + cur%*curmax) / (oldmax+curmax)
374 $pe->{cpu} = (($pe->{cpu} * $pe->{maxcpu}) + ($entry->{cpu} * $entry->{maxcpu})) / ($pe->{maxcpu} + $entry->{maxcpu});
375 $pe->{maxcpu} += $entry->{maxcpu};
376 }
377 }
378 }
379
380 # only skip now to next to ensure that the pool stats above are filled, if eligible
381 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
382
383 if (defined(my $lock = $locked_vms->{$vmid}->{lock})) {
384 $entry->{lock} = $lock;
385 }
386
387 # get ha status
388 if (my $hatype = $hatypemap->{$entry->{type}}) {
389 my $sid = "$hatype:$vmid";
390 my $service;
391 if ($service = $hastatus->{service_status}->{$sid}) {
392 $entry->{hastate} = $service->{state};
393 } elsif ($service = $haresources->{ids}->{$sid}) {
394 $entry->{hastate} = $service->{state};
395 }
396 }
397
398 push @$res, $entry;
399 }
400 }
401
402 if (!$param->{type} || $param->{type} eq 'node') {
403 foreach my $node (@$nodelist) {
404 my $can_audit = $rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Audit' ], 1);
405 my $entry = PVE::API2Tools::extract_node_stats($node, $members, $rrd, !$can_audit);
406 push @$res, $entry;
407 }
408 }
409
410 if (!$param->{type} || $param->{type} eq 'storage') {
411
412 my $cfg = PVE::Storage::config();
413 my @sids = PVE::Storage::storage_ids ($cfg);
414
415 foreach my $storeid (@sids) {
416 next if !$rpcenv->check($authuser, "/storage/$storeid", [ 'Datastore.Audit' ], 1);
417
418 my $scfg = PVE::Storage::storage_config($cfg, $storeid);
419 # we create a entry for each node
420 foreach my $node (@$nodelist) {
421 next if !PVE::Storage::storage_check_enabled($cfg, $storeid, $node, 1);
422
423 my $entry = PVE::API2Tools::extract_storage_stats($storeid, $scfg, $node, $rrd);
424 push @$res, $entry;
425 }
426 }
427 }
428
429 if ($have_sdn) {
430 if (!$param->{type} || $param->{type} eq 'sdn') {
431
432 my $nodes = PVE::Cluster::get_node_kv("sdn");
433
434 for my $node (sort keys %{$nodes}) {
435 my $sdns = decode_json($nodes->{$node});
436
437 for my $id (sort keys %{$sdns}) {
438 next if !$rpcenv->check($authuser, "/sdn/zones/$id", [ 'SDN.Audit' ], 1);
439 my $sdn = $sdns->{$id};
440 my $entry = {
441 id => "sdn/$node/$id",
442 sdn => $id,
443 node => $node,
444 type => 'sdn',
445 status => $sdn->{'status'},
446 };
447 push @$res, $entry;
448 }
449 }
450 }
451 }
452
453 return $res;
454 }});
455
456 __PACKAGE__->register_method({
457 name => 'tasks',
458 path => 'tasks',
459 method => 'GET',
460 description => "List recent tasks (cluster wide).",
461 permissions => { user => 'all' },
462 parameters => {
463 additionalProperties => 0,
464 properties => {},
465 },
466 returns => {
467 type => 'array',
468 items => {
469 type => "object",
470 properties => {
471 upid => { type => 'string' },
472 },
473 },
474 },
475 code => sub {
476 my ($param) = @_;
477
478 my $rpcenv = PVE::RPCEnvironment::get();
479 my $authuser = $rpcenv->get_user();
480
481 my $tlist = PVE::Cluster::get_tasklist();
482
483 my $res = [];
484
485 return $res if !$tlist;
486
487 my $all = $rpcenv->check($authuser, "/", [ 'Sys.Audit' ], 1);
488
489 foreach my $task (@$tlist) {
490 if (PVE::AccessControl::pve_verify_tokenid($task->{user}, 1)) {
491 ($task->{user}, $task->{tokenid}) = PVE::AccessControl::split_tokenid($task->{user});
492 }
493 push @$res, $task if $all || ($task->{user} eq $authuser);
494 }
495
496 return $res;
497 }});
498
499 __PACKAGE__->register_method({
500 name => 'get_options',
501 path => 'options',
502 method => 'GET',
503 description => "Get datacenter options.",
504 permissions => {
505 check => ['perm', '/', [ 'Sys.Audit' ]],
506 },
507 parameters => {
508 additionalProperties => 0,
509 properties => {},
510 },
511 returns => {
512 type => "object",
513 properties => {},
514 },
515 code => sub {
516 my ($param) = @_;
517
518 return PVE::Cluster::cfs_read_file('datacenter.cfg');
519 }});
520
521 __PACKAGE__->register_method({
522 name => 'set_options',
523 path => 'options',
524 method => 'PUT',
525 description => "Set datacenter options.",
526 permissions => {
527 check => ['perm', '/', [ 'Sys.Modify' ]],
528 },
529 protected => 1,
530 parameters => {
531 additionalProperties => 0,
532 properties => $dc_properties,
533 },
534 returns => { type => "null" },
535 code => sub {
536 my ($param) = @_;
537
538 my $filename = 'datacenter.cfg';
539
540 my $delete = extract_param($param, 'delete');
541
542 my $code = sub {
543
544 my $conf = cfs_read_file($filename);
545
546 foreach my $opt (keys %$param) {
547 $conf->{$opt} = $param->{$opt};
548 }
549
550 foreach my $opt (PVE::Tools::split_list($delete)) {
551 delete $conf->{$opt};
552 };
553
554 cfs_write_file($filename, $conf);
555 };
556
557 cfs_lock_file($filename, undef, $code);
558 die $@ if $@;
559
560 return undef;
561 }});
562
563 __PACKAGE__->register_method({
564 name => 'get_status',
565 path => 'status',
566 method => 'GET',
567 description => "Get cluster status information.",
568 permissions => {
569 check => ['perm', '/', [ 'Sys.Audit' ]],
570 },
571 protected => 1,
572 parameters => {
573 additionalProperties => 0,
574 properties => {},
575 },
576 returns => {
577 type => 'array',
578 items => {
579 type => "object",
580 properties => {
581 type => {
582 type => 'string',
583 enum => ['cluster', 'node'],
584 description => 'Indicates the type, either cluster or node. The type defines the object properties e.g. quorate available for type cluster.'
585 },
586 id => {
587 type => 'string',
588 },
589 name => {
590 type => 'string',
591 },
592 nodes => {
593 type => 'integer',
594 optional => 1,
595 description => '[cluster] Nodes count, including offline nodes.',
596 },
597 version => {
598 type => 'integer',
599 optional => 1,
600 description => '[cluster] Current version of the corosync configuration file.',
601 },
602 quorate => {
603 type => 'boolean',
604 optional => 1,
605 description => '[cluster] Indicates if there is a majority of nodes online to make decisions',
606 },
607 nodeid => {
608 type => 'integer',
609 optional => 1,
610 description => '[node] ID of the node from the corosync configuration.',
611 },
612 ip => {
613 type => 'string',
614 optional => 1,
615 description => '[node] IP of the resolved nodename.',
616 },
617 'local' => {
618 type => 'boolean',
619 optional => 1,
620 description => '[node] Indicates if this is the responding node.',
621 },
622 online => {
623 type => 'boolean',
624 optional => 1,
625 description => '[node] Indicates if the node is online or offline.',
626 },
627 level => {
628 type => 'string',
629 optional => 1,
630 description => '[node] Proxmox VE Subscription level, indicates if eligible for enterprise support as well as access to the stable Proxmox VE Enterprise Repository.',
631 }
632 },
633 },
634 },
635 code => sub {
636 my ($param) = @_;
637
638 # make sure we get current info
639 PVE::Cluster::cfs_update();
640
641 # we also add info from pmxcfs
642 my $clinfo = PVE::Cluster::get_clinfo();
643 my $members = PVE::Cluster::get_members();
644 my $nodename = PVE::INotify::nodename();
645 my $rrd = PVE::Cluster::rrd_dump();
646
647 if ($members) {
648 my $res = [];
649
650 if (my $d = $clinfo->{cluster}) {
651 push @$res, {
652 type => 'cluster',
653 id => 'cluster',
654 nodes => $d->{nodes},
655 version => $d->{version},
656 name => $d->{name},
657 quorate => $d->{quorate},
658 };
659 }
660
661 foreach my $node (keys %$members) {
662 my $d = $members->{$node};
663 my $entry = {
664 type => 'node',
665 id => "node/$node",
666 name => $node,
667 nodeid => $d->{id},
668 'local' => ($node eq $nodename) ? 1 : 0,
669 online => $d->{online},
670 };
671
672 if (defined($d->{ip})) {
673 $entry->{ip} = $d->{ip};
674 }
675
676 if (my $d = PVE::API2Tools::extract_node_stats($node, $members, $rrd)) {
677 $entry->{level} = $d->{level} || '';
678 }
679
680 push @$res, $entry;
681 }
682 return $res;
683 } else {
684 # fake entry for local node if no cluster defined
685 my $pmxcfs = ($clinfo && $clinfo->{version}) ? 1 : 0; # pmxcfs online ?
686
687 my $subinfo = PVE::INotify::read_file('subscription');
688 my $sublevel = $subinfo->{level} || '';
689
690 return [{
691 type => 'node',
692 id => "node/$nodename",
693 name => $nodename,
694 ip => scalar(PVE::Cluster::remote_node_ip($nodename)),
695 'local' => 1,
696 nodeid => 0,
697 online => 1,
698 level => $sublevel,
699 }];
700 }
701 }});
702
703 __PACKAGE__->register_method({
704 name => 'nextid',
705 path => 'nextid',
706 method => 'GET',
707 description => "Get next free VMID. If you pass an VMID it will raise an error if the ID is already used.",
708 permissions => { user => 'all' },
709 parameters => {
710 additionalProperties => 0,
711 properties => {
712 vmid => get_standard_option('pve-vmid', {optional => 1}),
713 },
714 },
715 returns => {
716 type => 'integer',
717 description => "The next free VMID.",
718 },
719 code => sub {
720 my ($param) = @_;
721
722 my $vmlist = PVE::Cluster::get_vmlist() || {};
723 my $idlist = $vmlist->{ids} || {};
724
725 if (my $vmid = $param->{vmid}) {
726 return $vmid if !defined($idlist->{$vmid});
727 raise_param_exc({ vmid => "VM $vmid already exists" });
728 }
729
730 for (my $i = 100; $i < 10000; $i++) {
731 return $i if !defined($idlist->{$i});
732 }
733
734 die "unable to get any free VMID\n";
735 }});
736
737 1;