]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
Add delay in startall to reduce load at boot time
[qemu-server.git] / PVE / API2 / Qemu.pm
CommitLineData
1e3baf05
DM
1package PVE::API2::Qemu;
2
3use strict;
4use warnings;
5
6use PVE::Cluster;
7use PVE::SafeSyslog;
8use PVE::Tools qw(extract_param);
9use PVE::Exception qw(raise raise_param_exc);
10use PVE::Storage;
11use PVE::JSONSchema qw(get_standard_option);
12use PVE::RESTHandler;
13use PVE::QemuServer;
14use PVE::RPCEnvironment;
15use PVE::AccessControl;
16use PVE::INotify;
17
18use Data::Dumper; # fixme: remove
19
20use base qw(PVE::RESTHandler);
21
22my $opt_force_description = "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.";
23
24my $resolve_cdrom_alias = sub {
25 my $param = shift;
26
27 if (my $value = $param->{cdrom}) {
28 $value .= ",media=cdrom" if $value !~ m/media=/;
29 $param->{ide2} = $value;
30 delete $param->{cdrom};
31 }
32};
33
34__PACKAGE__->register_method({
35 name => 'vmlist',
36 path => '',
37 method => 'GET',
38 description => "Virtual machine index (per node).",
39 proxyto => 'node',
40 protected => 1, # qemu pid files are only readable by root
41 parameters => {
42 additionalProperties => 0,
43 properties => {
44 node => get_standard_option('pve-node'),
45 },
46 },
47 returns => {
48 type => 'array',
49 items => {
50 type => "object",
51 properties => {},
52 },
53 links => [ { rel => 'child', href => "{vmid}" } ],
54 },
55 code => sub {
56 my ($param) = @_;
57
58 my $vmstatus = PVE::QemuServer::vmstatus();
59
60 return PVE::RESTHandler::hash_to_array($vmstatus, 'vmid');
61
62 }});
63
64__PACKAGE__->register_method({
65 name => 'create_vm',
66 path => '',
67 method => 'POST',
68 description => "Create new virtual machine.",
69 protected => 1,
70 proxyto => 'node',
71 parameters => {
72 additionalProperties => 0,
73 properties => PVE::QemuServer::json_config_properties(
74 {
75 node => get_standard_option('pve-node'),
76 vmid => get_standard_option('pve-vmid'),
77 }),
78 },
79 returns => { type => 'null'},
80 code => sub {
81 my ($param) = @_;
82
83 my $node = extract_param($param, 'node');
84
85 # fixme: fork worker?
86
87 my $vmid = extract_param($param, 'vmid');
88
89 my $filename = PVE::QemuServer::config_file($vmid);
90 # first test (befor locking)
91 die "unable to create vm $vmid: config file already exists\n"
92 if -f $filename;
93
94 my $storecfg = PVE::Storage::config();
95
96 &$resolve_cdrom_alias($param);
97
98 foreach my $opt (keys %$param) {
99 if (PVE::QemuServer::valid_drivename($opt)) {
100 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
101 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
102
103 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
104 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
105 }
106 }
107
108 PVE::QemuServer::add_random_macs($param);
109
110 #fixme: ? syslog ('info', "VM $vmid creating new virtual machine");
111
112 my $vollist = [];
113
114 my $createfn = sub {
115
116 # second test (after locking test is accurate)
117 die "unable to create vm $vmid: config file already exists\n"
118 if -f $filename;
119
120 $vollist = PVE::QemuServer::create_disks($storecfg, $vmid, $param);
121
122 # try to be smart about bootdisk
123 my @disks = PVE::QemuServer::disknames();
124 my $firstdisk;
125 foreach my $ds (reverse @disks) {
126 next if !$param->{$ds};
127 my $disk = PVE::QemuServer::parse_drive($ds, $param->{$ds});
128 next if PVE::QemuServer::drive_is_cdrom($disk);
129 $firstdisk = $ds;
130 }
131
132 if (!$param->{bootdisk} && $firstdisk) {
133 $param->{bootdisk} = $firstdisk;
134 }
135
136 PVE::QemuServer::create_conf_nolock($vmid, $param);
137 };
138
139 eval { PVE::QemuServer::lock_config($vmid, $createfn); };
140 my $err = $@;
141
142 if ($err) {
143 foreach my $volid (@$vollist) {
144 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
145 warn $@ if $@;
146 }
147 die "create failed - $err";
148 }
149
150 return undef;
151 }});
152
153__PACKAGE__->register_method({
154 name => 'vmdiridx',
155 path => '{vmid}',
156 method => 'GET',
157 proxyto => 'node',
158 description => "Directory index",
159 parameters => {
160 additionalProperties => 0,
161 properties => {
162 node => get_standard_option('pve-node'),
163 vmid => get_standard_option('pve-vmid'),
164 },
165 },
166 returns => {
167 type => 'array',
168 items => {
169 type => "object",
170 properties => {
171 subdir => { type => 'string' },
172 },
173 },
174 links => [ { rel => 'child', href => "{subdir}" } ],
175 },
176 code => sub {
177 my ($param) = @_;
178
179 my $res = [
180 { subdir => 'config' },
181 { subdir => 'status' },
182 { subdir => 'unlink' },
183 { subdir => 'vncproxy' },
184 { subdir => 'rrd' },
185 { subdir => 'rrddata' },
186 ];
187
188 return $res;
189 }});
190
191__PACKAGE__->register_method({
192 name => 'rrd',
193 path => '{vmid}/rrd',
194 method => 'GET',
195 protected => 1, # fixme: can we avoid that?
196 permissions => {
197 path => '/vms/{vmid}',
198 privs => [ 'VM.Audit' ],
199 },
200 description => "Read VM RRD statistics (returns PNG)",
201 parameters => {
202 additionalProperties => 0,
203 properties => {
204 node => get_standard_option('pve-node'),
205 vmid => get_standard_option('pve-vmid'),
206 timeframe => {
207 description => "Specify the time frame you are interested in.",
208 type => 'string',
209 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
210 },
211 ds => {
212 description => "The list of datasources you want to display.",
213 type => 'string', format => 'pve-configid-list',
214 },
215 cf => {
216 description => "The RRD consolidation function",
217 type => 'string',
218 enum => [ 'AVERAGE', 'MAX' ],
219 optional => 1,
220 },
221 },
222 },
223 returns => {
224 type => "object",
225 properties => {
226 filename => { type => 'string' },
227 },
228 },
229 code => sub {
230 my ($param) = @_;
231
232 return PVE::Cluster::create_rrd_graph(
233 "pve2-vm/$param->{vmid}", $param->{timeframe},
234 $param->{ds}, $param->{cf});
235
236 }});
237
238__PACKAGE__->register_method({
239 name => 'rrddata',
240 path => '{vmid}/rrddata',
241 method => 'GET',
242 protected => 1, # fixme: can we avoid that?
243 permissions => {
244 path => '/vms/{vmid}',
245 privs => [ 'VM.Audit' ],
246 },
247 description => "Read VM RRD statistics",
248 parameters => {
249 additionalProperties => 0,
250 properties => {
251 node => get_standard_option('pve-node'),
252 vmid => get_standard_option('pve-vmid'),
253 timeframe => {
254 description => "Specify the time frame you are interested in.",
255 type => 'string',
256 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
257 },
258 cf => {
259 description => "The RRD consolidation function",
260 type => 'string',
261 enum => [ 'AVERAGE', 'MAX' ],
262 optional => 1,
263 },
264 },
265 },
266 returns => {
267 type => "array",
268 items => {
269 type => "object",
270 properties => {},
271 },
272 },
273 code => sub {
274 my ($param) = @_;
275
276 return PVE::Cluster::create_rrd_data(
277 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
278 }});
279
280
281__PACKAGE__->register_method({
282 name => 'vm_config',
283 path => '{vmid}/config',
284 method => 'GET',
285 proxyto => 'node',
286 description => "Get virtual machine configuration.",
287 parameters => {
288 additionalProperties => 0,
289 properties => {
290 node => get_standard_option('pve-node'),
291 vmid => get_standard_option('pve-vmid'),
292 },
293 },
294 returns => {
295 type => "object",
296 properties => {},
297 },
298 code => sub {
299 my ($param) = @_;
300
301 my $conf = PVE::QemuServer::load_config($param->{vmid});
302
303 return $conf;
304 }});
305
306__PACKAGE__->register_method({
307 name => 'update_vm',
308 path => '{vmid}/config',
309 method => 'PUT',
310 protected => 1,
311 proxyto => 'node',
312 description => "Set virtual machine options.",
313 parameters => {
314 additionalProperties => 0,
315 properties => PVE::QemuServer::json_config_properties(
316 {
317 node => get_standard_option('pve-node'),
318 vmid => get_standard_option('pve-vmid'),
319 skiplock => {
320 description => "Ignore locks - only root is allowed to use this option.",
321 type => 'boolean',
322 optional => 1,
323 },
324 delete => {
325 type => 'string', format => 'pve-configid-list',
326 description => "A list of settings you want to delete.",
327 optional => 1,
328 },
329 force => {
330 type => 'boolean',
331 description => $opt_force_description,
332 optional => 1,
333 requires => 'delete',
334 },
335 }),
336 },
337 returns => { type => 'null'},
338 code => sub {
339 my ($param) = @_;
340
341 my $rpcenv = PVE::RPCEnvironment::get();
342
343 my $user = $rpcenv->get_user();
344
345 my $node = extract_param($param, 'node');
346
347 # fixme: fork worker?
348
349 my $vmid = extract_param($param, 'vmid');
350
351 my $skiplock = extract_param($param, 'skiplock');
352 raise_param_exc({ skiplock => "Only root may use this option." }) if $user ne 'root@pam';
353
354 my $delete = extract_param($param, 'delete');
355 my $force = extract_param($param, 'force');
356
357 die "no options specified\n" if !$delete && !scalar(keys %$param);
358
359 my $storecfg = PVE::Storage::config();
360
361 &$resolve_cdrom_alias($param);
362
363 my $eject = {};
364 my $cdchange = {};
365
366 foreach my $opt (keys %$param) {
367 if (PVE::QemuServer::valid_drivename($opt)) {
368 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
369 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
370 if ($drive->{file} eq 'eject') {
371 $eject->{$opt} = 1;
372 delete $param->{$opt};
373 next;
374 }
375
376 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
377 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
378
379 if (PVE::QemuServer::drive_is_cdrom($drive)) {
380 $cdchange->{$opt} = PVE::QemuServer::get_iso_path($storecfg, $vmid, $drive->{file});
381 }
382 }
383 }
384
385 foreach my $opt (PVE::Tools::split_list($delete)) {
386 $opt = 'ide2' if $opt eq 'cdrom';
387 die "you can't use '-$opt' and '-delete $opt' at the same time\n"
388 if defined($param->{$opt});
389 }
390
391 PVE::QemuServer::add_random_macs($param);
392
393 my $vollist = [];
394
395 my $updatefn = sub {
396
397 my $conf = PVE::QemuServer::load_config($vmid);
398
399 PVE::QemuServer::check_lock($conf) if !$skiplock;
400
401 foreach my $opt (keys %$eject) {
402 if ($conf->{$opt}) {
403 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
404 $cdchange->{$opt} = undef if PVE::QemuServer::drive_is_cdrom($drive);
405 } else {
406 raise_param_exc({ $opt => "eject failed - drive does not exist." });
407 }
408 }
409
410 foreach my $opt (keys %$param) {
411 next if !PVE::QemuServer::valid_drivename($opt);
412 next if !$conf->{$opt};
413 my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
414 next if PVE::QemuServer::drive_is_cdrom($old_drive);
415 my $new_drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
416 if ($new_drive->{file} ne $old_drive->{file}) {
417 my ($path, $owner);
418 eval { ($path, $owner) = PVE::Storage::path($storecfg, $old_drive->{file}); };
419 if ($owner && ($owner == $vmid)) {
420 PVE::QemuServer::add_unused_volume($conf, $param, $old_drive->{file});
421 }
422 }
423 }
424
425 my $unset = {};
426
427 foreach my $opt (PVE::Tools::split_list($delete)) {
428 $opt = 'ide2' if $opt eq 'cdrom';
429 if (!PVE::QemuServer::option_exists($opt)) {
430 raise_param_exc({ delete => "unknown option '$opt'" });
431 }
432 next if !defined($conf->{$opt});
433 if (PVE::QemuServer::valid_drivename($opt)) {
434 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
435 if (PVE::QemuServer::drive_is_cdrom($drive)) {
436 $cdchange->{$opt} = undef;
437 } else {
438 my $volid = $drive->{file};
439
440 if ($volid !~ m|^/|) {
441 my ($path, $owner);
442 eval { ($path, $owner) = PVE::Storage::path($storecfg, $volid); };
443 if ($owner && ($owner == $vmid)) {
444 if ($force) {
445 push @$vollist, $volid;
446 } else {
447 PVE::QemuServer::add_unused_volume($conf, $param, $volid);
448 }
449 }
450 }
451 }
452 } elsif ($opt =~ m/^unused/) {
453 push @$vollist, $conf->{$opt};
454 }
455
456 $unset->{$opt} = 1;
457 }
458
459 PVE::QemuServer::create_disks($storecfg, $vmid, $param);
460
461 PVE::QemuServer::change_config_nolock($vmid, $param, $unset, 1);
462
463 return if !PVE::QemuServer::check_running($vmid);
464
465 foreach my $opt (keys %$cdchange) {
466 my $qdn = PVE::QemuServer::qemu_drive_name($opt, 'cdrom');
467 my $path = $cdchange->{$opt};
468 PVE::QemuServer::vm_monitor_command($vmid, "eject $qdn", 0);
469 PVE::QemuServer::vm_monitor_command($vmid, "change $qdn \"$path\"", 0) if $path;
470 }
471 };
472
473 PVE::QemuServer::lock_config($vmid, $updatefn);
474
475 foreach my $volid (@$vollist) {
476 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
477 # fixme: log ?
478 warn $@ if $@;
479 }
480
481 return undef;
482 }});
483
484
485__PACKAGE__->register_method({
486 name => 'destroy_vm',
487 path => '{vmid}',
488 method => 'DELETE',
489 protected => 1,
490 proxyto => 'node',
491 description => "Destroy the vm (also delete all used/owned volumes).",
492 parameters => {
493 additionalProperties => 0,
494 properties => {
495 node => get_standard_option('pve-node'),
496 vmid => get_standard_option('pve-vmid'),
497 },
498 },
499 returns => { type => 'null' },
500 code => sub {
501 my ($param) = @_;
502
503 my $rpcenv = PVE::RPCEnvironment::get();
504
505 my $user = $rpcenv->get_user();
506
507 my $vmid = $param->{vmid};
508
509 my $skiplock = $param->{skiplock};
510 raise_param_exc({ skiplock => "Only root may use this option." })
511 if $user ne 'root@pam';
512
513 my $storecfg = PVE::Storage::config();
514
515 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
516
517 return undef;
518 }});
519
520__PACKAGE__->register_method({
521 name => 'unlink',
522 path => '{vmid}/unlink',
523 method => 'PUT',
524 protected => 1,
525 proxyto => 'node',
526 description => "Unlink/delete disk images.",
527 parameters => {
528 additionalProperties => 0,
529 properties => {
530 node => get_standard_option('pve-node'),
531 vmid => get_standard_option('pve-vmid'),
532 idlist => {
533 type => 'string', format => 'pve-configid-list',
534 description => "A list of disk IDs you want to delete.",
535 },
536 force => {
537 type => 'boolean',
538 description => $opt_force_description,
539 optional => 1,
540 },
541 },
542 },
543 returns => { type => 'null'},
544 code => sub {
545 my ($param) = @_;
546
547 $param->{delete} = extract_param($param, 'idlist');
548
549 __PACKAGE__->update_vm($param);
550
551 return undef;
552 }});
553
554my $sslcert;
555
556__PACKAGE__->register_method({
557 name => 'vncproxy',
558 path => '{vmid}/vncproxy',
559 method => 'POST',
560 protected => 1,
561 permissions => {
562 path => '/vms/{vmid}',
563 privs => [ 'VM.Console' ],
564 },
565 description => "Creates a TCP VNC proxy connections.",
566 parameters => {
567 additionalProperties => 0,
568 properties => {
569 node => get_standard_option('pve-node'),
570 vmid => get_standard_option('pve-vmid'),
571 },
572 },
573 returns => {
574 additionalProperties => 0,
575 properties => {
576 user => { type => 'string' },
577 ticket => { type => 'string' },
578 cert => { type => 'string' },
579 port => { type => 'integer' },
580 upid => { type => 'string' },
581 },
582 },
583 code => sub {
584 my ($param) = @_;
585
586 my $rpcenv = PVE::RPCEnvironment::get();
587
588 my $user = $rpcenv->get_user();
589 my $ticket = PVE::AccessControl::assemble_ticket($user);
590
591 my $vmid = $param->{vmid};
592 my $node = $param->{node};
593
594 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
595 if !$sslcert;
596
597 my $port = PVE::Tools::next_vnc_port();
598
599 my $remip;
600
601 if ($node ne PVE::INotify::nodename()) {
602 $remip = PVE::Cluster::remote_node_ip($node);
603 }
604
605 # NOTE: kvm VNC traffic is already TLS encrypted,
606 # so we select the fastest chipher here (or 'none'?)
607 my $remcmd = $remip ? ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes',
608 '-c', 'blowfish-cbc', $remip] : [];
609
610 my $timeout = 10;
611
612 my $realcmd = sub {
613 my $upid = shift;
614
615 syslog('info', "starting vnc proxy $upid\n");
616
617 my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
618
619 my $qmstr = join(' ', @$qmcmd);
620
621 # also redirect stderr (else we get RFB protocol errors)
622 my @cmd = ('/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null");
623
624 my $cmdstr = join(' ', @cmd);
625 syslog('info', "CMD3: $cmdstr");
626
627 if (system(@cmd) != 0) {
628 my $msg = "VM $vmid vnc proxy failed - $?";
629 syslog('err', $msg);
630 return;
631 }
632
633 return;
634 };
635
636 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $user, $realcmd);
637
638 return {
639 user => $user,
640 ticket => $ticket,
641 port => $port,
642 upid => $upid,
643 cert => $sslcert,
644 };
645 }});
646
647__PACKAGE__->register_method({
648 name => 'vm_status',
649 path => '{vmid}/status',
650 method => 'GET',
651 proxyto => 'node',
652 protected => 1, # qemu pid files are only readable by root
653 description => "Get virtual machine status.",
654 parameters => {
655 additionalProperties => 0,
656 properties => {
657 node => get_standard_option('pve-node'),
658 vmid => get_standard_option('pve-vmid'),
659 },
660 },
661 returns => { type => 'object' },
662 code => sub {
663 my ($param) = @_;
664
665 # test if VM exists
666 my $conf = PVE::QemuServer::load_config($param->{vmid});
667
668 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid});
669
670 return $vmstatus->{$param->{vmid}};
671 }});
672
673__PACKAGE__->register_method({
674 name => 'vm_command',
675 path => '{vmid}/status',
676 method => 'PUT',
677 protected => 1,
678 proxyto => 'node',
679 description => "Set virtual machine status.",
680 parameters => {
681 additionalProperties => 0,
682 properties => {
683 node => get_standard_option('pve-node'),
684 vmid => get_standard_option('pve-vmid'),
685 skiplock => {
686 description => "Ignore locks - only root is allowed to use this option.",
687 type => 'boolean',
688 optional => 1,
689 },
690 command => {
691 type => 'string',
692 enum => [qw(start stop reset shutdown cad suspend resume) ],
693 },
694 },
695 },
696 returns => { type => 'null'},
697 code => sub {
698 my ($param) = @_;
699
700 my $rpcenv = PVE::RPCEnvironment::get();
701
702 my $user = $rpcenv->get_user();
703
704 my $node = extract_param($param, 'node');
705
706 # fixme: proxy to correct node
707 # fixme: fork worker?
708
709 my $vmid = extract_param($param, 'vmid');
710
711 my $skiplock = extract_param($param, 'skiplock');
712 raise_param_exc({ skiplock => "Only root may use this option." })
713 if $user ne 'root@pam';
714
715 my $command = $param->{command};
716
717 my $storecfg = PVE::Storage::config();
718
719 if ($command eq 'start') {
720 my $statefile = undef; # fixme: --incoming parameter
721 PVE::QemuServer::vm_start($storecfg, $vmid, $statefile, $skiplock);
722 } elsif ($command eq 'stop') {
723 PVE::QemuServer::vm_stop($vmid, $skiplock);
724 } elsif ($command eq 'reset') {
725 PVE::QemuServer::vm_reset($vmid, $skiplock);
726 } elsif ($command eq 'shutdown') {
727 PVE::QemuServer::vm_shutdown($vmid, $skiplock);
728 } elsif ($command eq 'suspend') {
729 PVE::QemuServer::vm_suspend($vmid, $skiplock);
730 } elsif ($command eq 'resume') {
731 PVE::QemuServer::vm_resume($vmid, $skiplock);
732 } elsif ($command eq 'cad') {
733 PVE::QemuServer::vm_cad($vmid, $skiplock);
734 } else {
735 raise_param_exc({ command => "unknown command '$command'" })
736 }
737
738 return undef;
739 }});
740
741
7421;