]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
rework of hotplug/unplug in Qemu.pm
[qemu-server.git] / PVE / API2 / Qemu.pm
CommitLineData
1e3baf05
DM
1package PVE::API2::Qemu;
2
3use strict;
4use warnings;
5b9d692a 5use Cwd 'abs_path';
1e3baf05
DM
6
7use PVE::Cluster;
8use PVE::SafeSyslog;
9use PVE::Tools qw(extract_param);
10use PVE::Exception qw(raise raise_param_exc);
11use PVE::Storage;
12use PVE::JSONSchema qw(get_standard_option);
13use PVE::RESTHandler;
14use PVE::QemuServer;
3ea94c60 15use PVE::QemuMigrate;
1e3baf05
DM
16use PVE::RPCEnvironment;
17use PVE::AccessControl;
18use PVE::INotify;
19
20use Data::Dumper; # fixme: remove
21
22use base qw(PVE::RESTHandler);
23
24my $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.";
25
26my $resolve_cdrom_alias = sub {
27 my $param = shift;
28
29 if (my $value = $param->{cdrom}) {
30 $value .= ",media=cdrom" if $value !~ m/media=/;
31 $param->{ide2} = $value;
32 delete $param->{cdrom};
33 }
34};
35
36__PACKAGE__->register_method({
37 name => 'vmlist',
38 path => '',
39 method => 'GET',
40 description => "Virtual machine index (per node).",
41 proxyto => 'node',
42 protected => 1, # qemu pid files are only readable by root
43 parameters => {
44 additionalProperties => 0,
45 properties => {
46 node => get_standard_option('pve-node'),
47 },
48 },
49 returns => {
50 type => 'array',
51 items => {
52 type => "object",
53 properties => {},
54 },
55 links => [ { rel => 'child', href => "{vmid}" } ],
56 },
57 code => sub {
58 my ($param) = @_;
59
60 my $vmstatus = PVE::QemuServer::vmstatus();
61
62 return PVE::RESTHandler::hash_to_array($vmstatus, 'vmid');
63
64 }});
65
66__PACKAGE__->register_method({
67 name => 'create_vm',
68 path => '',
69 method => 'POST',
3e16d5fc 70 description => "Create or restore a virtual machine.",
1e3baf05
DM
71 protected => 1,
72 proxyto => 'node',
73 parameters => {
74 additionalProperties => 0,
75 properties => PVE::QemuServer::json_config_properties(
76 {
77 node => get_standard_option('pve-node'),
78 vmid => get_standard_option('pve-vmid'),
3e16d5fc
DM
79 archive => {
80 description => "The backup file.",
81 type => 'string',
82 optional => 1,
83 maxLength => 255,
84 },
85 storage => get_standard_option('pve-storage-id', {
86 description => "Default storage.",
87 optional => 1,
88 }),
89 force => {
90 optional => 1,
91 type => 'boolean',
92 description => "Allow to overwrite existing VM.",
51586c3a
DM
93 requires => 'archive',
94 },
95 unique => {
96 optional => 1,
97 type => 'boolean',
98 description => "Assign a unique random ethernet address.",
99 requires => 'archive',
3e16d5fc 100 },
1e3baf05
DM
101 }),
102 },
5fdbe4f0
DM
103 returns => {
104 type => 'string',
105 },
1e3baf05
DM
106 code => sub {
107 my ($param) = @_;
108
5fdbe4f0
DM
109 my $rpcenv = PVE::RPCEnvironment::get();
110
111 my $user = $rpcenv->get_user();
112
1e3baf05
DM
113 my $node = extract_param($param, 'node');
114
1e3baf05
DM
115 my $vmid = extract_param($param, 'vmid');
116
3e16d5fc
DM
117 my $archive = extract_param($param, 'archive');
118
119 my $storage = extract_param($param, 'storage');
120
51586c3a
DM
121 my $force = extract_param($param, 'force');
122
123 my $unique = extract_param($param, 'unique');
124
1e3baf05 125 my $filename = PVE::QemuServer::config_file($vmid);
1e3baf05
DM
126
127 my $storecfg = PVE::Storage::config();
128
3e16d5fc 129 PVE::Cluster::check_cfs_quorum();
1e3baf05 130
3e16d5fc
DM
131 if (!$archive) {
132 &$resolve_cdrom_alias($param);
1e3baf05 133
3e16d5fc
DM
134 foreach my $opt (keys %$param) {
135 if (PVE::QemuServer::valid_drivename($opt)) {
136 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
137 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
138
139 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
140 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
141 }
1e3baf05 142 }
3e16d5fc
DM
143
144 PVE::QemuServer::add_random_macs($param);
51586c3a
DM
145 } else {
146 my $keystr = join(' ', keys %$param);
bc4dcb99
DM
147 raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr;
148
5b9d692a
DM
149 if ($archive eq '-') {
150 die "pipe requires cli environment\n"
151 && $rpcenv->{type} ne 'cli';
152 } else {
971f27c4 153 my $path;
5b9d692a 154 if (PVE::Storage::parse_volume_id($archive, 1)) {
971f27c4 155 $path = PVE::Storage::path($storecfg, $archive);
5b9d692a
DM
156 } else {
157 raise_param_exc({ archive => "Only root can pass arbitrary paths." })
158 if $user ne 'root@pam';
159
971f27c4 160 $path = abs_path($archive);
5b9d692a 161 }
971f27c4
DM
162 die "can't find archive file '$archive'\n" if !($path && -f $path);
163 $archive = $path;
164 }
1e3baf05
DM
165 }
166
3e16d5fc
DM
167 my $restorefn = sub {
168
169 if (-f $filename) {
170 die "unable to restore vm $vmid: config file already exists\n"
51586c3a 171 if !$force;
3e16d5fc
DM
172
173 die "unable to restore vm $vmid: vm is running\n"
174 if PVE::QemuServer::check_running($vmid);
a6af7b3e
DM
175
176 # destroy existing data - keep empty config
177 PVE::QemuServer::destroy_vm($storecfg, $vmid, 1);
3e16d5fc
DM
178 }
179
180 my $realcmd = sub {
51586c3a
DM
181 PVE::QemuServer::restore_archive($archive, $vmid, {
182 storage => $storage,
183 unique => $unique });
3e16d5fc
DM
184 };
185
186 return $rpcenv->fork_worker('qmrestore', $vmid, $user, $realcmd);
187 };
1e3baf05 188
1e3baf05
DM
189 my $createfn = sub {
190
191 # second test (after locking test is accurate)
192 die "unable to create vm $vmid: config file already exists\n"
193 if -f $filename;
194
5fdbe4f0 195 my $realcmd = sub {
1e3baf05 196
5fdbe4f0 197 my $vollist = [];
1e3baf05 198
5fdbe4f0 199 eval {
3e16d5fc 200 $vollist = PVE::QemuServer::create_disks($storecfg, $vmid, $param, $storage);
1e3baf05 201
5fdbe4f0
DM
202 # try to be smart about bootdisk
203 my @disks = PVE::QemuServer::disknames();
204 my $firstdisk;
205 foreach my $ds (reverse @disks) {
206 next if !$param->{$ds};
207 my $disk = PVE::QemuServer::parse_drive($ds, $param->{$ds});
208 next if PVE::QemuServer::drive_is_cdrom($disk);
209 $firstdisk = $ds;
210 }
1e3baf05 211
5fdbe4f0
DM
212 if (!$param->{bootdisk} && $firstdisk) {
213 $param->{bootdisk} = $firstdisk;
214 }
1e3baf05 215
5fdbe4f0
DM
216 PVE::QemuServer::create_conf_nolock($vmid, $param);
217 };
218 my $err = $@;
1e3baf05 219
5fdbe4f0
DM
220 if ($err) {
221 foreach my $volid (@$vollist) {
222 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
223 warn $@ if $@;
224 }
225 die "create failed - $err";
226 }
227 };
228
229 return $rpcenv->fork_worker('qmcreate', $vmid, $user, $realcmd);
230 };
231
3e16d5fc 232 return PVE::QemuServer::lock_config($vmid, $archive ? $restorefn : $createfn);
1e3baf05
DM
233 }});
234
235__PACKAGE__->register_method({
236 name => 'vmdiridx',
237 path => '{vmid}',
238 method => 'GET',
239 proxyto => 'node',
240 description => "Directory index",
241 parameters => {
242 additionalProperties => 0,
243 properties => {
244 node => get_standard_option('pve-node'),
245 vmid => get_standard_option('pve-vmid'),
246 },
247 },
248 returns => {
249 type => 'array',
250 items => {
251 type => "object",
252 properties => {
253 subdir => { type => 'string' },
254 },
255 },
256 links => [ { rel => 'child', href => "{subdir}" } ],
257 },
258 code => sub {
259 my ($param) = @_;
260
261 my $res = [
262 { subdir => 'config' },
263 { subdir => 'status' },
264 { subdir => 'unlink' },
265 { subdir => 'vncproxy' },
3ea94c60 266 { subdir => 'migrate' },
1e3baf05
DM
267 { subdir => 'rrd' },
268 { subdir => 'rrddata' },
91c94f0a 269 { subdir => 'monitor' },
1e3baf05
DM
270 ];
271
272 return $res;
273 }});
274
275__PACKAGE__->register_method({
276 name => 'rrd',
277 path => '{vmid}/rrd',
278 method => 'GET',
279 protected => 1, # fixme: can we avoid that?
280 permissions => {
281 path => '/vms/{vmid}',
282 privs => [ 'VM.Audit' ],
283 },
284 description => "Read VM RRD statistics (returns PNG)",
285 parameters => {
286 additionalProperties => 0,
287 properties => {
288 node => get_standard_option('pve-node'),
289 vmid => get_standard_option('pve-vmid'),
290 timeframe => {
291 description => "Specify the time frame you are interested in.",
292 type => 'string',
293 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
294 },
295 ds => {
296 description => "The list of datasources you want to display.",
297 type => 'string', format => 'pve-configid-list',
298 },
299 cf => {
300 description => "The RRD consolidation function",
301 type => 'string',
302 enum => [ 'AVERAGE', 'MAX' ],
303 optional => 1,
304 },
305 },
306 },
307 returns => {
308 type => "object",
309 properties => {
310 filename => { type => 'string' },
311 },
312 },
313 code => sub {
314 my ($param) = @_;
315
316 return PVE::Cluster::create_rrd_graph(
317 "pve2-vm/$param->{vmid}", $param->{timeframe},
318 $param->{ds}, $param->{cf});
319
320 }});
321
322__PACKAGE__->register_method({
323 name => 'rrddata',
324 path => '{vmid}/rrddata',
325 method => 'GET',
326 protected => 1, # fixme: can we avoid that?
327 permissions => {
328 path => '/vms/{vmid}',
329 privs => [ 'VM.Audit' ],
330 },
331 description => "Read VM RRD statistics",
332 parameters => {
333 additionalProperties => 0,
334 properties => {
335 node => get_standard_option('pve-node'),
336 vmid => get_standard_option('pve-vmid'),
337 timeframe => {
338 description => "Specify the time frame you are interested in.",
339 type => 'string',
340 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
341 },
342 cf => {
343 description => "The RRD consolidation function",
344 type => 'string',
345 enum => [ 'AVERAGE', 'MAX' ],
346 optional => 1,
347 },
348 },
349 },
350 returns => {
351 type => "array",
352 items => {
353 type => "object",
354 properties => {},
355 },
356 },
357 code => sub {
358 my ($param) = @_;
359
360 return PVE::Cluster::create_rrd_data(
361 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
362 }});
363
364
365__PACKAGE__->register_method({
366 name => 'vm_config',
367 path => '{vmid}/config',
368 method => 'GET',
369 proxyto => 'node',
370 description => "Get virtual machine configuration.",
371 parameters => {
372 additionalProperties => 0,
373 properties => {
374 node => get_standard_option('pve-node'),
375 vmid => get_standard_option('pve-vmid'),
376 },
377 },
378 returns => {
379 type => "object",
554ac7e7
DM
380 properties => {
381 digest => {
382 type => 'string',
383 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
384 }
385 },
1e3baf05
DM
386 },
387 code => sub {
388 my ($param) = @_;
389
390 my $conf = PVE::QemuServer::load_config($param->{vmid});
391
392 return $conf;
393 }});
394
395__PACKAGE__->register_method({
396 name => 'update_vm',
397 path => '{vmid}/config',
398 method => 'PUT',
399 protected => 1,
400 proxyto => 'node',
401 description => "Set virtual machine options.",
402 parameters => {
403 additionalProperties => 0,
404 properties => PVE::QemuServer::json_config_properties(
405 {
406 node => get_standard_option('pve-node'),
407 vmid => get_standard_option('pve-vmid'),
3ea94c60 408 skiplock => get_standard_option('skiplock'),
1e3baf05
DM
409 delete => {
410 type => 'string', format => 'pve-configid-list',
411 description => "A list of settings you want to delete.",
412 optional => 1,
413 },
414 force => {
415 type => 'boolean',
416 description => $opt_force_description,
417 optional => 1,
418 requires => 'delete',
419 },
554ac7e7
DM
420 digest => {
421 type => 'string',
422 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
423 maxLength => 40,
424 optional => 1,
425 }
1e3baf05
DM
426 }),
427 },
428 returns => { type => 'null'},
429 code => sub {
430 my ($param) = @_;
431
432 my $rpcenv = PVE::RPCEnvironment::get();
433
434 my $user = $rpcenv->get_user();
435
436 my $node = extract_param($param, 'node');
437
1e3baf05
DM
438 my $vmid = extract_param($param, 'vmid');
439
5fdbe4f0
DM
440 my $digest = extract_param($param, 'digest');
441
fcdb0117 442 my @hotplugerr = ();
5fdbe4f0
DM
443 my @paramarr = (); # used for log message
444 foreach my $key (keys %$param) {
445 push @paramarr, "-$key", $param->{$key};
446 }
447
1e3baf05 448 my $skiplock = extract_param($param, 'skiplock');
3ea94c60
DM
449 raise_param_exc({ skiplock => "Only root may use this option." })
450 if $skiplock && $user ne 'root@pam';
1e3baf05
DM
451
452 my $delete = extract_param($param, 'delete');
453 my $force = extract_param($param, 'force');
454
455 die "no options specified\n" if !$delete && !scalar(keys %$param);
456
457 my $storecfg = PVE::Storage::config();
458
459 &$resolve_cdrom_alias($param);
460
461 my $eject = {};
462 my $cdchange = {};
463
464 foreach my $opt (keys %$param) {
465 if (PVE::QemuServer::valid_drivename($opt)) {
466 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
467 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
468 if ($drive->{file} eq 'eject') {
469 $eject->{$opt} = 1;
470 delete $param->{$opt};
471 next;
472 }
473
474 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
475 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
476
477 if (PVE::QemuServer::drive_is_cdrom($drive)) {
478 $cdchange->{$opt} = PVE::QemuServer::get_iso_path($storecfg, $vmid, $drive->{file});
479 }
480 }
481 }
482
483 foreach my $opt (PVE::Tools::split_list($delete)) {
484 $opt = 'ide2' if $opt eq 'cdrom';
485 die "you can't use '-$opt' and '-delete $opt' at the same time\n"
486 if defined($param->{$opt});
487 }
488
489 PVE::QemuServer::add_random_macs($param);
490
491 my $vollist = [];
492
493 my $updatefn = sub {
494
495 my $conf = PVE::QemuServer::load_config($vmid);
496
554ac7e7
DM
497 die "checksum missmatch (file change by other user?)\n"
498 if $digest && $digest ne $conf->{digest};
499
1e3baf05
DM
500 PVE::QemuServer::check_lock($conf) if !$skiplock;
501
5fdbe4f0
DM
502 PVE::Cluster::log_msg('info', $user, "update VM $vmid: " . join (' ', @paramarr));
503
fcdb0117
DA
504 my @newdelete = ();
505 foreach my $opt (PVE::Tools::split_list($delete)) {
506 if(PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt)) {
507 push(@newdelete, $opt);
508 }
509 else {
510 push(@hotplugerr, $opt);
511 }
512 }
513 $delete = join(',', @newdelete) if scalar(@newdelete) > 0;
514
1e3baf05
DM
515 foreach my $opt (keys %$eject) {
516 if ($conf->{$opt}) {
517 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
518 $cdchange->{$opt} = undef if PVE::QemuServer::drive_is_cdrom($drive);
519 } else {
520 raise_param_exc({ $opt => "eject failed - drive does not exist." });
521 }
522 }
523
524 foreach my $opt (keys %$param) {
525 next if !PVE::QemuServer::valid_drivename($opt);
526 next if !$conf->{$opt};
527 my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
528 next if PVE::QemuServer::drive_is_cdrom($old_drive);
529 my $new_drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
530 if ($new_drive->{file} ne $old_drive->{file}) {
531 my ($path, $owner);
532 eval { ($path, $owner) = PVE::Storage::path($storecfg, $old_drive->{file}); };
533 if ($owner && ($owner == $vmid)) {
534 PVE::QemuServer::add_unused_volume($conf, $param, $old_drive->{file});
535 }
536 }
537 }
538
539 my $unset = {};
540
541 foreach my $opt (PVE::Tools::split_list($delete)) {
542 $opt = 'ide2' if $opt eq 'cdrom';
543 if (!PVE::QemuServer::option_exists($opt)) {
544 raise_param_exc({ delete => "unknown option '$opt'" });
545 }
546 next if !defined($conf->{$opt});
547 if (PVE::QemuServer::valid_drivename($opt)) {
548 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
549 if (PVE::QemuServer::drive_is_cdrom($drive)) {
550 $cdchange->{$opt} = undef;
551 } else {
552 my $volid = $drive->{file};
553
554 if ($volid !~ m|^/|) {
555 my ($path, $owner);
556 eval { ($path, $owner) = PVE::Storage::path($storecfg, $volid); };
557 if ($owner && ($owner == $vmid)) {
558 if ($force) {
559 push @$vollist, $volid;
560 } else {
561 PVE::QemuServer::add_unused_volume($conf, $param, $volid);
562 }
563 }
564 }
565 }
566 } elsif ($opt =~ m/^unused/) {
567 push @$vollist, $conf->{$opt};
568 }
569
570 $unset->{$opt} = 1;
571 }
572
f19d1c47 573 PVE::QemuServer::create_disks($storecfg, $vmid, $param, $conf);
1e3baf05 574
fcdb0117
DA
575 #hotplug disks
576 foreach my $opt (keys %$param) {
577 if($opt =~ m/^(scsi|virtio)(\d+)$/) {
578 my $device = PVE::QemuServer::parse_drive($opt, $param->{$opt});
579 if(!PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $device)) {
580 $unset->{$opt} = 1;
581 PVE::QemuServer::add_unused_volume($param, $device->{file});
582 push(@hotplugerr, $opt);
583 }
584 }
585 }
586
1e3baf05
DM
587 PVE::QemuServer::change_config_nolock($vmid, $param, $unset, 1);
588
589 return if !PVE::QemuServer::check_running($vmid);
590
591 foreach my $opt (keys %$cdchange) {
592 my $qdn = PVE::QemuServer::qemu_drive_name($opt, 'cdrom');
593 my $path = $cdchange->{$opt};
594 PVE::QemuServer::vm_monitor_command($vmid, "eject $qdn", 0);
595 PVE::QemuServer::vm_monitor_command($vmid, "change $qdn \"$path\"", 0) if $path;
596 }
597 };
598
599 PVE::QemuServer::lock_config($vmid, $updatefn);
600
601 foreach my $volid (@$vollist) {
602 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
603 # fixme: log ?
604 warn $@ if $@;
605 }
606
fcdb0117
DA
607 raise_param_exc({ hotplug => "error hotplug/unplug ".join(',', @hotplugerr)})
608 if scalar(@hotplugerr) > 0;
609
1e3baf05
DM
610 return undef;
611 }});
612
613
614__PACKAGE__->register_method({
615 name => 'destroy_vm',
616 path => '{vmid}',
617 method => 'DELETE',
618 protected => 1,
619 proxyto => 'node',
620 description => "Destroy the vm (also delete all used/owned volumes).",
621 parameters => {
622 additionalProperties => 0,
623 properties => {
624 node => get_standard_option('pve-node'),
625 vmid => get_standard_option('pve-vmid'),
3ea94c60 626 skiplock => get_standard_option('skiplock'),
1e3baf05
DM
627 },
628 },
5fdbe4f0
DM
629 returns => {
630 type => 'string',
631 },
1e3baf05
DM
632 code => sub {
633 my ($param) = @_;
634
635 my $rpcenv = PVE::RPCEnvironment::get();
636
637 my $user = $rpcenv->get_user();
638
639 my $vmid = $param->{vmid};
640
641 my $skiplock = $param->{skiplock};
642 raise_param_exc({ skiplock => "Only root may use this option." })
3ea94c60 643 if $skiplock && $user ne 'root@pam';
1e3baf05 644
5fdbe4f0
DM
645 # test if VM exists
646 my $conf = PVE::QemuServer::load_config($vmid);
647
1e3baf05
DM
648 my $storecfg = PVE::Storage::config();
649
5fdbe4f0 650 my $realcmd = sub {
ff1a2432
DM
651 my $upid = shift;
652
653 syslog('info', "destroy VM $vmid: $upid\n");
654
5fdbe4f0
DM
655 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
656 };
1e3baf05 657
5fdbe4f0 658 return $rpcenv->fork_worker('qmdestroy', $vmid, $user, $realcmd);
1e3baf05
DM
659 }});
660
661__PACKAGE__->register_method({
662 name => 'unlink',
663 path => '{vmid}/unlink',
664 method => 'PUT',
665 protected => 1,
666 proxyto => 'node',
667 description => "Unlink/delete disk images.",
668 parameters => {
669 additionalProperties => 0,
670 properties => {
671 node => get_standard_option('pve-node'),
672 vmid => get_standard_option('pve-vmid'),
673 idlist => {
674 type => 'string', format => 'pve-configid-list',
675 description => "A list of disk IDs you want to delete.",
676 },
677 force => {
678 type => 'boolean',
679 description => $opt_force_description,
680 optional => 1,
681 },
682 },
683 },
684 returns => { type => 'null'},
685 code => sub {
686 my ($param) = @_;
687
688 $param->{delete} = extract_param($param, 'idlist');
689
690 __PACKAGE__->update_vm($param);
691
692 return undef;
693 }});
694
695my $sslcert;
696
697__PACKAGE__->register_method({
698 name => 'vncproxy',
699 path => '{vmid}/vncproxy',
700 method => 'POST',
701 protected => 1,
702 permissions => {
703 path => '/vms/{vmid}',
704 privs => [ 'VM.Console' ],
705 },
706 description => "Creates a TCP VNC proxy connections.",
707 parameters => {
708 additionalProperties => 0,
709 properties => {
710 node => get_standard_option('pve-node'),
711 vmid => get_standard_option('pve-vmid'),
712 },
713 },
714 returns => {
715 additionalProperties => 0,
716 properties => {
717 user => { type => 'string' },
718 ticket => { type => 'string' },
719 cert => { type => 'string' },
720 port => { type => 'integer' },
721 upid => { type => 'string' },
722 },
723 },
724 code => sub {
725 my ($param) = @_;
726
727 my $rpcenv = PVE::RPCEnvironment::get();
728
729 my $user = $rpcenv->get_user();
1e3baf05
DM
730
731 my $vmid = $param->{vmid};
732 my $node = $param->{node};
733
b6f39da2
DM
734 my $authpath = "/vms/$vmid";
735
736 my $ticket = PVE::AccessControl::assemble_vnc_ticket($user, $authpath);
737
1e3baf05
DM
738 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
739 if !$sslcert;
740
741 my $port = PVE::Tools::next_vnc_port();
742
743 my $remip;
744
4f1be36c 745 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1e3baf05
DM
746 $remip = PVE::Cluster::remote_node_ip($node);
747 }
748
749 # NOTE: kvm VNC traffic is already TLS encrypted,
750 # so we select the fastest chipher here (or 'none'?)
751 my $remcmd = $remip ? ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes',
752 '-c', 'blowfish-cbc', $remip] : [];
753
754 my $timeout = 10;
755
756 my $realcmd = sub {
757 my $upid = shift;
758
759 syslog('info', "starting vnc proxy $upid\n");
760
761 my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
762
763 my $qmstr = join(' ', @$qmcmd);
764
765 # also redirect stderr (else we get RFB protocol errors)
be62c45c 766 my $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"];
1e3baf05 767
be62c45c 768 PVE::Tools::run_command($cmd);
1e3baf05
DM
769
770 return;
771 };
772
773 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $user, $realcmd);
774
775 return {
776 user => $user,
777 ticket => $ticket,
778 port => $port,
779 upid => $upid,
780 cert => $sslcert,
781 };
782 }});
783
5fdbe4f0
DM
784__PACKAGE__->register_method({
785 name => 'vmcmdidx',
786 path => '{vmid}/status',
787 method => 'GET',
788 proxyto => 'node',
789 description => "Directory index",
790 parameters => {
791 additionalProperties => 0,
792 properties => {
793 node => get_standard_option('pve-node'),
794 vmid => get_standard_option('pve-vmid'),
795 },
796 },
797 returns => {
798 type => 'array',
799 items => {
800 type => "object",
801 properties => {
802 subdir => { type => 'string' },
803 },
804 },
805 links => [ { rel => 'child', href => "{subdir}" } ],
806 },
807 code => sub {
808 my ($param) = @_;
809
810 # test if VM exists
811 my $conf = PVE::QemuServer::load_config($param->{vmid});
812
813 my $res = [
814 { subdir => 'current' },
815 { subdir => 'start' },
816 { subdir => 'stop' },
817 ];
818
819 return $res;
820 }});
821
1e3baf05
DM
822__PACKAGE__->register_method({
823 name => 'vm_status',
5fdbe4f0 824 path => '{vmid}/status/current',
1e3baf05
DM
825 method => 'GET',
826 proxyto => 'node',
827 protected => 1, # qemu pid files are only readable by root
828 description => "Get virtual machine status.",
829 parameters => {
830 additionalProperties => 0,
831 properties => {
832 node => get_standard_option('pve-node'),
833 vmid => get_standard_option('pve-vmid'),
834 },
835 },
836 returns => { type => 'object' },
837 code => sub {
838 my ($param) = @_;
839
840 # test if VM exists
841 my $conf = PVE::QemuServer::load_config($param->{vmid});
842
ff1a2432 843 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid});
8610701a 844 my $status = $vmstatus->{$param->{vmid}};
1e3baf05 845
8610701a
DM
846 my $cc = PVE::Cluster::cfs_read_file('cluster.conf');
847 if (PVE::Cluster::cluster_conf_lookup_pvevm($cc, 0, $param->{vmid}, 1)) {
848 $status->{ha} = 1;
849 } else {
850 $status->{ha} = 0;
851 }
852
853 return $status;
1e3baf05
DM
854 }});
855
856__PACKAGE__->register_method({
5fdbe4f0
DM
857 name => 'vm_start',
858 path => '{vmid}/status/start',
859 method => 'POST',
1e3baf05
DM
860 protected => 1,
861 proxyto => 'node',
5fdbe4f0 862 description => "Start virtual machine.",
1e3baf05
DM
863 parameters => {
864 additionalProperties => 0,
865 properties => {
866 node => get_standard_option('pve-node'),
867 vmid => get_standard_option('pve-vmid'),
3ea94c60
DM
868 skiplock => get_standard_option('skiplock'),
869 stateuri => get_standard_option('pve-qm-stateuri'),
1e3baf05
DM
870 },
871 },
5fdbe4f0
DM
872 returns => {
873 type => 'string',
874 },
1e3baf05
DM
875 code => sub {
876 my ($param) = @_;
877
878 my $rpcenv = PVE::RPCEnvironment::get();
879
880 my $user = $rpcenv->get_user();
881
882 my $node = extract_param($param, 'node');
883
1e3baf05
DM
884 my $vmid = extract_param($param, 'vmid');
885
3ea94c60
DM
886 my $stateuri = extract_param($param, 'stateuri');
887 raise_param_exc({ stateuri => "Only root may use this option." })
888 if $stateuri && $user ne 'root@pam';
889
1e3baf05
DM
890 my $skiplock = extract_param($param, 'skiplock');
891 raise_param_exc({ skiplock => "Only root may use this option." })
3ea94c60 892 if $skiplock && $user ne 'root@pam';
1e3baf05 893
1e3baf05 894 my $storecfg = PVE::Storage::config();
5fdbe4f0
DM
895
896 my $realcmd = sub {
897 my $upid = shift;
898
899 syslog('info', "start VM $vmid: $upid\n");
900
3ea94c60 901 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock);
5fdbe4f0
DM
902
903 return;
904 };
905
906 return $rpcenv->fork_worker('qmstart', $vmid, $user, $realcmd);
907 }});
908
909__PACKAGE__->register_method({
910 name => 'vm_stop',
911 path => '{vmid}/status/stop',
912 method => 'POST',
913 protected => 1,
914 proxyto => 'node',
915 description => "Stop virtual machine.",
916 parameters => {
917 additionalProperties => 0,
918 properties => {
919 node => get_standard_option('pve-node'),
920 vmid => get_standard_option('pve-vmid'),
921 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
922 timeout => {
923 description => "Wait maximal timeout seconds.",
924 type => 'integer',
925 minimum => 0,
926 optional => 1,
254575e9
DM
927 },
928 keepActive => {
929 description => "Do not decativate storage volumes.",
930 type => 'boolean',
931 optional => 1,
932 default => 0,
c6bb9502 933 }
5fdbe4f0
DM
934 },
935 },
936 returns => {
937 type => 'string',
938 },
939 code => sub {
940 my ($param) = @_;
941
942 my $rpcenv = PVE::RPCEnvironment::get();
943
944 my $user = $rpcenv->get_user();
945
946 my $node = extract_param($param, 'node');
947
948 my $vmid = extract_param($param, 'vmid');
949
950 my $skiplock = extract_param($param, 'skiplock');
951 raise_param_exc({ skiplock => "Only root may use this option." })
952 if $skiplock && $user ne 'root@pam';
953
254575e9
DM
954 my $keepActive = extract_param($param, 'keepActive');
955 raise_param_exc({ keepActive => "Only root may use this option." })
956 if $keepActive && $user ne 'root@pam';
957
ff1a2432
DM
958 my $storecfg = PVE::Storage::config();
959
5fdbe4f0
DM
960 my $realcmd = sub {
961 my $upid = shift;
962
963 syslog('info', "stop VM $vmid: $upid\n");
964
254575e9
DM
965 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
966 $param->{timeout}, 0, 1, $keepActive);
c6bb9502 967
5fdbe4f0
DM
968 return;
969 };
970
971 return $rpcenv->fork_worker('qmstop', $vmid, $user, $realcmd);
972 }});
973
974__PACKAGE__->register_method({
975 name => 'vm_reset',
976 path => '{vmid}/status/reset',
977 method => 'POST',
978 protected => 1,
979 proxyto => 'node',
980 description => "Reset virtual machine.",
981 parameters => {
982 additionalProperties => 0,
983 properties => {
984 node => get_standard_option('pve-node'),
985 vmid => get_standard_option('pve-vmid'),
986 skiplock => get_standard_option('skiplock'),
987 },
988 },
989 returns => {
990 type => 'string',
991 },
992 code => sub {
993 my ($param) = @_;
994
995 my $rpcenv = PVE::RPCEnvironment::get();
996
997 my $user = $rpcenv->get_user();
998
999 my $node = extract_param($param, 'node');
1000
1001 my $vmid = extract_param($param, 'vmid');
1002
1003 my $skiplock = extract_param($param, 'skiplock');
1004 raise_param_exc({ skiplock => "Only root may use this option." })
1005 if $skiplock && $user ne 'root@pam';
1006
ff1a2432
DM
1007 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1008
5fdbe4f0
DM
1009 my $realcmd = sub {
1010 my $upid = shift;
1011
1e3baf05 1012 PVE::QemuServer::vm_reset($vmid, $skiplock);
5fdbe4f0
DM
1013
1014 return;
1015 };
1016
1017 return $rpcenv->fork_worker('qmreset', $vmid, $user, $realcmd);
1018 }});
1019
1020__PACKAGE__->register_method({
1021 name => 'vm_shutdown',
1022 path => '{vmid}/status/shutdown',
1023 method => 'POST',
1024 protected => 1,
1025 proxyto => 'node',
1026 description => "Shutdown virtual machine.",
1027 parameters => {
1028 additionalProperties => 0,
1029 properties => {
1030 node => get_standard_option('pve-node'),
1031 vmid => get_standard_option('pve-vmid'),
1032 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
1033 timeout => {
1034 description => "Wait maximal timeout seconds.",
1035 type => 'integer',
1036 minimum => 0,
1037 optional => 1,
9269013a
DM
1038 },
1039 forceStop => {
1040 description => "Make sure the VM stops.",
1041 type => 'boolean',
1042 optional => 1,
1043 default => 0,
254575e9
DM
1044 },
1045 keepActive => {
1046 description => "Do not decativate storage volumes.",
1047 type => 'boolean',
1048 optional => 1,
1049 default => 0,
c6bb9502 1050 }
5fdbe4f0
DM
1051 },
1052 },
1053 returns => {
1054 type => 'string',
1055 },
1056 code => sub {
1057 my ($param) = @_;
1058
1059 my $rpcenv = PVE::RPCEnvironment::get();
1060
1061 my $user = $rpcenv->get_user();
1062
1063 my $node = extract_param($param, 'node');
1064
1065 my $vmid = extract_param($param, 'vmid');
1066
1067 my $skiplock = extract_param($param, 'skiplock');
1068 raise_param_exc({ skiplock => "Only root may use this option." })
1069 if $skiplock && $user ne 'root@pam';
1070
254575e9
DM
1071 my $keepActive = extract_param($param, 'keepActive');
1072 raise_param_exc({ keepActive => "Only root may use this option." })
1073 if $keepActive && $user ne 'root@pam';
1074
02d07cf5
DM
1075 my $storecfg = PVE::Storage::config();
1076
5fdbe4f0
DM
1077 my $realcmd = sub {
1078 my $upid = shift;
1079
1080 syslog('info', "shutdown VM $vmid: $upid\n");
1081
254575e9
DM
1082 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
1083 1, $param->{forceStop}, $keepActive);
c6bb9502 1084
5fdbe4f0
DM
1085 return;
1086 };
1087
1088 return $rpcenv->fork_worker('qmshutdown', $vmid, $user, $realcmd);
1089 }});
1090
1091__PACKAGE__->register_method({
1092 name => 'vm_suspend',
1093 path => '{vmid}/status/suspend',
1094 method => 'POST',
1095 protected => 1,
1096 proxyto => 'node',
1097 description => "Suspend virtual machine.",
1098 parameters => {
1099 additionalProperties => 0,
1100 properties => {
1101 node => get_standard_option('pve-node'),
1102 vmid => get_standard_option('pve-vmid'),
1103 skiplock => get_standard_option('skiplock'),
1104 },
1105 },
1106 returns => {
1107 type => 'string',
1108 },
1109 code => sub {
1110 my ($param) = @_;
1111
1112 my $rpcenv = PVE::RPCEnvironment::get();
1113
1114 my $user = $rpcenv->get_user();
1115
1116 my $node = extract_param($param, 'node');
1117
1118 my $vmid = extract_param($param, 'vmid');
1119
1120 my $skiplock = extract_param($param, 'skiplock');
1121 raise_param_exc({ skiplock => "Only root may use this option." })
1122 if $skiplock && $user ne 'root@pam';
1123
ff1a2432
DM
1124 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1125
5fdbe4f0
DM
1126 my $realcmd = sub {
1127 my $upid = shift;
1128
1129 syslog('info', "suspend VM $vmid: $upid\n");
1130
1e3baf05 1131 PVE::QemuServer::vm_suspend($vmid, $skiplock);
5fdbe4f0
DM
1132
1133 return;
1134 };
1135
1136 return $rpcenv->fork_worker('qmsuspend', $vmid, $user, $realcmd);
1137 }});
1138
1139__PACKAGE__->register_method({
1140 name => 'vm_resume',
1141 path => '{vmid}/status/resume',
1142 method => 'POST',
1143 protected => 1,
1144 proxyto => 'node',
1145 description => "Resume virtual machine.",
1146 parameters => {
1147 additionalProperties => 0,
1148 properties => {
1149 node => get_standard_option('pve-node'),
1150 vmid => get_standard_option('pve-vmid'),
1151 skiplock => get_standard_option('skiplock'),
1152 },
1153 },
1154 returns => {
1155 type => 'string',
1156 },
1157 code => sub {
1158 my ($param) = @_;
1159
1160 my $rpcenv = PVE::RPCEnvironment::get();
1161
1162 my $user = $rpcenv->get_user();
1163
1164 my $node = extract_param($param, 'node');
1165
1166 my $vmid = extract_param($param, 'vmid');
1167
1168 my $skiplock = extract_param($param, 'skiplock');
1169 raise_param_exc({ skiplock => "Only root may use this option." })
1170 if $skiplock && $user ne 'root@pam';
1171
b7eeab21 1172 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
ff1a2432 1173
5fdbe4f0
DM
1174 my $realcmd = sub {
1175 my $upid = shift;
1176
1177 syslog('info', "resume VM $vmid: $upid\n");
1178
1e3baf05 1179 PVE::QemuServer::vm_resume($vmid, $skiplock);
1e3baf05 1180
5fdbe4f0
DM
1181 return;
1182 };
1183
1184 return $rpcenv->fork_worker('qmresume', $vmid, $user, $realcmd);
1185 }});
1186
1187__PACKAGE__->register_method({
1188 name => 'vm_sendkey',
1189 path => '{vmid}/sendkey',
1190 method => 'PUT',
1191 protected => 1,
1192 proxyto => 'node',
1193 description => "Send key event to virtual machine.",
1194 parameters => {
1195 additionalProperties => 0,
1196 properties => {
1197 node => get_standard_option('pve-node'),
1198 vmid => get_standard_option('pve-vmid'),
1199 skiplock => get_standard_option('skiplock'),
1200 key => {
1201 description => "The key (qemu monitor encoding).",
1202 type => 'string'
1203 }
1204 },
1205 },
1206 returns => { type => 'null'},
1207 code => sub {
1208 my ($param) = @_;
1209
1210 my $rpcenv = PVE::RPCEnvironment::get();
1211
1212 my $user = $rpcenv->get_user();
1213
1214 my $node = extract_param($param, 'node');
1215
1216 my $vmid = extract_param($param, 'vmid');
1217
1218 my $skiplock = extract_param($param, 'skiplock');
1219 raise_param_exc({ skiplock => "Only root may use this option." })
1220 if $skiplock && $user ne 'root@pam';
1221
1222 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
1223
1224 return;
1e3baf05
DM
1225 }});
1226
3ea94c60
DM
1227__PACKAGE__->register_method({
1228 name => 'migrate_vm',
1229 path => '{vmid}/migrate',
1230 method => 'POST',
1231 protected => 1,
1232 proxyto => 'node',
1233 description => "Migrate virtual machine. Creates a new migration task.",
1234 parameters => {
1235 additionalProperties => 0,
1236 properties => {
1237 node => get_standard_option('pve-node'),
1238 vmid => get_standard_option('pve-vmid'),
1239 target => get_standard_option('pve-node', { description => "Target node." }),
1240 online => {
1241 type => 'boolean',
1242 description => "Use online/live migration.",
1243 optional => 1,
1244 },
1245 force => {
1246 type => 'boolean',
1247 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
1248 optional => 1,
1249 },
1250 },
1251 },
1252 returns => {
1253 type => 'string',
1254 description => "the task ID.",
1255 },
1256 code => sub {
1257 my ($param) = @_;
1258
1259 my $rpcenv = PVE::RPCEnvironment::get();
1260
1261 my $user = $rpcenv->get_user();
1262
1263 my $target = extract_param($param, 'target');
1264
1265 my $localnode = PVE::INotify::nodename();
1266 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
1267
1268 PVE::Cluster::check_cfs_quorum();
1269
1270 PVE::Cluster::check_node_exists($target);
1271
1272 my $targetip = PVE::Cluster::remote_node_ip($target);
1273
1274 my $vmid = extract_param($param, 'vmid');
1275
a591eeba
DM
1276 raise_param_exc({ force => "Only root may use this option." })
1277 if $param->{force} && $user ne 'root@pam';
3ea94c60
DM
1278
1279 # test if VM exists
a5ed42d3 1280 my $conf = PVE::QemuServer::load_config($vmid);
3ea94c60
DM
1281
1282 # try to detect errors early
a5ed42d3
DM
1283
1284 PVE::QemuServer::check_lock($conf);
1285
3ea94c60
DM
1286 if (PVE::QemuServer::check_running($vmid)) {
1287 die "cant migrate running VM without --online\n"
1288 if !$param->{online};
1289 }
1290
1291 my $realcmd = sub {
1292 my $upid = shift;
1293
16e903f2 1294 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
3ea94c60
DM
1295 };
1296
1297 my $upid = $rpcenv->fork_worker('qmigrate', $vmid, $user, $realcmd);
1298
1299 return $upid;
1300 }});
1e3baf05 1301
91c94f0a
DM
1302__PACKAGE__->register_method({
1303 name => 'monitor',
1304 path => '{vmid}/monitor',
1305 method => 'POST',
1306 protected => 1,
1307 proxyto => 'node',
1308 description => "Execute Qemu monitor commands.",
1309 parameters => {
1310 additionalProperties => 0,
1311 properties => {
1312 node => get_standard_option('pve-node'),
1313 vmid => get_standard_option('pve-vmid'),
1314 command => {
1315 type => 'string',
1316 description => "The monitor command.",
1317 }
1318 },
1319 },
1320 returns => { type => 'string'},
1321 code => sub {
1322 my ($param) = @_;
1323
1324 my $vmid = $param->{vmid};
1325
1326 my $conf = PVE::QemuServer::load_config ($vmid); # check if VM exists
1327
1328 my $res = '';
1329 eval {
1330 $res = PVE::QemuServer::vm_monitor_command($vmid, $param->{command});
1331 };
1332 $res = "ERROR: $@" if $@;
1333
1334 return $res;
1335 }});
1336
1e3baf05 13371;