]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
turn off alarm timer after accepting a vnc connection
[qemu-server.git] / PVE / API2 / Qemu.pm
1 package PVE::API2::Qemu;
2
3 use strict;
4 use warnings;
5 use Cwd 'abs_path';
6 use Net::SSLeay;
7 use UUID;
8 use POSIX;
9 use IO::Socket::IP;
10
11 use PVE::Cluster qw (cfs_read_file cfs_write_file);;
12 use PVE::SafeSyslog;
13 use PVE::Tools qw(extract_param);
14 use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
15 use PVE::Storage;
16 use PVE::JSONSchema qw(get_standard_option);
17 use PVE::RESTHandler;
18 use PVE::QemuConfig;
19 use PVE::QemuServer;
20 use PVE::QemuMigrate;
21 use PVE::RPCEnvironment;
22 use PVE::AccessControl;
23 use PVE::INotify;
24 use PVE::Network;
25 use PVE::Firewall;
26 use PVE::API2::Firewall::VM;
27
28 BEGIN {
29 if (!$ENV{PVE_GENERATING_DOCS}) {
30 require PVE::HA::Env::PVE2;
31 import PVE::HA::Env::PVE2;
32 require PVE::HA::Config;
33 import PVE::HA::Config;
34 }
35 }
36
37 use Data::Dumper; # fixme: remove
38
39 use base qw(PVE::RESTHandler);
40
41 my $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.";
42
43 my $resolve_cdrom_alias = sub {
44 my $param = shift;
45
46 if (my $value = $param->{cdrom}) {
47 $value .= ",media=cdrom" if $value !~ m/media=/;
48 $param->{ide2} = $value;
49 delete $param->{cdrom};
50 }
51 };
52
53 my $check_storage_access = sub {
54 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
55
56 PVE::QemuServer::foreach_drive($settings, sub {
57 my ($ds, $drive) = @_;
58
59 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
60
61 my $volid = $drive->{file};
62
63 if (!$volid || $volid eq 'none') {
64 # nothing to check
65 } elsif ($isCDROM && ($volid eq 'cdrom')) {
66 $rpcenv->check($authuser, "/", ['Sys.Console']);
67 } elsif (!$isCDROM && ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/)) {
68 my ($storeid, $size) = ($2 || $default_storage, $3);
69 die "no storage ID specified (and no default storage)\n" if !$storeid;
70 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
71 } else {
72 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
73 }
74 });
75 };
76
77 my $check_storage_access_clone = sub {
78 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
79
80 my $sharedvm = 1;
81
82 PVE::QemuServer::foreach_drive($conf, sub {
83 my ($ds, $drive) = @_;
84
85 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
86
87 my $volid = $drive->{file};
88
89 return if !$volid || $volid eq 'none';
90
91 if ($isCDROM) {
92 if ($volid eq 'cdrom') {
93 $rpcenv->check($authuser, "/", ['Sys.Console']);
94 } else {
95 # we simply allow access
96 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
97 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
98 $sharedvm = 0 if !$scfg->{shared};
99
100 }
101 } else {
102 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
103 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
104 $sharedvm = 0 if !$scfg->{shared};
105
106 $sid = $storage if $storage;
107 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
108 }
109 });
110
111 return $sharedvm;
112 };
113
114 # Note: $pool is only needed when creating a VM, because pool permissions
115 # are automatically inherited if VM already exists inside a pool.
116 my $create_disks = sub {
117 my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
118
119 my $vollist = [];
120
121 my $res = {};
122 PVE::QemuServer::foreach_drive($settings, sub {
123 my ($ds, $disk) = @_;
124
125 my $volid = $disk->{file};
126
127 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
128 delete $disk->{size};
129 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
130 } elsif ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/) {
131 my ($storeid, $size) = ($2 || $default_storage, $3);
132 die "no storage ID specified (and no default storage)\n" if !$storeid;
133 my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
134 my $fmt = $disk->{format} || $defformat;
135
136 my $volid;
137 if ($ds eq 'efidisk0') {
138 # handle efidisk
139 my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd';
140 die "uefi vars image not found\n" if ! -f $ovmfvars;
141 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
142 $fmt, undef, 128);
143 $disk->{file} = $volid;
144 $disk->{size} = 128*1024;
145 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
146 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
147 my $qemufmt = PVE::QemuServer::qemu_img_format($scfg, $volname);
148 my $path = PVE::Storage::path($storecfg, $volid);
149 my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt];
150 push @$efidiskcmd, $ovmfvars;
151 push @$efidiskcmd, $path;
152
153 PVE::Storage::activate_volumes($storecfg, [$volid]);
154
155 eval { PVE::Tools::run_command($efidiskcmd); };
156 my $err = $@;
157 die "Copying of EFI Vars image failed: $err" if $err;
158 } else {
159 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
160 $fmt, undef, $size*1024*1024);
161 $disk->{file} = $volid;
162 $disk->{size} = $size*1024*1024*1024;
163 }
164 push @$vollist, $volid;
165 delete $disk->{format}; # no longer needed
166 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
167 } else {
168
169 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
170
171 my $volid_is_new = 1;
172
173 if ($conf->{$ds}) {
174 my $olddrive = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
175 $volid_is_new = undef if $olddrive->{file} && $olddrive->{file} eq $volid;
176 }
177
178 if ($volid_is_new) {
179
180 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
181
182 PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid;
183
184 my $size = PVE::Storage::volume_size_info($storecfg, $volid);
185
186 die "volume $volid does not exists\n" if !$size;
187
188 $disk->{size} = $size;
189 }
190
191 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
192 }
193 });
194
195 # free allocated images on error
196 if (my $err = $@) {
197 syslog('err', "VM $vmid creating disks failed");
198 foreach my $volid (@$vollist) {
199 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
200 warn $@ if $@;
201 }
202 die $err;
203 }
204
205 # modify vm config if everything went well
206 foreach my $ds (keys %$res) {
207 $conf->{$ds} = $res->{$ds};
208 }
209
210 return $vollist;
211 };
212
213 my $cpuoptions = {
214 'cores' => 1,
215 'cpu' => 1,
216 'cpulimit' => 1,
217 'cpuunits' => 1,
218 'numa' => 1,
219 'smp' => 1,
220 'sockets' => 1,
221 'vcpus' => 1,
222 };
223
224 my $memoryoptions = {
225 'memory' => 1,
226 'balloon' => 1,
227 'shares' => 1,
228 };
229
230 my $hwtypeoptions = {
231 'acpi' => 1,
232 'hotplug' => 1,
233 'kvm' => 1,
234 'machine' => 1,
235 'scsihw' => 1,
236 'smbios1' => 1,
237 'tablet' => 1,
238 'vga' => 1,
239 'watchdog' => 1,
240 };
241
242 my $generaloptions = {
243 'agent' => 1,
244 'autostart' => 1,
245 'bios' => 1,
246 'description' => 1,
247 'keyboard' => 1,
248 'localtime' => 1,
249 'migrate_downtime' => 1,
250 'migrate_speed' => 1,
251 'name' => 1,
252 'onboot' => 1,
253 'ostype' => 1,
254 'protection' => 1,
255 'reboot' => 1,
256 'startdate' => 1,
257 'startup' => 1,
258 'tdf' => 1,
259 'template' => 1,
260 };
261
262 my $vmpoweroptions = {
263 'freeze' => 1,
264 };
265
266 my $diskoptions = {
267 'boot' => 1,
268 'bootdisk' => 1,
269 };
270
271 my $check_vm_modify_config_perm = sub {
272 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
273
274 return 1 if $authuser eq 'root@pam';
275
276 foreach my $opt (@$key_list) {
277 # disk checks need to be done somewhere else
278 next if PVE::QemuServer::is_valid_drivename($opt);
279 next if $opt eq 'cdrom';
280 next if $opt =~ m/^unused\d+$/;
281
282 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
283 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
284 } elsif ($memoryoptions->{$opt}) {
285 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
286 } elsif ($hwtypeoptions->{$opt}) {
287 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
288 } elsif ($generaloptions->{$opt}) {
289 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
290 # special case for startup since it changes host behaviour
291 if ($opt eq 'startup') {
292 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
293 }
294 } elsif ($vmpoweroptions->{$opt}) {
295 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
296 } elsif ($diskoptions->{$opt}) {
297 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
298 } elsif ($opt =~ m/^net\d+$/) {
299 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
300 } else {
301 # catches usb\d+, hostpci\d+, args, lock, etc.
302 # new options will be checked here
303 die "only root can set '$opt' config\n";
304 }
305 }
306
307 return 1;
308 };
309
310 __PACKAGE__->register_method({
311 name => 'vmlist',
312 path => '',
313 method => 'GET',
314 description => "Virtual machine index (per node).",
315 permissions => {
316 description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
317 user => 'all',
318 },
319 proxyto => 'node',
320 protected => 1, # qemu pid files are only readable by root
321 parameters => {
322 additionalProperties => 0,
323 properties => {
324 node => get_standard_option('pve-node'),
325 full => {
326 type => 'boolean',
327 optional => 1,
328 description => "Determine the full status of active VMs.",
329 },
330 },
331 },
332 returns => {
333 type => 'array',
334 items => {
335 type => "object",
336 properties => {},
337 },
338 links => [ { rel => 'child', href => "{vmid}" } ],
339 },
340 code => sub {
341 my ($param) = @_;
342
343 my $rpcenv = PVE::RPCEnvironment::get();
344 my $authuser = $rpcenv->get_user();
345
346 my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full});
347
348 my $res = [];
349 foreach my $vmid (keys %$vmstatus) {
350 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
351
352 my $data = $vmstatus->{$vmid};
353 $data->{vmid} = int($vmid);
354 push @$res, $data;
355 }
356
357 return $res;
358 }});
359
360
361
362 __PACKAGE__->register_method({
363 name => 'create_vm',
364 path => '',
365 method => 'POST',
366 description => "Create or restore a virtual machine.",
367 permissions => {
368 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
369 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
370 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
371 user => 'all', # check inside
372 },
373 protected => 1,
374 proxyto => 'node',
375 parameters => {
376 additionalProperties => 0,
377 properties => PVE::QemuServer::json_config_properties(
378 {
379 node => get_standard_option('pve-node'),
380 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
381 archive => {
382 description => "The backup file.",
383 type => 'string',
384 optional => 1,
385 maxLength => 255,
386 completion => \&PVE::QemuServer::complete_backup_archives,
387 },
388 storage => get_standard_option('pve-storage-id', {
389 description => "Default storage.",
390 optional => 1,
391 completion => \&PVE::QemuServer::complete_storage,
392 }),
393 force => {
394 optional => 1,
395 type => 'boolean',
396 description => "Allow to overwrite existing VM.",
397 requires => 'archive',
398 },
399 unique => {
400 optional => 1,
401 type => 'boolean',
402 description => "Assign a unique random ethernet address.",
403 requires => 'archive',
404 },
405 pool => {
406 optional => 1,
407 type => 'string', format => 'pve-poolid',
408 description => "Add the VM to the specified pool.",
409 },
410 }),
411 },
412 returns => {
413 type => 'string',
414 },
415 code => sub {
416 my ($param) = @_;
417
418 my $rpcenv = PVE::RPCEnvironment::get();
419
420 my $authuser = $rpcenv->get_user();
421
422 my $node = extract_param($param, 'node');
423
424 my $vmid = extract_param($param, 'vmid');
425
426 my $archive = extract_param($param, 'archive');
427
428 my $storage = extract_param($param, 'storage');
429
430 my $force = extract_param($param, 'force');
431
432 my $unique = extract_param($param, 'unique');
433
434 my $pool = extract_param($param, 'pool');
435
436 my $filename = PVE::QemuConfig->config_file($vmid);
437
438 my $storecfg = PVE::Storage::config();
439
440 PVE::Cluster::check_cfs_quorum();
441
442 if (defined($pool)) {
443 $rpcenv->check_pool_exist($pool);
444 }
445
446 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
447 if defined($storage);
448
449 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
450 # OK
451 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
452 # OK
453 } elsif ($archive && $force && (-f $filename) &&
454 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
455 # OK: user has VM.Backup permissions, and want to restore an existing VM
456 } else {
457 raise_perm_exc();
458 }
459
460 if (!$archive) {
461 &$resolve_cdrom_alias($param);
462
463 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
464
465 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
466
467 foreach my $opt (keys %$param) {
468 if (PVE::QemuServer::is_valid_drivename($opt)) {
469 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
470 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
471
472 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
473 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
474 }
475 }
476
477 PVE::QemuServer::add_random_macs($param);
478 } else {
479 my $keystr = join(' ', keys %$param);
480 raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr;
481
482 if ($archive eq '-') {
483 die "pipe requires cli environment\n"
484 if $rpcenv->{type} ne 'cli';
485 } else {
486 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $archive);
487 $archive = PVE::Storage::abs_filesystem_path($storecfg, $archive);
488 }
489 }
490
491 my $restorefn = sub {
492 my $vmlist = PVE::Cluster::get_vmlist();
493 if ($vmlist->{ids}->{$vmid}) {
494 my $current_node = $vmlist->{ids}->{$vmid}->{node};
495 if ($current_node eq $node) {
496 my $conf = PVE::QemuConfig->load_config($vmid);
497
498 PVE::QemuConfig->check_protection($conf, "unable to restore VM $vmid");
499
500 die "unable to restore vm $vmid - config file already exists\n"
501 if !$force;
502
503 die "unable to restore vm $vmid - vm is running\n"
504 if PVE::QemuServer::check_running($vmid);
505
506 die "unable to restore vm $vmid - vm is a template\n"
507 if PVE::QemuConfig->is_template($conf);
508
509 } else {
510 die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n";
511 }
512 }
513
514 my $realcmd = sub {
515 PVE::QemuServer::restore_archive($archive, $vmid, $authuser, {
516 storage => $storage,
517 pool => $pool,
518 unique => $unique });
519
520 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
521 };
522
523 return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd);
524 };
525
526 my $createfn = sub {
527
528 # test after locking
529 PVE::Cluster::check_vmid_unused($vmid);
530
531 my $realcmd = sub {
532
533 my $vollist = [];
534
535 my $conf = $param;
536
537 eval {
538
539 $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
540
541 # try to be smart about bootdisk
542 my @disks = PVE::QemuServer::valid_drive_names();
543 my $firstdisk;
544 foreach my $ds (reverse @disks) {
545 next if !$conf->{$ds};
546 my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
547 next if PVE::QemuServer::drive_is_cdrom($disk);
548 $firstdisk = $ds;
549 }
550
551 if (!$conf->{bootdisk} && $firstdisk) {
552 $conf->{bootdisk} = $firstdisk;
553 }
554
555 # auto generate uuid if user did not specify smbios1 option
556 if (!$conf->{smbios1}) {
557 my ($uuid, $uuid_str);
558 UUID::generate($uuid);
559 UUID::unparse($uuid, $uuid_str);
560 $conf->{smbios1} = "uuid=$uuid_str";
561 }
562
563 PVE::QemuConfig->write_config($vmid, $conf);
564
565 };
566 my $err = $@;
567
568 if ($err) {
569 foreach my $volid (@$vollist) {
570 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
571 warn $@ if $@;
572 }
573 die "create failed - $err";
574 }
575
576 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
577 };
578
579 return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd);
580 };
581
582 return PVE::QemuConfig->lock_config_full($vmid, 1, $archive ? $restorefn : $createfn);
583 }});
584
585 __PACKAGE__->register_method({
586 name => 'vmdiridx',
587 path => '{vmid}',
588 method => 'GET',
589 proxyto => 'node',
590 description => "Directory index",
591 permissions => {
592 user => 'all',
593 },
594 parameters => {
595 additionalProperties => 0,
596 properties => {
597 node => get_standard_option('pve-node'),
598 vmid => get_standard_option('pve-vmid'),
599 },
600 },
601 returns => {
602 type => 'array',
603 items => {
604 type => "object",
605 properties => {
606 subdir => { type => 'string' },
607 },
608 },
609 links => [ { rel => 'child', href => "{subdir}" } ],
610 },
611 code => sub {
612 my ($param) = @_;
613
614 my $res = [
615 { subdir => 'config' },
616 { subdir => 'pending' },
617 { subdir => 'status' },
618 { subdir => 'unlink' },
619 { subdir => 'vncproxy' },
620 { subdir => 'migrate' },
621 { subdir => 'resize' },
622 { subdir => 'move' },
623 { subdir => 'rrd' },
624 { subdir => 'rrddata' },
625 { subdir => 'monitor' },
626 { subdir => 'agent' },
627 { subdir => 'snapshot' },
628 { subdir => 'spiceproxy' },
629 { subdir => 'sendkey' },
630 { subdir => 'firewall' },
631 ];
632
633 return $res;
634 }});
635
636 __PACKAGE__->register_method ({
637 subclass => "PVE::API2::Firewall::VM",
638 path => '{vmid}/firewall',
639 });
640
641 __PACKAGE__->register_method({
642 name => 'rrd',
643 path => '{vmid}/rrd',
644 method => 'GET',
645 protected => 1, # fixme: can we avoid that?
646 permissions => {
647 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
648 },
649 description => "Read VM RRD statistics (returns PNG)",
650 parameters => {
651 additionalProperties => 0,
652 properties => {
653 node => get_standard_option('pve-node'),
654 vmid => get_standard_option('pve-vmid'),
655 timeframe => {
656 description => "Specify the time frame you are interested in.",
657 type => 'string',
658 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
659 },
660 ds => {
661 description => "The list of datasources you want to display.",
662 type => 'string', format => 'pve-configid-list',
663 },
664 cf => {
665 description => "The RRD consolidation function",
666 type => 'string',
667 enum => [ 'AVERAGE', 'MAX' ],
668 optional => 1,
669 },
670 },
671 },
672 returns => {
673 type => "object",
674 properties => {
675 filename => { type => 'string' },
676 },
677 },
678 code => sub {
679 my ($param) = @_;
680
681 return PVE::Cluster::create_rrd_graph(
682 "pve2-vm/$param->{vmid}", $param->{timeframe},
683 $param->{ds}, $param->{cf});
684
685 }});
686
687 __PACKAGE__->register_method({
688 name => 'rrddata',
689 path => '{vmid}/rrddata',
690 method => 'GET',
691 protected => 1, # fixme: can we avoid that?
692 permissions => {
693 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
694 },
695 description => "Read VM RRD statistics",
696 parameters => {
697 additionalProperties => 0,
698 properties => {
699 node => get_standard_option('pve-node'),
700 vmid => get_standard_option('pve-vmid'),
701 timeframe => {
702 description => "Specify the time frame you are interested in.",
703 type => 'string',
704 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
705 },
706 cf => {
707 description => "The RRD consolidation function",
708 type => 'string',
709 enum => [ 'AVERAGE', 'MAX' ],
710 optional => 1,
711 },
712 },
713 },
714 returns => {
715 type => "array",
716 items => {
717 type => "object",
718 properties => {},
719 },
720 },
721 code => sub {
722 my ($param) = @_;
723
724 return PVE::Cluster::create_rrd_data(
725 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
726 }});
727
728
729 __PACKAGE__->register_method({
730 name => 'vm_config',
731 path => '{vmid}/config',
732 method => 'GET',
733 proxyto => 'node',
734 description => "Get current virtual machine configuration. This does not include pending configuration changes (see 'pending' API).",
735 permissions => {
736 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
737 },
738 parameters => {
739 additionalProperties => 0,
740 properties => {
741 node => get_standard_option('pve-node'),
742 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
743 current => {
744 description => "Get current values (instead of pending values).",
745 optional => 1,
746 default => 0,
747 type => 'boolean',
748 },
749 },
750 },
751 returns => {
752 type => "object",
753 properties => {
754 digest => {
755 type => 'string',
756 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
757 }
758 },
759 },
760 code => sub {
761 my ($param) = @_;
762
763 my $conf = PVE::QemuConfig->load_config($param->{vmid});
764
765 delete $conf->{snapshots};
766
767 if (!$param->{current}) {
768 foreach my $opt (keys %{$conf->{pending}}) {
769 next if $opt eq 'delete';
770 my $value = $conf->{pending}->{$opt};
771 next if ref($value); # just to be sure
772 $conf->{$opt} = $value;
773 }
774 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
775 foreach my $opt (keys %$pending_delete_hash) {
776 delete $conf->{$opt} if $conf->{$opt};
777 }
778 }
779
780 delete $conf->{pending};
781
782 return $conf;
783 }});
784
785 __PACKAGE__->register_method({
786 name => 'vm_pending',
787 path => '{vmid}/pending',
788 method => 'GET',
789 proxyto => 'node',
790 description => "Get virtual machine configuration, including pending changes.",
791 permissions => {
792 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
793 },
794 parameters => {
795 additionalProperties => 0,
796 properties => {
797 node => get_standard_option('pve-node'),
798 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
799 },
800 },
801 returns => {
802 type => "array",
803 items => {
804 type => "object",
805 properties => {
806 key => {
807 description => "Configuration option name.",
808 type => 'string',
809 },
810 value => {
811 description => "Current value.",
812 type => 'string',
813 optional => 1,
814 },
815 pending => {
816 description => "Pending value.",
817 type => 'string',
818 optional => 1,
819 },
820 delete => {
821 description => "Indicates a pending delete request if present and not 0. " .
822 "The value 2 indicates a force-delete request.",
823 type => 'integer',
824 minimum => 0,
825 maximum => 2,
826 optional => 1,
827 },
828 },
829 },
830 },
831 code => sub {
832 my ($param) = @_;
833
834 my $conf = PVE::QemuConfig->load_config($param->{vmid});
835
836 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
837
838 my $res = [];
839
840 foreach my $opt (keys %$conf) {
841 next if ref($conf->{$opt});
842 my $item = { key => $opt };
843 $item->{value} = $conf->{$opt} if defined($conf->{$opt});
844 $item->{pending} = $conf->{pending}->{$opt} if defined($conf->{pending}->{$opt});
845 $item->{delete} = ($pending_delete_hash->{$opt} ? 2 : 1) if exists $pending_delete_hash->{$opt};
846 push @$res, $item;
847 }
848
849 foreach my $opt (keys %{$conf->{pending}}) {
850 next if $opt eq 'delete';
851 next if ref($conf->{pending}->{$opt}); # just to be sure
852 next if defined($conf->{$opt});
853 my $item = { key => $opt };
854 $item->{pending} = $conf->{pending}->{$opt};
855 push @$res, $item;
856 }
857
858 while (my ($opt, $force) = each %$pending_delete_hash) {
859 next if $conf->{pending}->{$opt}; # just to be sure
860 next if $conf->{$opt};
861 my $item = { key => $opt, delete => ($force ? 2 : 1)};
862 push @$res, $item;
863 }
864
865 return $res;
866 }});
867
868 # POST/PUT {vmid}/config implementation
869 #
870 # The original API used PUT (idempotent) an we assumed that all operations
871 # are fast. But it turned out that almost any configuration change can
872 # involve hot-plug actions, or disk alloc/free. Such actions can take long
873 # time to complete and have side effects (not idempotent).
874 #
875 # The new implementation uses POST and forks a worker process. We added
876 # a new option 'background_delay'. If specified we wait up to
877 # 'background_delay' second for the worker task to complete. It returns null
878 # if the task is finished within that time, else we return the UPID.
879
880 my $update_vm_api = sub {
881 my ($param, $sync) = @_;
882
883 my $rpcenv = PVE::RPCEnvironment::get();
884
885 my $authuser = $rpcenv->get_user();
886
887 my $node = extract_param($param, 'node');
888
889 my $vmid = extract_param($param, 'vmid');
890
891 my $digest = extract_param($param, 'digest');
892
893 my $background_delay = extract_param($param, 'background_delay');
894
895 my @paramarr = (); # used for log message
896 foreach my $key (keys %$param) {
897 push @paramarr, "-$key", $param->{$key};
898 }
899
900 my $skiplock = extract_param($param, 'skiplock');
901 raise_param_exc({ skiplock => "Only root may use this option." })
902 if $skiplock && $authuser ne 'root@pam';
903
904 my $delete_str = extract_param($param, 'delete');
905
906 my $revert_str = extract_param($param, 'revert');
907
908 my $force = extract_param($param, 'force');
909
910 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
911
912 my $storecfg = PVE::Storage::config();
913
914 my $defaults = PVE::QemuServer::load_defaults();
915
916 &$resolve_cdrom_alias($param);
917
918 # now try to verify all parameters
919
920 my $revert = {};
921 foreach my $opt (PVE::Tools::split_list($revert_str)) {
922 if (!PVE::QemuServer::option_exists($opt)) {
923 raise_param_exc({ revert => "unknown option '$opt'" });
924 }
925
926 raise_param_exc({ delete => "you can't use '-$opt' and " .
927 "-revert $opt' at the same time" })
928 if defined($param->{$opt});
929
930 $revert->{$opt} = 1;
931 }
932
933 my @delete = ();
934 foreach my $opt (PVE::Tools::split_list($delete_str)) {
935 $opt = 'ide2' if $opt eq 'cdrom';
936
937 raise_param_exc({ delete => "you can't use '-$opt' and " .
938 "-delete $opt' at the same time" })
939 if defined($param->{$opt});
940
941 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
942 "-revert $opt' at the same time" })
943 if $revert->{$opt};
944
945 if (!PVE::QemuServer::option_exists($opt)) {
946 raise_param_exc({ delete => "unknown option '$opt'" });
947 }
948
949 push @delete, $opt;
950 }
951
952 foreach my $opt (keys %$param) {
953 if (PVE::QemuServer::is_valid_drivename($opt)) {
954 # cleanup drive path
955 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
956 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
957 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
958 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
959 } elsif ($opt =~ m/^net(\d+)$/) {
960 # add macaddr
961 my $net = PVE::QemuServer::parse_net($param->{$opt});
962 $param->{$opt} = PVE::QemuServer::print_net($net);
963 }
964 }
965
966 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
967
968 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
969
970 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
971
972 my $updatefn = sub {
973
974 my $conf = PVE::QemuConfig->load_config($vmid);
975
976 die "checksum missmatch (file change by other user?)\n"
977 if $digest && $digest ne $conf->{digest};
978
979 PVE::QemuConfig->check_lock($conf) if !$skiplock;
980
981 foreach my $opt (keys %$revert) {
982 if (defined($conf->{$opt})) {
983 $param->{$opt} = $conf->{$opt};
984 } elsif (defined($conf->{pending}->{$opt})) {
985 push @delete, $opt;
986 }
987 }
988
989 if ($param->{memory} || defined($param->{balloon})) {
990 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
991 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
992
993 die "balloon value too large (must be smaller than assigned memory)\n"
994 if $balloon && $balloon > $maxmem;
995 }
996
997 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
998
999 my $worker = sub {
1000
1001 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1002
1003 # write updates to pending section
1004
1005 my $modified = {}; # record what $option we modify
1006
1007 foreach my $opt (@delete) {
1008 $modified->{$opt} = 1;
1009 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1010 if (!defined($conf->{$opt})) {
1011 warn "cannot delete '$opt' - not set in current configuration!\n";
1012 $modified->{$opt} = 0;
1013 next;
1014 }
1015
1016 if ($opt =~ m/^unused/) {
1017 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
1018 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
1019 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1020 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1021 delete $conf->{$opt};
1022 PVE::QemuConfig->write_config($vmid, $conf);
1023 }
1024 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1025 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
1026 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1027 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1028 if defined($conf->{pending}->{$opt});
1029 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1030 PVE::QemuConfig->write_config($vmid, $conf);
1031 } else {
1032 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1033 PVE::QemuConfig->write_config($vmid, $conf);
1034 }
1035 }
1036
1037 foreach my $opt (keys %$param) { # add/change
1038 $modified->{$opt} = 1;
1039 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1040 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1041
1042 if (PVE::QemuServer::is_valid_drivename($opt)) {
1043 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1044 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1045 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1046 } else {
1047 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1048 }
1049 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1050 if defined($conf->{pending}->{$opt});
1051
1052 &$create_disks($rpcenv, $authuser, $conf->{pending}, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1053 } else {
1054 $conf->{pending}->{$opt} = $param->{$opt};
1055 }
1056 PVE::QemuServer::vmconfig_undelete_pending_option($conf, $opt);
1057 PVE::QemuConfig->write_config($vmid, $conf);
1058 }
1059
1060 # remove pending changes when nothing changed
1061 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1062 my $changes = PVE::QemuServer::vmconfig_cleanup_pending($conf);
1063 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
1064
1065 return if !scalar(keys %{$conf->{pending}});
1066
1067 my $running = PVE::QemuServer::check_running($vmid);
1068
1069 # apply pending changes
1070
1071 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1072
1073 if ($running) {
1074 my $errors = {};
1075 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
1076 raise_param_exc($errors) if scalar(keys %$errors);
1077 } else {
1078 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $running);
1079 }
1080
1081 return;
1082 };
1083
1084 if ($sync) {
1085 &$worker();
1086 return undef;
1087 } else {
1088 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1089
1090 if ($background_delay) {
1091
1092 # Note: It would be better to do that in the Event based HTTPServer
1093 # to avoid blocking call to sleep.
1094
1095 my $end_time = time() + $background_delay;
1096
1097 my $task = PVE::Tools::upid_decode($upid);
1098
1099 my $running = 1;
1100 while (time() < $end_time) {
1101 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1102 last if !$running;
1103 sleep(1); # this gets interrupted when child process ends
1104 }
1105
1106 if (!$running) {
1107 my $status = PVE::Tools::upid_read_status($upid);
1108 return undef if $status eq 'OK';
1109 die $status;
1110 }
1111 }
1112
1113 return $upid;
1114 }
1115 };
1116
1117 return PVE::QemuConfig->lock_config($vmid, $updatefn);
1118 };
1119
1120 my $vm_config_perm_list = [
1121 'VM.Config.Disk',
1122 'VM.Config.CDROM',
1123 'VM.Config.CPU',
1124 'VM.Config.Memory',
1125 'VM.Config.Network',
1126 'VM.Config.HWType',
1127 'VM.Config.Options',
1128 ];
1129
1130 __PACKAGE__->register_method({
1131 name => 'update_vm_async',
1132 path => '{vmid}/config',
1133 method => 'POST',
1134 protected => 1,
1135 proxyto => 'node',
1136 description => "Set virtual machine options (asynchrounous API).",
1137 permissions => {
1138 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1139 },
1140 parameters => {
1141 additionalProperties => 0,
1142 properties => PVE::QemuServer::json_config_properties(
1143 {
1144 node => get_standard_option('pve-node'),
1145 vmid => get_standard_option('pve-vmid'),
1146 skiplock => get_standard_option('skiplock'),
1147 delete => {
1148 type => 'string', format => 'pve-configid-list',
1149 description => "A list of settings you want to delete.",
1150 optional => 1,
1151 },
1152 revert => {
1153 type => 'string', format => 'pve-configid-list',
1154 description => "Revert a pending change.",
1155 optional => 1,
1156 },
1157 force => {
1158 type => 'boolean',
1159 description => $opt_force_description,
1160 optional => 1,
1161 requires => 'delete',
1162 },
1163 digest => {
1164 type => 'string',
1165 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1166 maxLength => 40,
1167 optional => 1,
1168 },
1169 background_delay => {
1170 type => 'integer',
1171 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1172 minimum => 1,
1173 maximum => 30,
1174 optional => 1,
1175 },
1176 }),
1177 },
1178 returns => {
1179 type => 'string',
1180 optional => 1,
1181 },
1182 code => $update_vm_api,
1183 });
1184
1185 __PACKAGE__->register_method({
1186 name => 'update_vm',
1187 path => '{vmid}/config',
1188 method => 'PUT',
1189 protected => 1,
1190 proxyto => 'node',
1191 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1192 permissions => {
1193 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1194 },
1195 parameters => {
1196 additionalProperties => 0,
1197 properties => PVE::QemuServer::json_config_properties(
1198 {
1199 node => get_standard_option('pve-node'),
1200 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1201 skiplock => get_standard_option('skiplock'),
1202 delete => {
1203 type => 'string', format => 'pve-configid-list',
1204 description => "A list of settings you want to delete.",
1205 optional => 1,
1206 },
1207 revert => {
1208 type => 'string', format => 'pve-configid-list',
1209 description => "Revert a pending change.",
1210 optional => 1,
1211 },
1212 force => {
1213 type => 'boolean',
1214 description => $opt_force_description,
1215 optional => 1,
1216 requires => 'delete',
1217 },
1218 digest => {
1219 type => 'string',
1220 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1221 maxLength => 40,
1222 optional => 1,
1223 },
1224 }),
1225 },
1226 returns => { type => 'null' },
1227 code => sub {
1228 my ($param) = @_;
1229 &$update_vm_api($param, 1);
1230 return undef;
1231 }
1232 });
1233
1234
1235 __PACKAGE__->register_method({
1236 name => 'destroy_vm',
1237 path => '{vmid}',
1238 method => 'DELETE',
1239 protected => 1,
1240 proxyto => 'node',
1241 description => "Destroy the vm (also delete all used/owned volumes).",
1242 permissions => {
1243 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1244 },
1245 parameters => {
1246 additionalProperties => 0,
1247 properties => {
1248 node => get_standard_option('pve-node'),
1249 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1250 skiplock => get_standard_option('skiplock'),
1251 },
1252 },
1253 returns => {
1254 type => 'string',
1255 },
1256 code => sub {
1257 my ($param) = @_;
1258
1259 my $rpcenv = PVE::RPCEnvironment::get();
1260
1261 my $authuser = $rpcenv->get_user();
1262
1263 my $vmid = $param->{vmid};
1264
1265 my $skiplock = $param->{skiplock};
1266 raise_param_exc({ skiplock => "Only root may use this option." })
1267 if $skiplock && $authuser ne 'root@pam';
1268
1269 # test if VM exists
1270 my $conf = PVE::QemuConfig->load_config($vmid);
1271
1272 my $storecfg = PVE::Storage::config();
1273
1274 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
1275
1276 die "unable to remove VM $vmid - used in HA resources\n"
1277 if PVE::HA::Config::vm_is_ha_managed($vmid);
1278
1279 # early tests (repeat after locking)
1280 die "VM $vmid is running - destroy failed\n"
1281 if PVE::QemuServer::check_running($vmid);
1282
1283 my $realcmd = sub {
1284 my $upid = shift;
1285
1286 syslog('info', "destroy VM $vmid: $upid\n");
1287
1288 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
1289
1290 PVE::AccessControl::remove_vm_access($vmid);
1291
1292 PVE::Firewall::remove_vmfw_conf($vmid);
1293 };
1294
1295 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1296 }});
1297
1298 __PACKAGE__->register_method({
1299 name => 'unlink',
1300 path => '{vmid}/unlink',
1301 method => 'PUT',
1302 protected => 1,
1303 proxyto => 'node',
1304 description => "Unlink/delete disk images.",
1305 permissions => {
1306 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1307 },
1308 parameters => {
1309 additionalProperties => 0,
1310 properties => {
1311 node => get_standard_option('pve-node'),
1312 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1313 idlist => {
1314 type => 'string', format => 'pve-configid-list',
1315 description => "A list of disk IDs you want to delete.",
1316 },
1317 force => {
1318 type => 'boolean',
1319 description => $opt_force_description,
1320 optional => 1,
1321 },
1322 },
1323 },
1324 returns => { type => 'null'},
1325 code => sub {
1326 my ($param) = @_;
1327
1328 $param->{delete} = extract_param($param, 'idlist');
1329
1330 __PACKAGE__->update_vm($param);
1331
1332 return undef;
1333 }});
1334
1335 my $sslcert;
1336
1337 __PACKAGE__->register_method({
1338 name => 'vncproxy',
1339 path => '{vmid}/vncproxy',
1340 method => 'POST',
1341 protected => 1,
1342 permissions => {
1343 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1344 },
1345 description => "Creates a TCP VNC proxy connections.",
1346 parameters => {
1347 additionalProperties => 0,
1348 properties => {
1349 node => get_standard_option('pve-node'),
1350 vmid => get_standard_option('pve-vmid'),
1351 websocket => {
1352 optional => 1,
1353 type => 'boolean',
1354 description => "starts websockify instead of vncproxy",
1355 },
1356 },
1357 },
1358 returns => {
1359 additionalProperties => 0,
1360 properties => {
1361 user => { type => 'string' },
1362 ticket => { type => 'string' },
1363 cert => { type => 'string' },
1364 port => { type => 'integer' },
1365 upid => { type => 'string' },
1366 },
1367 },
1368 code => sub {
1369 my ($param) = @_;
1370
1371 my $rpcenv = PVE::RPCEnvironment::get();
1372
1373 my $authuser = $rpcenv->get_user();
1374
1375 my $vmid = $param->{vmid};
1376 my $node = $param->{node};
1377 my $websocket = $param->{websocket};
1378
1379 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1380
1381 my $authpath = "/vms/$vmid";
1382
1383 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1384
1385 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1386 if !$sslcert;
1387
1388 my ($remip, $family);
1389 my $remcmd = [];
1390
1391 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1392 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
1393 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1394 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1395 } else {
1396 $family = PVE::Tools::get_host_address_family($node);
1397 }
1398
1399 my $port = PVE::Tools::next_vnc_port($family);
1400
1401 my $timeout = 10;
1402
1403 my $realcmd = sub {
1404 my $upid = shift;
1405
1406 syslog('info', "starting vnc proxy $upid\n");
1407
1408 my $cmd;
1409
1410 if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) {
1411
1412 die "Websocket mode is not supported in vga serial mode!" if $websocket;
1413
1414 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ];
1415 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1416 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1417 '-timeout', $timeout, '-authpath', $authpath,
1418 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1419 PVE::Tools::run_command($cmd);
1420 } else {
1421
1422 $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
1423
1424 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1425
1426 my $sock = IO::Socket::IP->new(
1427 Listen => 1,
1428 LocalPort => $port,
1429 Proto => 'tcp',
1430 GetAddrInfoFlags => 0,
1431 ) or die "failed to create socket: $!\n";
1432 # Inside the worker we shouldn't have any previous alarms
1433 # running anyway...:
1434 alarm(0);
1435 local $SIG{ALRM} = sub { die "connection timed out\n" };
1436 alarm $timeout;
1437 accept(my $cli, $sock) or die "connection failed: $!\n";
1438 alarm(0);
1439 close($sock);
1440 if (PVE::Tools::run_command($cmd,
1441 output => '>&'.fileno($cli),
1442 input => '<&'.fileno($cli),
1443 noerr => 1) != 0)
1444 {
1445 die "Failed to run vncproxy.\n";
1446 }
1447 }
1448
1449 return;
1450 };
1451
1452 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
1453
1454 PVE::Tools::wait_for_vnc_port($port);
1455
1456 return {
1457 user => $authuser,
1458 ticket => $ticket,
1459 port => $port,
1460 upid => $upid,
1461 cert => $sslcert,
1462 };
1463 }});
1464
1465 __PACKAGE__->register_method({
1466 name => 'vncwebsocket',
1467 path => '{vmid}/vncwebsocket',
1468 method => 'GET',
1469 permissions => {
1470 description => "You also need to pass a valid ticket (vncticket).",
1471 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1472 },
1473 description => "Opens a weksocket for VNC traffic.",
1474 parameters => {
1475 additionalProperties => 0,
1476 properties => {
1477 node => get_standard_option('pve-node'),
1478 vmid => get_standard_option('pve-vmid'),
1479 vncticket => {
1480 description => "Ticket from previous call to vncproxy.",
1481 type => 'string',
1482 maxLength => 512,
1483 },
1484 port => {
1485 description => "Port number returned by previous vncproxy call.",
1486 type => 'integer',
1487 minimum => 5900,
1488 maximum => 5999,
1489 },
1490 },
1491 },
1492 returns => {
1493 type => "object",
1494 properties => {
1495 port => { type => 'string' },
1496 },
1497 },
1498 code => sub {
1499 my ($param) = @_;
1500
1501 my $rpcenv = PVE::RPCEnvironment::get();
1502
1503 my $authuser = $rpcenv->get_user();
1504
1505 my $vmid = $param->{vmid};
1506 my $node = $param->{node};
1507
1508 my $authpath = "/vms/$vmid";
1509
1510 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1511
1512 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
1513
1514 # Note: VNC ports are acessible from outside, so we do not gain any
1515 # security if we verify that $param->{port} belongs to VM $vmid. This
1516 # check is done by verifying the VNC ticket (inside VNC protocol).
1517
1518 my $port = $param->{port};
1519
1520 return { port => $port };
1521 }});
1522
1523 __PACKAGE__->register_method({
1524 name => 'spiceproxy',
1525 path => '{vmid}/spiceproxy',
1526 method => 'POST',
1527 protected => 1,
1528 proxyto => 'node',
1529 permissions => {
1530 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1531 },
1532 description => "Returns a SPICE configuration to connect to the VM.",
1533 parameters => {
1534 additionalProperties => 0,
1535 properties => {
1536 node => get_standard_option('pve-node'),
1537 vmid => get_standard_option('pve-vmid'),
1538 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1539 },
1540 },
1541 returns => get_standard_option('remote-viewer-config'),
1542 code => sub {
1543 my ($param) = @_;
1544
1545 my $rpcenv = PVE::RPCEnvironment::get();
1546
1547 my $authuser = $rpcenv->get_user();
1548
1549 my $vmid = $param->{vmid};
1550 my $node = $param->{node};
1551 my $proxy = $param->{proxy};
1552
1553 my $conf = PVE::QemuConfig->load_config($vmid, $node);
1554 my $title = "VM $vmid";
1555 $title .= " - ". $conf->{name} if $conf->{name};
1556
1557 my $port = PVE::QemuServer::spice_port($vmid);
1558
1559 my ($ticket, undef, $remote_viewer_config) =
1560 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
1561
1562 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1563 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1564
1565 return $remote_viewer_config;
1566 }});
1567
1568 __PACKAGE__->register_method({
1569 name => 'vmcmdidx',
1570 path => '{vmid}/status',
1571 method => 'GET',
1572 proxyto => 'node',
1573 description => "Directory index",
1574 permissions => {
1575 user => 'all',
1576 },
1577 parameters => {
1578 additionalProperties => 0,
1579 properties => {
1580 node => get_standard_option('pve-node'),
1581 vmid => get_standard_option('pve-vmid'),
1582 },
1583 },
1584 returns => {
1585 type => 'array',
1586 items => {
1587 type => "object",
1588 properties => {
1589 subdir => { type => 'string' },
1590 },
1591 },
1592 links => [ { rel => 'child', href => "{subdir}" } ],
1593 },
1594 code => sub {
1595 my ($param) = @_;
1596
1597 # test if VM exists
1598 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1599
1600 my $res = [
1601 { subdir => 'current' },
1602 { subdir => 'start' },
1603 { subdir => 'stop' },
1604 ];
1605
1606 return $res;
1607 }});
1608
1609 __PACKAGE__->register_method({
1610 name => 'vm_status',
1611 path => '{vmid}/status/current',
1612 method => 'GET',
1613 proxyto => 'node',
1614 protected => 1, # qemu pid files are only readable by root
1615 description => "Get virtual machine status.",
1616 permissions => {
1617 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1618 },
1619 parameters => {
1620 additionalProperties => 0,
1621 properties => {
1622 node => get_standard_option('pve-node'),
1623 vmid => get_standard_option('pve-vmid'),
1624 },
1625 },
1626 returns => { type => 'object' },
1627 code => sub {
1628 my ($param) = @_;
1629
1630 # test if VM exists
1631 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1632
1633 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1634 my $status = $vmstatus->{$param->{vmid}};
1635
1636 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
1637
1638 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1639
1640 return $status;
1641 }});
1642
1643 __PACKAGE__->register_method({
1644 name => 'vm_start',
1645 path => '{vmid}/status/start',
1646 method => 'POST',
1647 protected => 1,
1648 proxyto => 'node',
1649 description => "Start virtual machine.",
1650 permissions => {
1651 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1652 },
1653 parameters => {
1654 additionalProperties => 0,
1655 properties => {
1656 node => get_standard_option('pve-node'),
1657 vmid => get_standard_option('pve-vmid',
1658 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1659 skiplock => get_standard_option('skiplock'),
1660 stateuri => get_standard_option('pve-qm-stateuri'),
1661 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1662 migration_type => {
1663 type => 'string',
1664 enum => ['secure', 'insecure'],
1665 description => "Migration traffic is encrypted using an SSH " .
1666 "tunnel by default. On secure, completely private networks " .
1667 "this can be disabled to increase performance.",
1668 optional => 1,
1669 },
1670 migration_network => {
1671 type => 'string', format => 'CIDR',
1672 description => "CIDR of the (sub) network that is used for migration.",
1673 optional => 1,
1674 },
1675 machine => get_standard_option('pve-qm-machine'),
1676 targetstorage => {
1677 description => "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1678 type => 'string',
1679 optional => 1
1680 }
1681 },
1682 },
1683 returns => {
1684 type => 'string',
1685 },
1686 code => sub {
1687 my ($param) = @_;
1688
1689 my $rpcenv = PVE::RPCEnvironment::get();
1690
1691 my $authuser = $rpcenv->get_user();
1692
1693 my $node = extract_param($param, 'node');
1694
1695 my $vmid = extract_param($param, 'vmid');
1696
1697 my $machine = extract_param($param, 'machine');
1698
1699 my $stateuri = extract_param($param, 'stateuri');
1700 raise_param_exc({ stateuri => "Only root may use this option." })
1701 if $stateuri && $authuser ne 'root@pam';
1702
1703 my $skiplock = extract_param($param, 'skiplock');
1704 raise_param_exc({ skiplock => "Only root may use this option." })
1705 if $skiplock && $authuser ne 'root@pam';
1706
1707 my $migratedfrom = extract_param($param, 'migratedfrom');
1708 raise_param_exc({ migratedfrom => "Only root may use this option." })
1709 if $migratedfrom && $authuser ne 'root@pam';
1710
1711 my $migration_type = extract_param($param, 'migration_type');
1712 raise_param_exc({ migration_type => "Only root may use this option." })
1713 if $migration_type && $authuser ne 'root@pam';
1714
1715 my $migration_network = extract_param($param, 'migration_network');
1716 raise_param_exc({ migration_network => "Only root may use this option." })
1717 if $migration_network && $authuser ne 'root@pam';
1718
1719 my $targetstorage = extract_param($param, 'targetstorage');
1720 raise_param_exc({ targetstorage => "Only root may use this option." })
1721 if $targetstorage && $authuser ne 'root@pam';
1722
1723 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
1724 if $targetstorage && !$migratedfrom;
1725
1726 # read spice ticket from STDIN
1727 my $spice_ticket;
1728 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
1729 if (defined(my $line = <>)) {
1730 chomp $line;
1731 $spice_ticket = $line;
1732 }
1733 }
1734
1735 PVE::Cluster::check_cfs_quorum();
1736
1737 my $storecfg = PVE::Storage::config();
1738
1739 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri &&
1740 $rpcenv->{type} ne 'ha') {
1741
1742 my $hacmd = sub {
1743 my $upid = shift;
1744
1745 my $service = "vm:$vmid";
1746
1747 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
1748
1749 print "Executing HA start for VM $vmid\n";
1750
1751 PVE::Tools::run_command($cmd);
1752
1753 return;
1754 };
1755
1756 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1757
1758 } else {
1759
1760 my $realcmd = sub {
1761 my $upid = shift;
1762
1763 syslog('info', "start VM $vmid: $upid\n");
1764
1765 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1766 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
1767
1768 return;
1769 };
1770
1771 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1772 }
1773 }});
1774
1775 __PACKAGE__->register_method({
1776 name => 'vm_stop',
1777 path => '{vmid}/status/stop',
1778 method => 'POST',
1779 protected => 1,
1780 proxyto => 'node',
1781 description => "Stop virtual machine. The qemu process will exit immediately. This" .
1782 "is akin to pulling the power plug of a running computer and may damage the VM data",
1783 permissions => {
1784 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1785 },
1786 parameters => {
1787 additionalProperties => 0,
1788 properties => {
1789 node => get_standard_option('pve-node'),
1790 vmid => get_standard_option('pve-vmid',
1791 { completion => \&PVE::QemuServer::complete_vmid_running }),
1792 skiplock => get_standard_option('skiplock'),
1793 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
1794 timeout => {
1795 description => "Wait maximal timeout seconds.",
1796 type => 'integer',
1797 minimum => 0,
1798 optional => 1,
1799 },
1800 keepActive => {
1801 description => "Do not deactivate storage volumes.",
1802 type => 'boolean',
1803 optional => 1,
1804 default => 0,
1805 }
1806 },
1807 },
1808 returns => {
1809 type => 'string',
1810 },
1811 code => sub {
1812 my ($param) = @_;
1813
1814 my $rpcenv = PVE::RPCEnvironment::get();
1815
1816 my $authuser = $rpcenv->get_user();
1817
1818 my $node = extract_param($param, 'node');
1819
1820 my $vmid = extract_param($param, 'vmid');
1821
1822 my $skiplock = extract_param($param, 'skiplock');
1823 raise_param_exc({ skiplock => "Only root may use this option." })
1824 if $skiplock && $authuser ne 'root@pam';
1825
1826 my $keepActive = extract_param($param, 'keepActive');
1827 raise_param_exc({ keepActive => "Only root may use this option." })
1828 if $keepActive && $authuser ne 'root@pam';
1829
1830 my $migratedfrom = extract_param($param, 'migratedfrom');
1831 raise_param_exc({ migratedfrom => "Only root may use this option." })
1832 if $migratedfrom && $authuser ne 'root@pam';
1833
1834
1835 my $storecfg = PVE::Storage::config();
1836
1837 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
1838
1839 my $hacmd = sub {
1840 my $upid = shift;
1841
1842 my $service = "vm:$vmid";
1843
1844 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
1845
1846 print "Executing HA stop for VM $vmid\n";
1847
1848 PVE::Tools::run_command($cmd);
1849
1850 return;
1851 };
1852
1853 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1854
1855 } else {
1856 my $realcmd = sub {
1857 my $upid = shift;
1858
1859 syslog('info', "stop VM $vmid: $upid\n");
1860
1861 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
1862 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
1863
1864 return;
1865 };
1866
1867 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1868 }
1869 }});
1870
1871 __PACKAGE__->register_method({
1872 name => 'vm_reset',
1873 path => '{vmid}/status/reset',
1874 method => 'POST',
1875 protected => 1,
1876 proxyto => 'node',
1877 description => "Reset virtual machine.",
1878 permissions => {
1879 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1880 },
1881 parameters => {
1882 additionalProperties => 0,
1883 properties => {
1884 node => get_standard_option('pve-node'),
1885 vmid => get_standard_option('pve-vmid',
1886 { completion => \&PVE::QemuServer::complete_vmid_running }),
1887 skiplock => get_standard_option('skiplock'),
1888 },
1889 },
1890 returns => {
1891 type => 'string',
1892 },
1893 code => sub {
1894 my ($param) = @_;
1895
1896 my $rpcenv = PVE::RPCEnvironment::get();
1897
1898 my $authuser = $rpcenv->get_user();
1899
1900 my $node = extract_param($param, 'node');
1901
1902 my $vmid = extract_param($param, 'vmid');
1903
1904 my $skiplock = extract_param($param, 'skiplock');
1905 raise_param_exc({ skiplock => "Only root may use this option." })
1906 if $skiplock && $authuser ne 'root@pam';
1907
1908 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1909
1910 my $realcmd = sub {
1911 my $upid = shift;
1912
1913 PVE::QemuServer::vm_reset($vmid, $skiplock);
1914
1915 return;
1916 };
1917
1918 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1919 }});
1920
1921 __PACKAGE__->register_method({
1922 name => 'vm_shutdown',
1923 path => '{vmid}/status/shutdown',
1924 method => 'POST',
1925 protected => 1,
1926 proxyto => 'node',
1927 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
1928 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
1929 permissions => {
1930 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1931 },
1932 parameters => {
1933 additionalProperties => 0,
1934 properties => {
1935 node => get_standard_option('pve-node'),
1936 vmid => get_standard_option('pve-vmid',
1937 { completion => \&PVE::QemuServer::complete_vmid_running }),
1938 skiplock => get_standard_option('skiplock'),
1939 timeout => {
1940 description => "Wait maximal timeout seconds.",
1941 type => 'integer',
1942 minimum => 0,
1943 optional => 1,
1944 },
1945 forceStop => {
1946 description => "Make sure the VM stops.",
1947 type => 'boolean',
1948 optional => 1,
1949 default => 0,
1950 },
1951 keepActive => {
1952 description => "Do not deactivate storage volumes.",
1953 type => 'boolean',
1954 optional => 1,
1955 default => 0,
1956 }
1957 },
1958 },
1959 returns => {
1960 type => 'string',
1961 },
1962 code => sub {
1963 my ($param) = @_;
1964
1965 my $rpcenv = PVE::RPCEnvironment::get();
1966
1967 my $authuser = $rpcenv->get_user();
1968
1969 my $node = extract_param($param, 'node');
1970
1971 my $vmid = extract_param($param, 'vmid');
1972
1973 my $skiplock = extract_param($param, 'skiplock');
1974 raise_param_exc({ skiplock => "Only root may use this option." })
1975 if $skiplock && $authuser ne 'root@pam';
1976
1977 my $keepActive = extract_param($param, 'keepActive');
1978 raise_param_exc({ keepActive => "Only root may use this option." })
1979 if $keepActive && $authuser ne 'root@pam';
1980
1981 my $storecfg = PVE::Storage::config();
1982
1983 my $shutdown = 1;
1984
1985 # if vm is paused, do not shutdown (but stop if forceStop = 1)
1986 # otherwise, we will infer a shutdown command, but run into the timeout,
1987 # then when the vm is resumed, it will instantly shutdown
1988 #
1989 # checking the qmp status here to get feedback to the gui/cli/api
1990 # and the status query should not take too long
1991 my $qmpstatus;
1992 eval {
1993 $qmpstatus = PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
1994 };
1995 my $err = $@ if $@;
1996
1997 if (!$err && $qmpstatus->{status} eq "paused") {
1998 if ($param->{forceStop}) {
1999 warn "VM is paused - stop instead of shutdown\n";
2000 $shutdown = 0;
2001 } else {
2002 die "VM is paused - cannot shutdown\n";
2003 }
2004 }
2005
2006 if (PVE::HA::Config::vm_is_ha_managed($vmid) &&
2007 ($rpcenv->{type} ne 'ha')) {
2008
2009 my $hacmd = sub {
2010 my $upid = shift;
2011
2012 my $service = "vm:$vmid";
2013
2014 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2015
2016 print "Executing HA stop for VM $vmid\n";
2017
2018 PVE::Tools::run_command($cmd);
2019
2020 return;
2021 };
2022
2023 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2024
2025 } else {
2026
2027 my $realcmd = sub {
2028 my $upid = shift;
2029
2030 syslog('info', "shutdown VM $vmid: $upid\n");
2031
2032 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2033 $shutdown, $param->{forceStop}, $keepActive);
2034
2035 return;
2036 };
2037
2038 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2039 }
2040 }});
2041
2042 __PACKAGE__->register_method({
2043 name => 'vm_suspend',
2044 path => '{vmid}/status/suspend',
2045 method => 'POST',
2046 protected => 1,
2047 proxyto => 'node',
2048 description => "Suspend virtual machine.",
2049 permissions => {
2050 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2051 },
2052 parameters => {
2053 additionalProperties => 0,
2054 properties => {
2055 node => get_standard_option('pve-node'),
2056 vmid => get_standard_option('pve-vmid',
2057 { completion => \&PVE::QemuServer::complete_vmid_running }),
2058 skiplock => get_standard_option('skiplock'),
2059 },
2060 },
2061 returns => {
2062 type => 'string',
2063 },
2064 code => sub {
2065 my ($param) = @_;
2066
2067 my $rpcenv = PVE::RPCEnvironment::get();
2068
2069 my $authuser = $rpcenv->get_user();
2070
2071 my $node = extract_param($param, 'node');
2072
2073 my $vmid = extract_param($param, 'vmid');
2074
2075 my $skiplock = extract_param($param, 'skiplock');
2076 raise_param_exc({ skiplock => "Only root may use this option." })
2077 if $skiplock && $authuser ne 'root@pam';
2078
2079 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2080
2081 my $realcmd = sub {
2082 my $upid = shift;
2083
2084 syslog('info', "suspend VM $vmid: $upid\n");
2085
2086 PVE::QemuServer::vm_suspend($vmid, $skiplock);
2087
2088 return;
2089 };
2090
2091 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
2092 }});
2093
2094 __PACKAGE__->register_method({
2095 name => 'vm_resume',
2096 path => '{vmid}/status/resume',
2097 method => 'POST',
2098 protected => 1,
2099 proxyto => 'node',
2100 description => "Resume virtual machine.",
2101 permissions => {
2102 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2103 },
2104 parameters => {
2105 additionalProperties => 0,
2106 properties => {
2107 node => get_standard_option('pve-node'),
2108 vmid => get_standard_option('pve-vmid',
2109 { completion => \&PVE::QemuServer::complete_vmid_running }),
2110 skiplock => get_standard_option('skiplock'),
2111 nocheck => { type => 'boolean', optional => 1 },
2112
2113 },
2114 },
2115 returns => {
2116 type => 'string',
2117 },
2118 code => sub {
2119 my ($param) = @_;
2120
2121 my $rpcenv = PVE::RPCEnvironment::get();
2122
2123 my $authuser = $rpcenv->get_user();
2124
2125 my $node = extract_param($param, 'node');
2126
2127 my $vmid = extract_param($param, 'vmid');
2128
2129 my $skiplock = extract_param($param, 'skiplock');
2130 raise_param_exc({ skiplock => "Only root may use this option." })
2131 if $skiplock && $authuser ne 'root@pam';
2132
2133 my $nocheck = extract_param($param, 'nocheck');
2134
2135 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid, $nocheck);
2136
2137 my $realcmd = sub {
2138 my $upid = shift;
2139
2140 syslog('info', "resume VM $vmid: $upid\n");
2141
2142 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2143
2144 return;
2145 };
2146
2147 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2148 }});
2149
2150 __PACKAGE__->register_method({
2151 name => 'vm_sendkey',
2152 path => '{vmid}/sendkey',
2153 method => 'PUT',
2154 protected => 1,
2155 proxyto => 'node',
2156 description => "Send key event to virtual machine.",
2157 permissions => {
2158 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2159 },
2160 parameters => {
2161 additionalProperties => 0,
2162 properties => {
2163 node => get_standard_option('pve-node'),
2164 vmid => get_standard_option('pve-vmid',
2165 { completion => \&PVE::QemuServer::complete_vmid_running }),
2166 skiplock => get_standard_option('skiplock'),
2167 key => {
2168 description => "The key (qemu monitor encoding).",
2169 type => 'string'
2170 }
2171 },
2172 },
2173 returns => { type => 'null'},
2174 code => sub {
2175 my ($param) = @_;
2176
2177 my $rpcenv = PVE::RPCEnvironment::get();
2178
2179 my $authuser = $rpcenv->get_user();
2180
2181 my $node = extract_param($param, 'node');
2182
2183 my $vmid = extract_param($param, 'vmid');
2184
2185 my $skiplock = extract_param($param, 'skiplock');
2186 raise_param_exc({ skiplock => "Only root may use this option." })
2187 if $skiplock && $authuser ne 'root@pam';
2188
2189 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2190
2191 return;
2192 }});
2193
2194 __PACKAGE__->register_method({
2195 name => 'vm_feature',
2196 path => '{vmid}/feature',
2197 method => 'GET',
2198 proxyto => 'node',
2199 protected => 1,
2200 description => "Check if feature for virtual machine is available.",
2201 permissions => {
2202 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2203 },
2204 parameters => {
2205 additionalProperties => 0,
2206 properties => {
2207 node => get_standard_option('pve-node'),
2208 vmid => get_standard_option('pve-vmid'),
2209 feature => {
2210 description => "Feature to check.",
2211 type => 'string',
2212 enum => [ 'snapshot', 'clone', 'copy' ],
2213 },
2214 snapname => get_standard_option('pve-snapshot-name', {
2215 optional => 1,
2216 }),
2217 },
2218 },
2219 returns => {
2220 type => "object",
2221 properties => {
2222 hasFeature => { type => 'boolean' },
2223 nodes => {
2224 type => 'array',
2225 items => { type => 'string' },
2226 }
2227 },
2228 },
2229 code => sub {
2230 my ($param) = @_;
2231
2232 my $node = extract_param($param, 'node');
2233
2234 my $vmid = extract_param($param, 'vmid');
2235
2236 my $snapname = extract_param($param, 'snapname');
2237
2238 my $feature = extract_param($param, 'feature');
2239
2240 my $running = PVE::QemuServer::check_running($vmid);
2241
2242 my $conf = PVE::QemuConfig->load_config($vmid);
2243
2244 if($snapname){
2245 my $snap = $conf->{snapshots}->{$snapname};
2246 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2247 $conf = $snap;
2248 }
2249 my $storecfg = PVE::Storage::config();
2250
2251 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2252 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
2253
2254 return {
2255 hasFeature => $hasFeature,
2256 nodes => [ keys %$nodelist ],
2257 };
2258 }});
2259
2260 __PACKAGE__->register_method({
2261 name => 'clone_vm',
2262 path => '{vmid}/clone',
2263 method => 'POST',
2264 protected => 1,
2265 proxyto => 'node',
2266 description => "Create a copy of virtual machine/template.",
2267 permissions => {
2268 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2269 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2270 "'Datastore.AllocateSpace' on any used storage.",
2271 check =>
2272 [ 'and',
2273 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2274 [ 'or',
2275 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2276 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2277 ],
2278 ]
2279 },
2280 parameters => {
2281 additionalProperties => 0,
2282 properties => {
2283 node => get_standard_option('pve-node'),
2284 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2285 newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }),
2286 name => {
2287 optional => 1,
2288 type => 'string', format => 'dns-name',
2289 description => "Set a name for the new VM.",
2290 },
2291 description => {
2292 optional => 1,
2293 type => 'string',
2294 description => "Description for the new VM.",
2295 },
2296 pool => {
2297 optional => 1,
2298 type => 'string', format => 'pve-poolid',
2299 description => "Add the new VM to the specified pool.",
2300 },
2301 snapname => get_standard_option('pve-snapshot-name', {
2302 optional => 1,
2303 }),
2304 storage => get_standard_option('pve-storage-id', {
2305 description => "Target storage for full clone.",
2306 requires => 'full',
2307 optional => 1,
2308 }),
2309 'format' => {
2310 description => "Target format for file storage.",
2311 requires => 'full',
2312 type => 'string',
2313 optional => 1,
2314 enum => [ 'raw', 'qcow2', 'vmdk'],
2315 },
2316 full => {
2317 optional => 1,
2318 type => 'boolean',
2319 description => "Create a full copy of all disk. This is always done when " .
2320 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2321 default => 0,
2322 },
2323 target => get_standard_option('pve-node', {
2324 description => "Target node. Only allowed if the original VM is on shared storage.",
2325 optional => 1,
2326 }),
2327 },
2328 },
2329 returns => {
2330 type => 'string',
2331 },
2332 code => sub {
2333 my ($param) = @_;
2334
2335 my $rpcenv = PVE::RPCEnvironment::get();
2336
2337 my $authuser = $rpcenv->get_user();
2338
2339 my $node = extract_param($param, 'node');
2340
2341 my $vmid = extract_param($param, 'vmid');
2342
2343 my $newid = extract_param($param, 'newid');
2344
2345 my $pool = extract_param($param, 'pool');
2346
2347 if (defined($pool)) {
2348 $rpcenv->check_pool_exist($pool);
2349 }
2350
2351 my $snapname = extract_param($param, 'snapname');
2352
2353 my $storage = extract_param($param, 'storage');
2354
2355 my $format = extract_param($param, 'format');
2356
2357 my $target = extract_param($param, 'target');
2358
2359 my $localnode = PVE::INotify::nodename();
2360
2361 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2362
2363 PVE::Cluster::check_node_exists($target) if $target;
2364
2365 my $storecfg = PVE::Storage::config();
2366
2367 if ($storage) {
2368 # check if storage is enabled on local node
2369 PVE::Storage::storage_check_enabled($storecfg, $storage);
2370 if ($target) {
2371 # check if storage is available on target node
2372 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2373 # clone only works if target storage is shared
2374 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2375 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2376 }
2377 }
2378
2379 PVE::Cluster::check_cfs_quorum();
2380
2381 my $running = PVE::QemuServer::check_running($vmid) || 0;
2382
2383 # exclusive lock if VM is running - else shared lock is enough;
2384 my $shared_lock = $running ? 0 : 1;
2385
2386 my $clonefn = sub {
2387
2388 # do all tests after lock
2389 # we also try to do all tests before we fork the worker
2390
2391 my $conf = PVE::QemuConfig->load_config($vmid);
2392
2393 PVE::QemuConfig->check_lock($conf);
2394
2395 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2396
2397 die "unexpected state change\n" if $verify_running != $running;
2398
2399 die "snapshot '$snapname' does not exist\n"
2400 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2401
2402 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2403
2404 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2405
2406 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2407
2408 my $conffile = PVE::QemuConfig->config_file($newid);
2409
2410 die "unable to create VM $newid: config file already exists\n"
2411 if -f $conffile;
2412
2413 my $newconf = { lock => 'clone' };
2414 my $drives = {};
2415 my $fullclone = {};
2416 my $vollist = [];
2417
2418 foreach my $opt (keys %$oldconf) {
2419 my $value = $oldconf->{$opt};
2420
2421 # do not copy snapshot related info
2422 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2423 $opt eq 'vmstate' || $opt eq 'snapstate';
2424
2425 # no need to copy unused images, because VMID(owner) changes anyways
2426 next if $opt =~ m/^unused\d+$/;
2427
2428 # always change MAC! address
2429 if ($opt =~ m/^net(\d+)$/) {
2430 my $net = PVE::QemuServer::parse_net($value);
2431 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2432 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
2433 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2434 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
2435 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2436 die "unable to parse drive options for '$opt'\n" if !$drive;
2437 if (PVE::QemuServer::drive_is_cdrom($drive)) {
2438 $newconf->{$opt} = $value; # simply copy configuration
2439 } else {
2440 if ($param->{full}) {
2441 die "Full clone feature is not available"
2442 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2443 $fullclone->{$opt} = 1;
2444 } else {
2445 # not full means clone instead of copy
2446 die "Linked clone feature is not available"
2447 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
2448 }
2449 $drives->{$opt} = $drive;
2450 push @$vollist, $drive->{file};
2451 }
2452 } else {
2453 # copy everything else
2454 $newconf->{$opt} = $value;
2455 }
2456 }
2457
2458 # auto generate a new uuid
2459 my ($uuid, $uuid_str);
2460 UUID::generate($uuid);
2461 UUID::unparse($uuid, $uuid_str);
2462 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
2463 $smbios1->{uuid} = $uuid_str;
2464 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2465
2466 delete $newconf->{template};
2467
2468 if ($param->{name}) {
2469 $newconf->{name} = $param->{name};
2470 } else {
2471 if ($oldconf->{name}) {
2472 $newconf->{name} = "Copy-of-$oldconf->{name}";
2473 } else {
2474 $newconf->{name} = "Copy-of-VM-$vmid";
2475 }
2476 }
2477
2478 if ($param->{description}) {
2479 $newconf->{description} = $param->{description};
2480 }
2481
2482 # create empty/temp config - this fails if VM already exists on other node
2483 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2484
2485 my $realcmd = sub {
2486 my $upid = shift;
2487
2488 my $newvollist = [];
2489 my $jobs = {};
2490
2491 eval {
2492 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2493
2494 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
2495
2496 my $total_jobs = scalar(keys %{$drives});
2497 my $i = 1;
2498
2499 foreach my $opt (keys %$drives) {
2500 my $drive = $drives->{$opt};
2501 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2502
2503 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2504 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2505 $jobs, $skipcomplete, $oldconf->{agent});
2506
2507 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2508
2509 PVE::QemuConfig->write_config($newid, $newconf);
2510 $i++;
2511 }
2512
2513 delete $newconf->{lock};
2514 PVE::QemuConfig->write_config($newid, $newconf);
2515
2516 if ($target) {
2517 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2518 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
2519 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
2520
2521 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
2522 die "Failed to move config to node '$target' - rename failed: $!\n"
2523 if !rename($conffile, $newconffile);
2524 }
2525
2526 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2527 };
2528 if (my $err = $@) {
2529 unlink $conffile;
2530
2531 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2532
2533 sleep 1; # some storage like rbd need to wait before release volume - really?
2534
2535 foreach my $volid (@$newvollist) {
2536 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2537 warn $@ if $@;
2538 }
2539 die "clone failed: $err";
2540 }
2541
2542 return;
2543 };
2544
2545 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2546
2547 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2548 };
2549
2550 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
2551 # Aquire exclusive lock lock for $newid
2552 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
2553 });
2554
2555 }});
2556
2557 __PACKAGE__->register_method({
2558 name => 'move_vm_disk',
2559 path => '{vmid}/move_disk',
2560 method => 'POST',
2561 protected => 1,
2562 proxyto => 'node',
2563 description => "Move volume to different storage.",
2564 permissions => {
2565 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2566 check => [ 'and',
2567 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2568 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2569 ],
2570 },
2571 parameters => {
2572 additionalProperties => 0,
2573 properties => {
2574 node => get_standard_option('pve-node'),
2575 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2576 disk => {
2577 type => 'string',
2578 description => "The disk you want to move.",
2579 enum => [ PVE::QemuServer::valid_drive_names() ],
2580 },
2581 storage => get_standard_option('pve-storage-id', {
2582 description => "Target storage.",
2583 completion => \&PVE::QemuServer::complete_storage,
2584 }),
2585 'format' => {
2586 type => 'string',
2587 description => "Target Format.",
2588 enum => [ 'raw', 'qcow2', 'vmdk' ],
2589 optional => 1,
2590 },
2591 delete => {
2592 type => 'boolean',
2593 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2594 optional => 1,
2595 default => 0,
2596 },
2597 digest => {
2598 type => 'string',
2599 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2600 maxLength => 40,
2601 optional => 1,
2602 },
2603 },
2604 },
2605 returns => {
2606 type => 'string',
2607 description => "the task ID.",
2608 },
2609 code => sub {
2610 my ($param) = @_;
2611
2612 my $rpcenv = PVE::RPCEnvironment::get();
2613
2614 my $authuser = $rpcenv->get_user();
2615
2616 my $node = extract_param($param, 'node');
2617
2618 my $vmid = extract_param($param, 'vmid');
2619
2620 my $digest = extract_param($param, 'digest');
2621
2622 my $disk = extract_param($param, 'disk');
2623
2624 my $storeid = extract_param($param, 'storage');
2625
2626 my $format = extract_param($param, 'format');
2627
2628 my $storecfg = PVE::Storage::config();
2629
2630 my $updatefn = sub {
2631
2632 my $conf = PVE::QemuConfig->load_config($vmid);
2633
2634 PVE::QemuConfig->check_lock($conf);
2635
2636 die "checksum missmatch (file change by other user?)\n"
2637 if $digest && $digest ne $conf->{digest};
2638
2639 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2640
2641 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2642
2643 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
2644
2645 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
2646
2647 my $oldfmt;
2648 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
2649 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2650 $oldfmt = $1;
2651 }
2652
2653 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2654 (!$format || !$oldfmt || $oldfmt eq $format);
2655
2656 # this only checks snapshots because $disk is passed!
2657 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
2658 die "you can't move a disk with snapshots and delete the source\n"
2659 if $snapshotted && $param->{delete};
2660
2661 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2662
2663 my $running = PVE::QemuServer::check_running($vmid);
2664
2665 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
2666
2667 my $realcmd = sub {
2668
2669 my $newvollist = [];
2670
2671 eval {
2672 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2673
2674 warn "moving disk with snapshots, snapshots will not be moved!\n"
2675 if $snapshotted;
2676
2677 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
2678 $vmid, $storeid, $format, 1, $newvollist);
2679
2680 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
2681
2682 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
2683
2684 # convert moved disk to base if part of template
2685 PVE::QemuServer::template_create($vmid, $conf, $disk)
2686 if PVE::QemuConfig->is_template($conf);
2687
2688 PVE::QemuConfig->write_config($vmid, $conf);
2689
2690 eval {
2691 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2692 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
2693 if !$running;
2694 };
2695 warn $@ if $@;
2696 };
2697 if (my $err = $@) {
2698
2699 foreach my $volid (@$newvollist) {
2700 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2701 warn $@ if $@;
2702 }
2703 die "storage migration failed: $err";
2704 }
2705
2706 if ($param->{delete}) {
2707 eval {
2708 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
2709 PVE::Storage::vdisk_free($storecfg, $old_volid);
2710 };
2711 warn $@ if $@;
2712 }
2713 };
2714
2715 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2716 };
2717
2718 return PVE::QemuConfig->lock_config($vmid, $updatefn);
2719 }});
2720
2721 __PACKAGE__->register_method({
2722 name => 'migrate_vm',
2723 path => '{vmid}/migrate',
2724 method => 'POST',
2725 protected => 1,
2726 proxyto => 'node',
2727 description => "Migrate virtual machine. Creates a new migration task.",
2728 permissions => {
2729 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2730 },
2731 parameters => {
2732 additionalProperties => 0,
2733 properties => {
2734 node => get_standard_option('pve-node'),
2735 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2736 target => get_standard_option('pve-node', {
2737 description => "Target node.",
2738 completion => \&PVE::Cluster::complete_migration_target,
2739 }),
2740 online => {
2741 type => 'boolean',
2742 description => "Use online/live migration.",
2743 optional => 1,
2744 },
2745 force => {
2746 type => 'boolean',
2747 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
2748 optional => 1,
2749 },
2750 migration_type => {
2751 type => 'string',
2752 enum => ['secure', 'insecure'],
2753 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2754 optional => 1,
2755 },
2756 migration_network => {
2757 type => 'string', format => 'CIDR',
2758 description => "CIDR of the (sub) network that is used for migration.",
2759 optional => 1,
2760 },
2761 "with-local-disks" => {
2762 type => 'boolean',
2763 description => "Enable live storage migration for local disk",
2764 optional => 1,
2765 },
2766 targetstorage => get_standard_option('pve-storage-id', {
2767 description => "Default target storage.",
2768 optional => 1,
2769 completion => \&PVE::QemuServer::complete_storage,
2770 }),
2771 },
2772 },
2773 returns => {
2774 type => 'string',
2775 description => "the task ID.",
2776 },
2777 code => sub {
2778 my ($param) = @_;
2779
2780 my $rpcenv = PVE::RPCEnvironment::get();
2781
2782 my $authuser = $rpcenv->get_user();
2783
2784 my $target = extract_param($param, 'target');
2785
2786 my $localnode = PVE::INotify::nodename();
2787 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
2788
2789 PVE::Cluster::check_cfs_quorum();
2790
2791 PVE::Cluster::check_node_exists($target);
2792
2793 my $targetip = PVE::Cluster::remote_node_ip($target);
2794
2795 my $vmid = extract_param($param, 'vmid');
2796
2797 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
2798 if !$param->{online} && $param->{targetstorage};
2799
2800 raise_param_exc({ force => "Only root may use this option." })
2801 if $param->{force} && $authuser ne 'root@pam';
2802
2803 raise_param_exc({ migration_type => "Only root may use this option." })
2804 if $param->{migration_type} && $authuser ne 'root@pam';
2805
2806 # allow root only until better network permissions are available
2807 raise_param_exc({ migration_network => "Only root may use this option." })
2808 if $param->{migration_network} && $authuser ne 'root@pam';
2809
2810 # test if VM exists
2811 my $conf = PVE::QemuConfig->load_config($vmid);
2812
2813 # try to detect errors early
2814
2815 PVE::QemuConfig->check_lock($conf);
2816
2817 if (PVE::QemuServer::check_running($vmid)) {
2818 die "cant migrate running VM without --online\n"
2819 if !$param->{online};
2820 }
2821
2822 my $storecfg = PVE::Storage::config();
2823 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
2824
2825 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2826
2827 my $hacmd = sub {
2828 my $upid = shift;
2829
2830 my $service = "vm:$vmid";
2831
2832 my $cmd = ['ha-manager', 'migrate', $service, $target];
2833
2834 print "Executing HA migrate for VM $vmid to node $target\n";
2835
2836 PVE::Tools::run_command($cmd);
2837
2838 return;
2839 };
2840
2841 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2842
2843 } else {
2844
2845 my $realcmd = sub {
2846 my $upid = shift;
2847
2848 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
2849 };
2850
2851 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd);
2852 }
2853
2854 }});
2855
2856 __PACKAGE__->register_method({
2857 name => 'monitor',
2858 path => '{vmid}/monitor',
2859 method => 'POST',
2860 protected => 1,
2861 proxyto => 'node',
2862 description => "Execute Qemu monitor commands.",
2863 permissions => {
2864 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
2865 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2866 },
2867 parameters => {
2868 additionalProperties => 0,
2869 properties => {
2870 node => get_standard_option('pve-node'),
2871 vmid => get_standard_option('pve-vmid'),
2872 command => {
2873 type => 'string',
2874 description => "The monitor command.",
2875 }
2876 },
2877 },
2878 returns => { type => 'string'},
2879 code => sub {
2880 my ($param) = @_;
2881
2882 my $rpcenv = PVE::RPCEnvironment::get();
2883 my $authuser = $rpcenv->get_user();
2884
2885 my $is_ro = sub {
2886 my $command = shift;
2887 return $command =~ m/^\s*info(\s+|$)/
2888 || $command =~ m/^\s*help\s*$/;
2889 };
2890
2891 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
2892 if !&$is_ro($param->{command});
2893
2894 my $vmid = $param->{vmid};
2895
2896 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
2897
2898 my $res = '';
2899 eval {
2900 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
2901 };
2902 $res = "ERROR: $@" if $@;
2903
2904 return $res;
2905 }});
2906
2907 my $guest_agent_commands = [
2908 'ping',
2909 'get-time',
2910 'info',
2911 'fsfreeze-status',
2912 'fsfreeze-freeze',
2913 'fsfreeze-thaw',
2914 'fstrim',
2915 'network-get-interfaces',
2916 'get-vcpus',
2917 'get-fsinfo',
2918 'get-memory-blocks',
2919 'get-memory-block-info',
2920 'suspend-hybrid',
2921 'suspend-ram',
2922 'suspend-disk',
2923 'shutdown',
2924 ];
2925
2926 __PACKAGE__->register_method({
2927 name => 'agent',
2928 path => '{vmid}/agent',
2929 method => 'POST',
2930 protected => 1,
2931 proxyto => 'node',
2932 description => "Execute Qemu Guest Agent commands.",
2933 permissions => {
2934 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2935 },
2936 parameters => {
2937 additionalProperties => 0,
2938 properties => {
2939 node => get_standard_option('pve-node'),
2940 vmid => get_standard_option('pve-vmid', {
2941 completion => \&PVE::QemuServer::complete_vmid_running }),
2942 command => {
2943 type => 'string',
2944 description => "The QGA command.",
2945 enum => $guest_agent_commands,
2946 },
2947 },
2948 },
2949 returns => {
2950 type => 'object',
2951 description => "Returns an object with a single `result` property. The type of that
2952 property depends on the executed command.",
2953 },
2954 code => sub {
2955 my ($param) = @_;
2956
2957 my $vmid = $param->{vmid};
2958
2959 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
2960
2961 die "No Qemu Guest Agent\n" if !defined($conf->{agent});
2962 die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid);
2963
2964 my $cmd = $param->{command};
2965
2966 my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd");
2967
2968 return { result => $res };
2969 }});
2970
2971 __PACKAGE__->register_method({
2972 name => 'resize_vm',
2973 path => '{vmid}/resize',
2974 method => 'PUT',
2975 protected => 1,
2976 proxyto => 'node',
2977 description => "Extend volume size.",
2978 permissions => {
2979 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2980 },
2981 parameters => {
2982 additionalProperties => 0,
2983 properties => {
2984 node => get_standard_option('pve-node'),
2985 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2986 skiplock => get_standard_option('skiplock'),
2987 disk => {
2988 type => 'string',
2989 description => "The disk you want to resize.",
2990 enum => [PVE::QemuServer::valid_drive_names()],
2991 },
2992 size => {
2993 type => 'string',
2994 pattern => '\+?\d+(\.\d+)?[KMGT]?',
2995 description => "The new size. With the `+` sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.",
2996 },
2997 digest => {
2998 type => 'string',
2999 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3000 maxLength => 40,
3001 optional => 1,
3002 },
3003 },
3004 },
3005 returns => { type => 'null'},
3006 code => sub {
3007 my ($param) = @_;
3008
3009 my $rpcenv = PVE::RPCEnvironment::get();
3010
3011 my $authuser = $rpcenv->get_user();
3012
3013 my $node = extract_param($param, 'node');
3014
3015 my $vmid = extract_param($param, 'vmid');
3016
3017 my $digest = extract_param($param, 'digest');
3018
3019 my $disk = extract_param($param, 'disk');
3020
3021 my $sizestr = extract_param($param, 'size');
3022
3023 my $skiplock = extract_param($param, 'skiplock');
3024 raise_param_exc({ skiplock => "Only root may use this option." })
3025 if $skiplock && $authuser ne 'root@pam';
3026
3027 my $storecfg = PVE::Storage::config();
3028
3029 my $updatefn = sub {
3030
3031 my $conf = PVE::QemuConfig->load_config($vmid);
3032
3033 die "checksum missmatch (file change by other user?)\n"
3034 if $digest && $digest ne $conf->{digest};
3035 PVE::QemuConfig->check_lock($conf) if !$skiplock;
3036
3037 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3038
3039 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3040
3041 my (undef, undef, undef, undef, undef, undef, $format) =
3042 PVE::Storage::parse_volname($storecfg, $drive->{file});
3043
3044 die "can't resize volume: $disk if snapshot exists\n"
3045 if %{$conf->{snapshots}} && $format eq 'qcow2';
3046
3047 my $volid = $drive->{file};
3048
3049 die "disk '$disk' has no associated volume\n" if !$volid;
3050
3051 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3052
3053 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3054
3055 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3056
3057 PVE::Storage::activate_volumes($storecfg, [$volid]);
3058 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3059
3060 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3061 my ($ext, $newsize, $unit) = ($1, $2, $4);
3062 if ($unit) {
3063 if ($unit eq 'K') {
3064 $newsize = $newsize * 1024;
3065 } elsif ($unit eq 'M') {
3066 $newsize = $newsize * 1024 * 1024;
3067 } elsif ($unit eq 'G') {
3068 $newsize = $newsize * 1024 * 1024 * 1024;
3069 } elsif ($unit eq 'T') {
3070 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3071 }
3072 }
3073 $newsize += $size if $ext;
3074 $newsize = int($newsize);
3075
3076 die "shrinking disks is not supported\n" if $newsize < $size;
3077
3078 return if $size == $newsize;
3079
3080 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3081
3082 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3083
3084 $drive->{size} = $newsize;
3085 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3086
3087 PVE::QemuConfig->write_config($vmid, $conf);
3088 };
3089
3090 PVE::QemuConfig->lock_config($vmid, $updatefn);
3091 return undef;
3092 }});
3093
3094 __PACKAGE__->register_method({
3095 name => 'snapshot_list',
3096 path => '{vmid}/snapshot',
3097 method => 'GET',
3098 description => "List all snapshots.",
3099 permissions => {
3100 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3101 },
3102 proxyto => 'node',
3103 protected => 1, # qemu pid files are only readable by root
3104 parameters => {
3105 additionalProperties => 0,
3106 properties => {
3107 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3108 node => get_standard_option('pve-node'),
3109 },
3110 },
3111 returns => {
3112 type => 'array',
3113 items => {
3114 type => "object",
3115 properties => {},
3116 },
3117 links => [ { rel => 'child', href => "{name}" } ],
3118 },
3119 code => sub {
3120 my ($param) = @_;
3121
3122 my $vmid = $param->{vmid};
3123
3124 my $conf = PVE::QemuConfig->load_config($vmid);
3125 my $snaphash = $conf->{snapshots} || {};
3126
3127 my $res = [];
3128
3129 foreach my $name (keys %$snaphash) {
3130 my $d = $snaphash->{$name};
3131 my $item = {
3132 name => $name,
3133 snaptime => $d->{snaptime} || 0,
3134 vmstate => $d->{vmstate} ? 1 : 0,
3135 description => $d->{description} || '',
3136 };
3137 $item->{parent} = $d->{parent} if $d->{parent};
3138 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
3139 push @$res, $item;
3140 }
3141
3142 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
3143 my $current = { name => 'current', digest => $conf->{digest}, running => $running };
3144 $current->{parent} = $conf->{parent} if $conf->{parent};
3145
3146 push @$res, $current;
3147
3148 return $res;
3149 }});
3150
3151 __PACKAGE__->register_method({
3152 name => 'snapshot',
3153 path => '{vmid}/snapshot',
3154 method => 'POST',
3155 protected => 1,
3156 proxyto => 'node',
3157 description => "Snapshot a VM.",
3158 permissions => {
3159 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3160 },
3161 parameters => {
3162 additionalProperties => 0,
3163 properties => {
3164 node => get_standard_option('pve-node'),
3165 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3166 snapname => get_standard_option('pve-snapshot-name'),
3167 vmstate => {
3168 optional => 1,
3169 type => 'boolean',
3170 description => "Save the vmstate",
3171 },
3172 description => {
3173 optional => 1,
3174 type => 'string',
3175 description => "A textual description or comment.",
3176 },
3177 },
3178 },
3179 returns => {
3180 type => 'string',
3181 description => "the task ID.",
3182 },
3183 code => sub {
3184 my ($param) = @_;
3185
3186 my $rpcenv = PVE::RPCEnvironment::get();
3187
3188 my $authuser = $rpcenv->get_user();
3189
3190 my $node = extract_param($param, 'node');
3191
3192 my $vmid = extract_param($param, 'vmid');
3193
3194 my $snapname = extract_param($param, 'snapname');
3195
3196 die "unable to use snapshot name 'current' (reserved name)\n"
3197 if $snapname eq 'current';
3198
3199 my $realcmd = sub {
3200 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
3201 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
3202 $param->{description});
3203 };
3204
3205 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3206 }});
3207
3208 __PACKAGE__->register_method({
3209 name => 'snapshot_cmd_idx',
3210 path => '{vmid}/snapshot/{snapname}',
3211 description => '',
3212 method => 'GET',
3213 permissions => {
3214 user => 'all',
3215 },
3216 parameters => {
3217 additionalProperties => 0,
3218 properties => {
3219 vmid => get_standard_option('pve-vmid'),
3220 node => get_standard_option('pve-node'),
3221 snapname => get_standard_option('pve-snapshot-name'),
3222 },
3223 },
3224 returns => {
3225 type => 'array',
3226 items => {
3227 type => "object",
3228 properties => {},
3229 },
3230 links => [ { rel => 'child', href => "{cmd}" } ],
3231 },
3232 code => sub {
3233 my ($param) = @_;
3234
3235 my $res = [];
3236
3237 push @$res, { cmd => 'rollback' };
3238 push @$res, { cmd => 'config' };
3239
3240 return $res;
3241 }});
3242
3243 __PACKAGE__->register_method({
3244 name => 'update_snapshot_config',
3245 path => '{vmid}/snapshot/{snapname}/config',
3246 method => 'PUT',
3247 protected => 1,
3248 proxyto => 'node',
3249 description => "Update snapshot metadata.",
3250 permissions => {
3251 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3252 },
3253 parameters => {
3254 additionalProperties => 0,
3255 properties => {
3256 node => get_standard_option('pve-node'),
3257 vmid => get_standard_option('pve-vmid'),
3258 snapname => get_standard_option('pve-snapshot-name'),
3259 description => {
3260 optional => 1,
3261 type => 'string',
3262 description => "A textual description or comment.",
3263 },
3264 },
3265 },
3266 returns => { type => 'null' },
3267 code => sub {
3268 my ($param) = @_;
3269
3270 my $rpcenv = PVE::RPCEnvironment::get();
3271
3272 my $authuser = $rpcenv->get_user();
3273
3274 my $vmid = extract_param($param, 'vmid');
3275
3276 my $snapname = extract_param($param, 'snapname');
3277
3278 return undef if !defined($param->{description});
3279
3280 my $updatefn = sub {
3281
3282 my $conf = PVE::QemuConfig->load_config($vmid);
3283
3284 PVE::QemuConfig->check_lock($conf);
3285
3286 my $snap = $conf->{snapshots}->{$snapname};
3287
3288 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3289
3290 $snap->{description} = $param->{description} if defined($param->{description});
3291
3292 PVE::QemuConfig->write_config($vmid, $conf);
3293 };
3294
3295 PVE::QemuConfig->lock_config($vmid, $updatefn);
3296
3297 return undef;
3298 }});
3299
3300 __PACKAGE__->register_method({
3301 name => 'get_snapshot_config',
3302 path => '{vmid}/snapshot/{snapname}/config',
3303 method => 'GET',
3304 proxyto => 'node',
3305 description => "Get snapshot configuration",
3306 permissions => {
3307 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3308 },
3309 parameters => {
3310 additionalProperties => 0,
3311 properties => {
3312 node => get_standard_option('pve-node'),
3313 vmid => get_standard_option('pve-vmid'),
3314 snapname => get_standard_option('pve-snapshot-name'),
3315 },
3316 },
3317 returns => { type => "object" },
3318 code => sub {
3319 my ($param) = @_;
3320
3321 my $rpcenv = PVE::RPCEnvironment::get();
3322
3323 my $authuser = $rpcenv->get_user();
3324
3325 my $vmid = extract_param($param, 'vmid');
3326
3327 my $snapname = extract_param($param, 'snapname');
3328
3329 my $conf = PVE::QemuConfig->load_config($vmid);
3330
3331 my $snap = $conf->{snapshots}->{$snapname};
3332
3333 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3334
3335 return $snap;
3336 }});
3337
3338 __PACKAGE__->register_method({
3339 name => 'rollback',
3340 path => '{vmid}/snapshot/{snapname}/rollback',
3341 method => 'POST',
3342 protected => 1,
3343 proxyto => 'node',
3344 description => "Rollback VM state to specified snapshot.",
3345 permissions => {
3346 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3347 },
3348 parameters => {
3349 additionalProperties => 0,
3350 properties => {
3351 node => get_standard_option('pve-node'),
3352 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3353 snapname => get_standard_option('pve-snapshot-name'),
3354 },
3355 },
3356 returns => {
3357 type => 'string',
3358 description => "the task ID.",
3359 },
3360 code => sub {
3361 my ($param) = @_;
3362
3363 my $rpcenv = PVE::RPCEnvironment::get();
3364
3365 my $authuser = $rpcenv->get_user();
3366
3367 my $node = extract_param($param, 'node');
3368
3369 my $vmid = extract_param($param, 'vmid');
3370
3371 my $snapname = extract_param($param, 'snapname');
3372
3373 my $realcmd = sub {
3374 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3375 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
3376 };
3377
3378 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd);
3379 }});
3380
3381 __PACKAGE__->register_method({
3382 name => 'delsnapshot',
3383 path => '{vmid}/snapshot/{snapname}',
3384 method => 'DELETE',
3385 protected => 1,
3386 proxyto => 'node',
3387 description => "Delete a VM snapshot.",
3388 permissions => {
3389 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3390 },
3391 parameters => {
3392 additionalProperties => 0,
3393 properties => {
3394 node => get_standard_option('pve-node'),
3395 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3396 snapname => get_standard_option('pve-snapshot-name'),
3397 force => {
3398 optional => 1,
3399 type => 'boolean',
3400 description => "For removal from config file, even if removing disk snapshots fails.",
3401 },
3402 },
3403 },
3404 returns => {
3405 type => 'string',
3406 description => "the task ID.",
3407 },
3408 code => sub {
3409 my ($param) = @_;
3410
3411 my $rpcenv = PVE::RPCEnvironment::get();
3412
3413 my $authuser = $rpcenv->get_user();
3414
3415 my $node = extract_param($param, 'node');
3416
3417 my $vmid = extract_param($param, 'vmid');
3418
3419 my $snapname = extract_param($param, 'snapname');
3420
3421 my $realcmd = sub {
3422 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3423 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
3424 };
3425
3426 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3427 }});
3428
3429 __PACKAGE__->register_method({
3430 name => 'template',
3431 path => '{vmid}/template',
3432 method => 'POST',
3433 protected => 1,
3434 proxyto => 'node',
3435 description => "Create a Template.",
3436 permissions => {
3437 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3438 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3439 },
3440 parameters => {
3441 additionalProperties => 0,
3442 properties => {
3443 node => get_standard_option('pve-node'),
3444 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3445 disk => {
3446 optional => 1,
3447 type => 'string',
3448 description => "If you want to convert only 1 disk to base image.",
3449 enum => [PVE::QemuServer::valid_drive_names()],
3450 },
3451
3452 },
3453 },
3454 returns => { type => 'null'},
3455 code => sub {
3456 my ($param) = @_;
3457
3458 my $rpcenv = PVE::RPCEnvironment::get();
3459
3460 my $authuser = $rpcenv->get_user();
3461
3462 my $node = extract_param($param, 'node');
3463
3464 my $vmid = extract_param($param, 'vmid');
3465
3466 my $disk = extract_param($param, 'disk');
3467
3468 my $updatefn = sub {
3469
3470 my $conf = PVE::QemuConfig->load_config($vmid);
3471
3472 PVE::QemuConfig->check_lock($conf);
3473
3474 die "unable to create template, because VM contains snapshots\n"
3475 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3476
3477 die "you can't convert a template to a template\n"
3478 if PVE::QemuConfig->is_template($conf) && !$disk;
3479
3480 die "you can't convert a VM to template if VM is running\n"
3481 if PVE::QemuServer::check_running($vmid);
3482
3483 my $realcmd = sub {
3484 PVE::QemuServer::template_create($vmid, $conf, $disk);
3485 };
3486
3487 $conf->{template} = 1;
3488 PVE::QemuConfig->write_config($vmid, $conf);
3489
3490 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3491 };
3492
3493 PVE::QemuConfig->lock_config($vmid, $updatefn);
3494 return undef;
3495 }});
3496
3497 1;