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