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