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