]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
use new PVE::ProcFSTools::read_proc_pid_stat()
[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",
554ac7e7
DM
296 properties => {
297 digest => {
298 type => 'string',
299 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
300 }
301 },
1e3baf05
DM
302 },
303 code => sub {
304 my ($param) = @_;
305
306 my $conf = PVE::QemuServer::load_config($param->{vmid});
307
308 return $conf;
309 }});
310
311__PACKAGE__->register_method({
312 name => 'update_vm',
313 path => '{vmid}/config',
314 method => 'PUT',
315 protected => 1,
316 proxyto => 'node',
317 description => "Set virtual machine options.",
318 parameters => {
319 additionalProperties => 0,
320 properties => PVE::QemuServer::json_config_properties(
321 {
322 node => get_standard_option('pve-node'),
323 vmid => get_standard_option('pve-vmid'),
324 skiplock => {
325 description => "Ignore locks - only root is allowed to use this option.",
326 type => 'boolean',
327 optional => 1,
328 },
329 delete => {
330 type => 'string', format => 'pve-configid-list',
331 description => "A list of settings you want to delete.",
332 optional => 1,
333 },
334 force => {
335 type => 'boolean',
336 description => $opt_force_description,
337 optional => 1,
338 requires => 'delete',
339 },
554ac7e7
DM
340 digest => {
341 type => 'string',
342 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
343 maxLength => 40,
344 optional => 1,
345 }
1e3baf05
DM
346 }),
347 },
348 returns => { type => 'null'},
349 code => sub {
350 my ($param) = @_;
351
352 my $rpcenv = PVE::RPCEnvironment::get();
353
354 my $user = $rpcenv->get_user();
355
356 my $node = extract_param($param, 'node');
357
358 # fixme: fork worker?
359
360 my $vmid = extract_param($param, 'vmid');
361
362 my $skiplock = extract_param($param, 'skiplock');
363 raise_param_exc({ skiplock => "Only root may use this option." }) if $user ne 'root@pam';
364
365 my $delete = extract_param($param, 'delete');
366 my $force = extract_param($param, 'force');
367
368 die "no options specified\n" if !$delete && !scalar(keys %$param);
369
554ac7e7
DM
370 my $digest = extract_param($param, 'digest');
371
1e3baf05
DM
372 my $storecfg = PVE::Storage::config();
373
374 &$resolve_cdrom_alias($param);
375
376 my $eject = {};
377 my $cdchange = {};
378
379 foreach my $opt (keys %$param) {
380 if (PVE::QemuServer::valid_drivename($opt)) {
381 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
382 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
383 if ($drive->{file} eq 'eject') {
384 $eject->{$opt} = 1;
385 delete $param->{$opt};
386 next;
387 }
388
389 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
390 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
391
392 if (PVE::QemuServer::drive_is_cdrom($drive)) {
393 $cdchange->{$opt} = PVE::QemuServer::get_iso_path($storecfg, $vmid, $drive->{file});
394 }
395 }
396 }
397
398 foreach my $opt (PVE::Tools::split_list($delete)) {
399 $opt = 'ide2' if $opt eq 'cdrom';
400 die "you can't use '-$opt' and '-delete $opt' at the same time\n"
401 if defined($param->{$opt});
402 }
403
404 PVE::QemuServer::add_random_macs($param);
405
406 my $vollist = [];
407
408 my $updatefn = sub {
409
410 my $conf = PVE::QemuServer::load_config($vmid);
411
554ac7e7
DM
412 die "checksum missmatch (file change by other user?)\n"
413 if $digest && $digest ne $conf->{digest};
414
1e3baf05
DM
415 PVE::QemuServer::check_lock($conf) if !$skiplock;
416
417 foreach my $opt (keys %$eject) {
418 if ($conf->{$opt}) {
419 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
420 $cdchange->{$opt} = undef if PVE::QemuServer::drive_is_cdrom($drive);
421 } else {
422 raise_param_exc({ $opt => "eject failed - drive does not exist." });
423 }
424 }
425
426 foreach my $opt (keys %$param) {
427 next if !PVE::QemuServer::valid_drivename($opt);
428 next if !$conf->{$opt};
429 my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
430 next if PVE::QemuServer::drive_is_cdrom($old_drive);
431 my $new_drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
432 if ($new_drive->{file} ne $old_drive->{file}) {
433 my ($path, $owner);
434 eval { ($path, $owner) = PVE::Storage::path($storecfg, $old_drive->{file}); };
435 if ($owner && ($owner == $vmid)) {
436 PVE::QemuServer::add_unused_volume($conf, $param, $old_drive->{file});
437 }
438 }
439 }
440
441 my $unset = {};
442
443 foreach my $opt (PVE::Tools::split_list($delete)) {
444 $opt = 'ide2' if $opt eq 'cdrom';
445 if (!PVE::QemuServer::option_exists($opt)) {
446 raise_param_exc({ delete => "unknown option '$opt'" });
447 }
448 next if !defined($conf->{$opt});
449 if (PVE::QemuServer::valid_drivename($opt)) {
450 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
451 if (PVE::QemuServer::drive_is_cdrom($drive)) {
452 $cdchange->{$opt} = undef;
453 } else {
454 my $volid = $drive->{file};
455
456 if ($volid !~ m|^/|) {
457 my ($path, $owner);
458 eval { ($path, $owner) = PVE::Storage::path($storecfg, $volid); };
459 if ($owner && ($owner == $vmid)) {
460 if ($force) {
461 push @$vollist, $volid;
462 } else {
463 PVE::QemuServer::add_unused_volume($conf, $param, $volid);
464 }
465 }
466 }
467 }
468 } elsif ($opt =~ m/^unused/) {
469 push @$vollist, $conf->{$opt};
470 }
471
472 $unset->{$opt} = 1;
473 }
474
475 PVE::QemuServer::create_disks($storecfg, $vmid, $param);
476
477 PVE::QemuServer::change_config_nolock($vmid, $param, $unset, 1);
478
479 return if !PVE::QemuServer::check_running($vmid);
480
481 foreach my $opt (keys %$cdchange) {
482 my $qdn = PVE::QemuServer::qemu_drive_name($opt, 'cdrom');
483 my $path = $cdchange->{$opt};
484 PVE::QemuServer::vm_monitor_command($vmid, "eject $qdn", 0);
485 PVE::QemuServer::vm_monitor_command($vmid, "change $qdn \"$path\"", 0) if $path;
486 }
487 };
488
489 PVE::QemuServer::lock_config($vmid, $updatefn);
490
491 foreach my $volid (@$vollist) {
492 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
493 # fixme: log ?
494 warn $@ if $@;
495 }
496
497 return undef;
498 }});
499
500
501__PACKAGE__->register_method({
502 name => 'destroy_vm',
503 path => '{vmid}',
504 method => 'DELETE',
505 protected => 1,
506 proxyto => 'node',
507 description => "Destroy the vm (also delete all used/owned volumes).",
508 parameters => {
509 additionalProperties => 0,
510 properties => {
511 node => get_standard_option('pve-node'),
512 vmid => get_standard_option('pve-vmid'),
513 },
514 },
515 returns => { type => 'null' },
516 code => sub {
517 my ($param) = @_;
518
519 my $rpcenv = PVE::RPCEnvironment::get();
520
521 my $user = $rpcenv->get_user();
522
523 my $vmid = $param->{vmid};
524
525 my $skiplock = $param->{skiplock};
526 raise_param_exc({ skiplock => "Only root may use this option." })
527 if $user ne 'root@pam';
528
529 my $storecfg = PVE::Storage::config();
530
531 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
532
533 return undef;
534 }});
535
536__PACKAGE__->register_method({
537 name => 'unlink',
538 path => '{vmid}/unlink',
539 method => 'PUT',
540 protected => 1,
541 proxyto => 'node',
542 description => "Unlink/delete disk images.",
543 parameters => {
544 additionalProperties => 0,
545 properties => {
546 node => get_standard_option('pve-node'),
547 vmid => get_standard_option('pve-vmid'),
548 idlist => {
549 type => 'string', format => 'pve-configid-list',
550 description => "A list of disk IDs you want to delete.",
551 },
552 force => {
553 type => 'boolean',
554 description => $opt_force_description,
555 optional => 1,
556 },
557 },
558 },
559 returns => { type => 'null'},
560 code => sub {
561 my ($param) = @_;
562
563 $param->{delete} = extract_param($param, 'idlist');
564
565 __PACKAGE__->update_vm($param);
566
567 return undef;
568 }});
569
570my $sslcert;
571
572__PACKAGE__->register_method({
573 name => 'vncproxy',
574 path => '{vmid}/vncproxy',
575 method => 'POST',
576 protected => 1,
577 permissions => {
578 path => '/vms/{vmid}',
579 privs => [ 'VM.Console' ],
580 },
581 description => "Creates a TCP VNC proxy connections.",
582 parameters => {
583 additionalProperties => 0,
584 properties => {
585 node => get_standard_option('pve-node'),
586 vmid => get_standard_option('pve-vmid'),
587 },
588 },
589 returns => {
590 additionalProperties => 0,
591 properties => {
592 user => { type => 'string' },
593 ticket => { type => 'string' },
594 cert => { type => 'string' },
595 port => { type => 'integer' },
596 upid => { type => 'string' },
597 },
598 },
599 code => sub {
600 my ($param) = @_;
601
602 my $rpcenv = PVE::RPCEnvironment::get();
603
604 my $user = $rpcenv->get_user();
605 my $ticket = PVE::AccessControl::assemble_ticket($user);
606
607 my $vmid = $param->{vmid};
608 my $node = $param->{node};
609
610 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
611 if !$sslcert;
612
613 my $port = PVE::Tools::next_vnc_port();
614
615 my $remip;
616
617 if ($node ne PVE::INotify::nodename()) {
618 $remip = PVE::Cluster::remote_node_ip($node);
619 }
620
621 # NOTE: kvm VNC traffic is already TLS encrypted,
622 # so we select the fastest chipher here (or 'none'?)
623 my $remcmd = $remip ? ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes',
624 '-c', 'blowfish-cbc', $remip] : [];
625
626 my $timeout = 10;
627
628 my $realcmd = sub {
629 my $upid = shift;
630
631 syslog('info', "starting vnc proxy $upid\n");
632
633 my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
634
635 my $qmstr = join(' ', @$qmcmd);
636
637 # also redirect stderr (else we get RFB protocol errors)
638 my @cmd = ('/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null");
639
640 my $cmdstr = join(' ', @cmd);
641 syslog('info', "CMD3: $cmdstr");
642
643 if (system(@cmd) != 0) {
644 my $msg = "VM $vmid vnc proxy failed - $?";
645 syslog('err', $msg);
646 return;
647 }
648
649 return;
650 };
651
652 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $user, $realcmd);
653
654 return {
655 user => $user,
656 ticket => $ticket,
657 port => $port,
658 upid => $upid,
659 cert => $sslcert,
660 };
661 }});
662
663__PACKAGE__->register_method({
664 name => 'vm_status',
665 path => '{vmid}/status',
666 method => 'GET',
667 proxyto => 'node',
668 protected => 1, # qemu pid files are only readable by root
669 description => "Get virtual machine status.",
670 parameters => {
671 additionalProperties => 0,
672 properties => {
673 node => get_standard_option('pve-node'),
674 vmid => get_standard_option('pve-vmid'),
675 },
676 },
677 returns => { type => 'object' },
678 code => sub {
679 my ($param) = @_;
680
681 # test if VM exists
682 my $conf = PVE::QemuServer::load_config($param->{vmid});
683
684 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid});
685
686 return $vmstatus->{$param->{vmid}};
687 }});
688
689__PACKAGE__->register_method({
690 name => 'vm_command',
691 path => '{vmid}/status',
692 method => 'PUT',
693 protected => 1,
694 proxyto => 'node',
695 description => "Set virtual machine status.",
696 parameters => {
697 additionalProperties => 0,
698 properties => {
699 node => get_standard_option('pve-node'),
700 vmid => get_standard_option('pve-vmid'),
701 skiplock => {
702 description => "Ignore locks - only root is allowed to use this option.",
703 type => 'boolean',
704 optional => 1,
705 },
706 command => {
707 type => 'string',
708 enum => [qw(start stop reset shutdown cad suspend resume) ],
709 },
710 },
711 },
712 returns => { type => 'null'},
713 code => sub {
714 my ($param) = @_;
715
716 my $rpcenv = PVE::RPCEnvironment::get();
717
718 my $user = $rpcenv->get_user();
719
720 my $node = extract_param($param, 'node');
721
722 # fixme: proxy to correct node
723 # fixme: fork worker?
724
725 my $vmid = extract_param($param, 'vmid');
726
727 my $skiplock = extract_param($param, 'skiplock');
728 raise_param_exc({ skiplock => "Only root may use this option." })
729 if $user ne 'root@pam';
730
731 my $command = $param->{command};
732
733 my $storecfg = PVE::Storage::config();
734
735 if ($command eq 'start') {
736 my $statefile = undef; # fixme: --incoming parameter
737 PVE::QemuServer::vm_start($storecfg, $vmid, $statefile, $skiplock);
738 } elsif ($command eq 'stop') {
739 PVE::QemuServer::vm_stop($vmid, $skiplock);
740 } elsif ($command eq 'reset') {
741 PVE::QemuServer::vm_reset($vmid, $skiplock);
742 } elsif ($command eq 'shutdown') {
743 PVE::QemuServer::vm_shutdown($vmid, $skiplock);
744 } elsif ($command eq 'suspend') {
745 PVE::QemuServer::vm_suspend($vmid, $skiplock);
746 } elsif ($command eq 'resume') {
747 PVE::QemuServer::vm_resume($vmid, $skiplock);
748 } elsif ($command eq 'cad') {
749 PVE::QemuServer::vm_cad($vmid, $skiplock);
750 } else {
751 raise_param_exc({ command => "unknown command '$command'" })
752 }
753
754 return undef;
755 }});
756
757
7581;