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