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