]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
remove netcat6 dependency
[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 ($opt =~ m/^unused/) {
1011 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
1012 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
1013 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1014 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1015 delete $conf->{$opt};
1016 PVE::QemuConfig->write_config($vmid, $conf);
1017 }
1018 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1019 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
1020 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1021 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1022 if defined($conf->{pending}->{$opt});
1023 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1024 PVE::QemuConfig->write_config($vmid, $conf);
1025 } else {
1026 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1027 PVE::QemuConfig->write_config($vmid, $conf);
1028 }
1029 }
1030
1031 foreach my $opt (keys %$param) { # add/change
1032 $modified->{$opt} = 1;
1033 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1034 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1035
1036 if (PVE::QemuServer::is_valid_drivename($opt)) {
1037 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1038 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1039 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1040 } else {
1041 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1042 }
1043 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1044 if defined($conf->{pending}->{$opt});
1045
1046 &$create_disks($rpcenv, $authuser, $conf->{pending}, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1047 } else {
1048 $conf->{pending}->{$opt} = $param->{$opt};
1049 }
1050 PVE::QemuServer::vmconfig_undelete_pending_option($conf, $opt);
1051 PVE::QemuConfig->write_config($vmid, $conf);
1052 }
1053
1054 # remove pending changes when nothing changed
1055 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1056 my $changes = PVE::QemuServer::vmconfig_cleanup_pending($conf);
1057 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
1058
1059 return if !scalar(keys %{$conf->{pending}});
1060
1061 my $running = PVE::QemuServer::check_running($vmid);
1062
1063 # apply pending changes
1064
1065 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1066
1067 if ($running) {
1068 my $errors = {};
1069 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
1070 raise_param_exc($errors) if scalar(keys %$errors);
1071 } else {
1072 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $running);
1073 }
1074
1075 return;
1076 };
1077
1078 if ($sync) {
1079 &$worker();
1080 return undef;
1081 } else {
1082 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1083
1084 if ($background_delay) {
1085
1086 # Note: It would be better to do that in the Event based HTTPServer
1087 # to avoid blocking call to sleep.
1088
1089 my $end_time = time() + $background_delay;
1090
1091 my $task = PVE::Tools::upid_decode($upid);
1092
1093 my $running = 1;
1094 while (time() < $end_time) {
1095 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1096 last if !$running;
1097 sleep(1); # this gets interrupted when child process ends
1098 }
1099
1100 if (!$running) {
1101 my $status = PVE::Tools::upid_read_status($upid);
1102 return undef if $status eq 'OK';
1103 die $status;
1104 }
1105 }
1106
1107 return $upid;
1108 }
1109 };
1110
1111 return PVE::QemuConfig->lock_config($vmid, $updatefn);
1112 };
1113
1114 my $vm_config_perm_list = [
1115 'VM.Config.Disk',
1116 'VM.Config.CDROM',
1117 'VM.Config.CPU',
1118 'VM.Config.Memory',
1119 'VM.Config.Network',
1120 'VM.Config.HWType',
1121 'VM.Config.Options',
1122 ];
1123
1124 __PACKAGE__->register_method({
1125 name => 'update_vm_async',
1126 path => '{vmid}/config',
1127 method => 'POST',
1128 protected => 1,
1129 proxyto => 'node',
1130 description => "Set virtual machine options (asynchrounous API).",
1131 permissions => {
1132 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1133 },
1134 parameters => {
1135 additionalProperties => 0,
1136 properties => PVE::QemuServer::json_config_properties(
1137 {
1138 node => get_standard_option('pve-node'),
1139 vmid => get_standard_option('pve-vmid'),
1140 skiplock => get_standard_option('skiplock'),
1141 delete => {
1142 type => 'string', format => 'pve-configid-list',
1143 description => "A list of settings you want to delete.",
1144 optional => 1,
1145 },
1146 revert => {
1147 type => 'string', format => 'pve-configid-list',
1148 description => "Revert a pending change.",
1149 optional => 1,
1150 },
1151 force => {
1152 type => 'boolean',
1153 description => $opt_force_description,
1154 optional => 1,
1155 requires => 'delete',
1156 },
1157 digest => {
1158 type => 'string',
1159 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1160 maxLength => 40,
1161 optional => 1,
1162 },
1163 background_delay => {
1164 type => 'integer',
1165 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1166 minimum => 1,
1167 maximum => 30,
1168 optional => 1,
1169 },
1170 }),
1171 },
1172 returns => {
1173 type => 'string',
1174 optional => 1,
1175 },
1176 code => $update_vm_api,
1177 });
1178
1179 __PACKAGE__->register_method({
1180 name => 'update_vm',
1181 path => '{vmid}/config',
1182 method => 'PUT',
1183 protected => 1,
1184 proxyto => 'node',
1185 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1186 permissions => {
1187 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1188 },
1189 parameters => {
1190 additionalProperties => 0,
1191 properties => PVE::QemuServer::json_config_properties(
1192 {
1193 node => get_standard_option('pve-node'),
1194 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1195 skiplock => get_standard_option('skiplock'),
1196 delete => {
1197 type => 'string', format => 'pve-configid-list',
1198 description => "A list of settings you want to delete.",
1199 optional => 1,
1200 },
1201 revert => {
1202 type => 'string', format => 'pve-configid-list',
1203 description => "Revert a pending change.",
1204 optional => 1,
1205 },
1206 force => {
1207 type => 'boolean',
1208 description => $opt_force_description,
1209 optional => 1,
1210 requires => 'delete',
1211 },
1212 digest => {
1213 type => 'string',
1214 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1215 maxLength => 40,
1216 optional => 1,
1217 },
1218 }),
1219 },
1220 returns => { type => 'null' },
1221 code => sub {
1222 my ($param) = @_;
1223 &$update_vm_api($param, 1);
1224 return undef;
1225 }
1226 });
1227
1228
1229 __PACKAGE__->register_method({
1230 name => 'destroy_vm',
1231 path => '{vmid}',
1232 method => 'DELETE',
1233 protected => 1,
1234 proxyto => 'node',
1235 description => "Destroy the vm (also delete all used/owned volumes).",
1236 permissions => {
1237 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1238 },
1239 parameters => {
1240 additionalProperties => 0,
1241 properties => {
1242 node => get_standard_option('pve-node'),
1243 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1244 skiplock => get_standard_option('skiplock'),
1245 },
1246 },
1247 returns => {
1248 type => 'string',
1249 },
1250 code => sub {
1251 my ($param) = @_;
1252
1253 my $rpcenv = PVE::RPCEnvironment::get();
1254
1255 my $authuser = $rpcenv->get_user();
1256
1257 my $vmid = $param->{vmid};
1258
1259 my $skiplock = $param->{skiplock};
1260 raise_param_exc({ skiplock => "Only root may use this option." })
1261 if $skiplock && $authuser ne 'root@pam';
1262
1263 # test if VM exists
1264 my $conf = PVE::QemuConfig->load_config($vmid);
1265
1266 my $storecfg = PVE::Storage::config();
1267
1268 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
1269
1270 die "unable to remove VM $vmid - used in HA resources\n"
1271 if PVE::HA::Config::vm_is_ha_managed($vmid);
1272
1273 # early tests (repeat after locking)
1274 die "VM $vmid is running - destroy failed\n"
1275 if PVE::QemuServer::check_running($vmid);
1276
1277 my $realcmd = sub {
1278 my $upid = shift;
1279
1280 syslog('info', "destroy VM $vmid: $upid\n");
1281
1282 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
1283
1284 PVE::AccessControl::remove_vm_access($vmid);
1285
1286 PVE::Firewall::remove_vmfw_conf($vmid);
1287 };
1288
1289 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1290 }});
1291
1292 __PACKAGE__->register_method({
1293 name => 'unlink',
1294 path => '{vmid}/unlink',
1295 method => 'PUT',
1296 protected => 1,
1297 proxyto => 'node',
1298 description => "Unlink/delete disk images.",
1299 permissions => {
1300 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1301 },
1302 parameters => {
1303 additionalProperties => 0,
1304 properties => {
1305 node => get_standard_option('pve-node'),
1306 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1307 idlist => {
1308 type => 'string', format => 'pve-configid-list',
1309 description => "A list of disk IDs you want to delete.",
1310 },
1311 force => {
1312 type => 'boolean',
1313 description => $opt_force_description,
1314 optional => 1,
1315 },
1316 },
1317 },
1318 returns => { type => 'null'},
1319 code => sub {
1320 my ($param) = @_;
1321
1322 $param->{delete} = extract_param($param, 'idlist');
1323
1324 __PACKAGE__->update_vm($param);
1325
1326 return undef;
1327 }});
1328
1329 my $sslcert;
1330
1331 __PACKAGE__->register_method({
1332 name => 'vncproxy',
1333 path => '{vmid}/vncproxy',
1334 method => 'POST',
1335 protected => 1,
1336 permissions => {
1337 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1338 },
1339 description => "Creates a TCP VNC proxy connections.",
1340 parameters => {
1341 additionalProperties => 0,
1342 properties => {
1343 node => get_standard_option('pve-node'),
1344 vmid => get_standard_option('pve-vmid'),
1345 websocket => {
1346 optional => 1,
1347 type => 'boolean',
1348 description => "starts websockify instead of vncproxy",
1349 },
1350 },
1351 },
1352 returns => {
1353 additionalProperties => 0,
1354 properties => {
1355 user => { type => 'string' },
1356 ticket => { type => 'string' },
1357 cert => { type => 'string' },
1358 port => { type => 'integer' },
1359 upid => { type => 'string' },
1360 },
1361 },
1362 code => sub {
1363 my ($param) = @_;
1364
1365 my $rpcenv = PVE::RPCEnvironment::get();
1366
1367 my $authuser = $rpcenv->get_user();
1368
1369 my $vmid = $param->{vmid};
1370 my $node = $param->{node};
1371 my $websocket = $param->{websocket};
1372
1373 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1374
1375 my $authpath = "/vms/$vmid";
1376
1377 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1378
1379 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1380 if !$sslcert;
1381
1382 my ($remip, $family);
1383 my $remcmd = [];
1384
1385 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1386 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
1387 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1388 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1389 } else {
1390 $family = PVE::Tools::get_host_address_family($node);
1391 }
1392
1393 my $port = PVE::Tools::next_vnc_port($family);
1394
1395 my $timeout = 10;
1396
1397 my $realcmd = sub {
1398 my $upid = shift;
1399
1400 syslog('info', "starting vnc proxy $upid\n");
1401
1402 my $cmd;
1403
1404 if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) {
1405
1406 die "Websocket mode is not supported in vga serial mode!" if $websocket;
1407
1408 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ];
1409 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1410 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1411 '-timeout', $timeout, '-authpath', $authpath,
1412 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1413 PVE::Tools::run_command($cmd);
1414 } else {
1415
1416 $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
1417
1418 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1419
1420 my $sock = IO::Socket::IP->new(
1421 Listen => 1,
1422 LocalPort => $port,
1423 Proto => 'tcp',
1424 GetAddrInfoFlags => 0,
1425 ) or die "failed to create socket: $!\n";
1426 # Inside the worker we shouldn't have any previous alarms
1427 # running anyway...:
1428 alarm(0);
1429 local $SIG{ALRM} = sub { die "connection timed out\n" };
1430 alarm $timeout;
1431 accept(my $cli, $sock) or die "connection failed: $!\n";
1432 close($sock);
1433 if (PVE::Tools::run_command($cmd,
1434 output => '>&'.fileno($cli),
1435 input => '<&'.fileno($cli),
1436 noerr => 1) != 0)
1437 {
1438 die "Failed to run vncproxy.\n";
1439 }
1440 }
1441
1442 return;
1443 };
1444
1445 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
1446
1447 PVE::Tools::wait_for_vnc_port($port);
1448
1449 return {
1450 user => $authuser,
1451 ticket => $ticket,
1452 port => $port,
1453 upid => $upid,
1454 cert => $sslcert,
1455 };
1456 }});
1457
1458 __PACKAGE__->register_method({
1459 name => 'vncwebsocket',
1460 path => '{vmid}/vncwebsocket',
1461 method => 'GET',
1462 permissions => {
1463 description => "You also need to pass a valid ticket (vncticket).",
1464 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1465 },
1466 description => "Opens a weksocket for VNC traffic.",
1467 parameters => {
1468 additionalProperties => 0,
1469 properties => {
1470 node => get_standard_option('pve-node'),
1471 vmid => get_standard_option('pve-vmid'),
1472 vncticket => {
1473 description => "Ticket from previous call to vncproxy.",
1474 type => 'string',
1475 maxLength => 512,
1476 },
1477 port => {
1478 description => "Port number returned by previous vncproxy call.",
1479 type => 'integer',
1480 minimum => 5900,
1481 maximum => 5999,
1482 },
1483 },
1484 },
1485 returns => {
1486 type => "object",
1487 properties => {
1488 port => { type => 'string' },
1489 },
1490 },
1491 code => sub {
1492 my ($param) = @_;
1493
1494 my $rpcenv = PVE::RPCEnvironment::get();
1495
1496 my $authuser = $rpcenv->get_user();
1497
1498 my $vmid = $param->{vmid};
1499 my $node = $param->{node};
1500
1501 my $authpath = "/vms/$vmid";
1502
1503 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1504
1505 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
1506
1507 # Note: VNC ports are acessible from outside, so we do not gain any
1508 # security if we verify that $param->{port} belongs to VM $vmid. This
1509 # check is done by verifying the VNC ticket (inside VNC protocol).
1510
1511 my $port = $param->{port};
1512
1513 return { port => $port };
1514 }});
1515
1516 __PACKAGE__->register_method({
1517 name => 'spiceproxy',
1518 path => '{vmid}/spiceproxy',
1519 method => 'POST',
1520 protected => 1,
1521 proxyto => 'node',
1522 permissions => {
1523 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1524 },
1525 description => "Returns a SPICE configuration to connect to the VM.",
1526 parameters => {
1527 additionalProperties => 0,
1528 properties => {
1529 node => get_standard_option('pve-node'),
1530 vmid => get_standard_option('pve-vmid'),
1531 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1532 },
1533 },
1534 returns => get_standard_option('remote-viewer-config'),
1535 code => sub {
1536 my ($param) = @_;
1537
1538 my $rpcenv = PVE::RPCEnvironment::get();
1539
1540 my $authuser = $rpcenv->get_user();
1541
1542 my $vmid = $param->{vmid};
1543 my $node = $param->{node};
1544 my $proxy = $param->{proxy};
1545
1546 my $conf = PVE::QemuConfig->load_config($vmid, $node);
1547 my $title = "VM $vmid";
1548 $title .= " - ". $conf->{name} if $conf->{name};
1549
1550 my $port = PVE::QemuServer::spice_port($vmid);
1551
1552 my ($ticket, undef, $remote_viewer_config) =
1553 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
1554
1555 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1556 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1557
1558 return $remote_viewer_config;
1559 }});
1560
1561 __PACKAGE__->register_method({
1562 name => 'vmcmdidx',
1563 path => '{vmid}/status',
1564 method => 'GET',
1565 proxyto => 'node',
1566 description => "Directory index",
1567 permissions => {
1568 user => 'all',
1569 },
1570 parameters => {
1571 additionalProperties => 0,
1572 properties => {
1573 node => get_standard_option('pve-node'),
1574 vmid => get_standard_option('pve-vmid'),
1575 },
1576 },
1577 returns => {
1578 type => 'array',
1579 items => {
1580 type => "object",
1581 properties => {
1582 subdir => { type => 'string' },
1583 },
1584 },
1585 links => [ { rel => 'child', href => "{subdir}" } ],
1586 },
1587 code => sub {
1588 my ($param) = @_;
1589
1590 # test if VM exists
1591 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1592
1593 my $res = [
1594 { subdir => 'current' },
1595 { subdir => 'start' },
1596 { subdir => 'stop' },
1597 ];
1598
1599 return $res;
1600 }});
1601
1602 __PACKAGE__->register_method({
1603 name => 'vm_status',
1604 path => '{vmid}/status/current',
1605 method => 'GET',
1606 proxyto => 'node',
1607 protected => 1, # qemu pid files are only readable by root
1608 description => "Get virtual machine status.",
1609 permissions => {
1610 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1611 },
1612 parameters => {
1613 additionalProperties => 0,
1614 properties => {
1615 node => get_standard_option('pve-node'),
1616 vmid => get_standard_option('pve-vmid'),
1617 },
1618 },
1619 returns => { type => 'object' },
1620 code => sub {
1621 my ($param) = @_;
1622
1623 # test if VM exists
1624 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1625
1626 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1627 my $status = $vmstatus->{$param->{vmid}};
1628
1629 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
1630
1631 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1632
1633 return $status;
1634 }});
1635
1636 __PACKAGE__->register_method({
1637 name => 'vm_start',
1638 path => '{vmid}/status/start',
1639 method => 'POST',
1640 protected => 1,
1641 proxyto => 'node',
1642 description => "Start virtual machine.",
1643 permissions => {
1644 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1645 },
1646 parameters => {
1647 additionalProperties => 0,
1648 properties => {
1649 node => get_standard_option('pve-node'),
1650 vmid => get_standard_option('pve-vmid',
1651 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1652 skiplock => get_standard_option('skiplock'),
1653 stateuri => get_standard_option('pve-qm-stateuri'),
1654 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1655 migration_type => {
1656 type => 'string',
1657 enum => ['secure', 'insecure'],
1658 description => "Migration traffic is encrypted using an SSH " .
1659 "tunnel by default. On secure, completely private networks " .
1660 "this can be disabled to increase performance.",
1661 optional => 1,
1662 },
1663 migration_network => {
1664 type => 'string', format => 'CIDR',
1665 description => "CIDR of the (sub) network that is used for migration.",
1666 optional => 1,
1667 },
1668 machine => get_standard_option('pve-qm-machine'),
1669 targetstorage => {
1670 description => "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1671 type => 'string',
1672 optional => 1
1673 }
1674 },
1675 },
1676 returns => {
1677 type => 'string',
1678 },
1679 code => sub {
1680 my ($param) = @_;
1681
1682 my $rpcenv = PVE::RPCEnvironment::get();
1683
1684 my $authuser = $rpcenv->get_user();
1685
1686 my $node = extract_param($param, 'node');
1687
1688 my $vmid = extract_param($param, 'vmid');
1689
1690 my $machine = extract_param($param, 'machine');
1691
1692 my $stateuri = extract_param($param, 'stateuri');
1693 raise_param_exc({ stateuri => "Only root may use this option." })
1694 if $stateuri && $authuser ne 'root@pam';
1695
1696 my $skiplock = extract_param($param, 'skiplock');
1697 raise_param_exc({ skiplock => "Only root may use this option." })
1698 if $skiplock && $authuser ne 'root@pam';
1699
1700 my $migratedfrom = extract_param($param, 'migratedfrom');
1701 raise_param_exc({ migratedfrom => "Only root may use this option." })
1702 if $migratedfrom && $authuser ne 'root@pam';
1703
1704 my $migration_type = extract_param($param, 'migration_type');
1705 raise_param_exc({ migration_type => "Only root may use this option." })
1706 if $migration_type && $authuser ne 'root@pam';
1707
1708 my $migration_network = extract_param($param, 'migration_network');
1709 raise_param_exc({ migration_network => "Only root may use this option." })
1710 if $migration_network && $authuser ne 'root@pam';
1711
1712 my $targetstorage = extract_param($param, 'targetstorage');
1713 raise_param_exc({ targetstorage => "Only root may use this option." })
1714 if $targetstorage && $authuser ne 'root@pam';
1715
1716 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
1717 if $targetstorage && !$migratedfrom;
1718
1719 # read spice ticket from STDIN
1720 my $spice_ticket;
1721 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
1722 if (defined(my $line = <>)) {
1723 chomp $line;
1724 $spice_ticket = $line;
1725 }
1726 }
1727
1728 PVE::Cluster::check_cfs_quorum();
1729
1730 my $storecfg = PVE::Storage::config();
1731
1732 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri &&
1733 $rpcenv->{type} ne 'ha') {
1734
1735 my $hacmd = sub {
1736 my $upid = shift;
1737
1738 my $service = "vm:$vmid";
1739
1740 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
1741
1742 print "Executing HA start for VM $vmid\n";
1743
1744 PVE::Tools::run_command($cmd);
1745
1746 return;
1747 };
1748
1749 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1750
1751 } else {
1752
1753 my $realcmd = sub {
1754 my $upid = shift;
1755
1756 syslog('info', "start VM $vmid: $upid\n");
1757
1758 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1759 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
1760
1761 return;
1762 };
1763
1764 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1765 }
1766 }});
1767
1768 __PACKAGE__->register_method({
1769 name => 'vm_stop',
1770 path => '{vmid}/status/stop',
1771 method => 'POST',
1772 protected => 1,
1773 proxyto => 'node',
1774 description => "Stop virtual machine. The qemu process will exit immediately. This" .
1775 "is akin to pulling the power plug of a running computer and may damage the VM data",
1776 permissions => {
1777 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1778 },
1779 parameters => {
1780 additionalProperties => 0,
1781 properties => {
1782 node => get_standard_option('pve-node'),
1783 vmid => get_standard_option('pve-vmid',
1784 { completion => \&PVE::QemuServer::complete_vmid_running }),
1785 skiplock => get_standard_option('skiplock'),
1786 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
1787 timeout => {
1788 description => "Wait maximal timeout seconds.",
1789 type => 'integer',
1790 minimum => 0,
1791 optional => 1,
1792 },
1793 keepActive => {
1794 description => "Do not deactivate storage volumes.",
1795 type => 'boolean',
1796 optional => 1,
1797 default => 0,
1798 }
1799 },
1800 },
1801 returns => {
1802 type => 'string',
1803 },
1804 code => sub {
1805 my ($param) = @_;
1806
1807 my $rpcenv = PVE::RPCEnvironment::get();
1808
1809 my $authuser = $rpcenv->get_user();
1810
1811 my $node = extract_param($param, 'node');
1812
1813 my $vmid = extract_param($param, 'vmid');
1814
1815 my $skiplock = extract_param($param, 'skiplock');
1816 raise_param_exc({ skiplock => "Only root may use this option." })
1817 if $skiplock && $authuser ne 'root@pam';
1818
1819 my $keepActive = extract_param($param, 'keepActive');
1820 raise_param_exc({ keepActive => "Only root may use this option." })
1821 if $keepActive && $authuser ne 'root@pam';
1822
1823 my $migratedfrom = extract_param($param, 'migratedfrom');
1824 raise_param_exc({ migratedfrom => "Only root may use this option." })
1825 if $migratedfrom && $authuser ne 'root@pam';
1826
1827
1828 my $storecfg = PVE::Storage::config();
1829
1830 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
1831
1832 my $hacmd = sub {
1833 my $upid = shift;
1834
1835 my $service = "vm:$vmid";
1836
1837 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
1838
1839 print "Executing HA stop for VM $vmid\n";
1840
1841 PVE::Tools::run_command($cmd);
1842
1843 return;
1844 };
1845
1846 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1847
1848 } else {
1849 my $realcmd = sub {
1850 my $upid = shift;
1851
1852 syslog('info', "stop VM $vmid: $upid\n");
1853
1854 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
1855 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
1856
1857 return;
1858 };
1859
1860 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1861 }
1862 }});
1863
1864 __PACKAGE__->register_method({
1865 name => 'vm_reset',
1866 path => '{vmid}/status/reset',
1867 method => 'POST',
1868 protected => 1,
1869 proxyto => 'node',
1870 description => "Reset virtual machine.",
1871 permissions => {
1872 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1873 },
1874 parameters => {
1875 additionalProperties => 0,
1876 properties => {
1877 node => get_standard_option('pve-node'),
1878 vmid => get_standard_option('pve-vmid',
1879 { completion => \&PVE::QemuServer::complete_vmid_running }),
1880 skiplock => get_standard_option('skiplock'),
1881 },
1882 },
1883 returns => {
1884 type => 'string',
1885 },
1886 code => sub {
1887 my ($param) = @_;
1888
1889 my $rpcenv = PVE::RPCEnvironment::get();
1890
1891 my $authuser = $rpcenv->get_user();
1892
1893 my $node = extract_param($param, 'node');
1894
1895 my $vmid = extract_param($param, 'vmid');
1896
1897 my $skiplock = extract_param($param, 'skiplock');
1898 raise_param_exc({ skiplock => "Only root may use this option." })
1899 if $skiplock && $authuser ne 'root@pam';
1900
1901 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1902
1903 my $realcmd = sub {
1904 my $upid = shift;
1905
1906 PVE::QemuServer::vm_reset($vmid, $skiplock);
1907
1908 return;
1909 };
1910
1911 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1912 }});
1913
1914 __PACKAGE__->register_method({
1915 name => 'vm_shutdown',
1916 path => '{vmid}/status/shutdown',
1917 method => 'POST',
1918 protected => 1,
1919 proxyto => 'node',
1920 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
1921 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
1922 permissions => {
1923 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1924 },
1925 parameters => {
1926 additionalProperties => 0,
1927 properties => {
1928 node => get_standard_option('pve-node'),
1929 vmid => get_standard_option('pve-vmid',
1930 { completion => \&PVE::QemuServer::complete_vmid_running }),
1931 skiplock => get_standard_option('skiplock'),
1932 timeout => {
1933 description => "Wait maximal timeout seconds.",
1934 type => 'integer',
1935 minimum => 0,
1936 optional => 1,
1937 },
1938 forceStop => {
1939 description => "Make sure the VM stops.",
1940 type => 'boolean',
1941 optional => 1,
1942 default => 0,
1943 },
1944 keepActive => {
1945 description => "Do not deactivate storage volumes.",
1946 type => 'boolean',
1947 optional => 1,
1948 default => 0,
1949 }
1950 },
1951 },
1952 returns => {
1953 type => 'string',
1954 },
1955 code => sub {
1956 my ($param) = @_;
1957
1958 my $rpcenv = PVE::RPCEnvironment::get();
1959
1960 my $authuser = $rpcenv->get_user();
1961
1962 my $node = extract_param($param, 'node');
1963
1964 my $vmid = extract_param($param, 'vmid');
1965
1966 my $skiplock = extract_param($param, 'skiplock');
1967 raise_param_exc({ skiplock => "Only root may use this option." })
1968 if $skiplock && $authuser ne 'root@pam';
1969
1970 my $keepActive = extract_param($param, 'keepActive');
1971 raise_param_exc({ keepActive => "Only root may use this option." })
1972 if $keepActive && $authuser ne 'root@pam';
1973
1974 my $storecfg = PVE::Storage::config();
1975
1976 my $shutdown = 1;
1977
1978 # if vm is paused, do not shutdown (but stop if forceStop = 1)
1979 # otherwise, we will infer a shutdown command, but run into the timeout,
1980 # then when the vm is resumed, it will instantly shutdown
1981 #
1982 # checking the qmp status here to get feedback to the gui/cli/api
1983 # and the status query should not take too long
1984 my $qmpstatus;
1985 eval {
1986 $qmpstatus = PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
1987 };
1988 my $err = $@ if $@;
1989
1990 if (!$err && $qmpstatus->{status} eq "paused") {
1991 if ($param->{forceStop}) {
1992 warn "VM is paused - stop instead of shutdown\n";
1993 $shutdown = 0;
1994 } else {
1995 die "VM is paused - cannot shutdown\n";
1996 }
1997 }
1998
1999 if (PVE::HA::Config::vm_is_ha_managed($vmid) &&
2000 ($rpcenv->{type} ne 'ha')) {
2001
2002 my $hacmd = sub {
2003 my $upid = shift;
2004
2005 my $service = "vm:$vmid";
2006
2007 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2008
2009 print "Executing HA stop for VM $vmid\n";
2010
2011 PVE::Tools::run_command($cmd);
2012
2013 return;
2014 };
2015
2016 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2017
2018 } else {
2019
2020 my $realcmd = sub {
2021 my $upid = shift;
2022
2023 syslog('info', "shutdown VM $vmid: $upid\n");
2024
2025 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2026 $shutdown, $param->{forceStop}, $keepActive);
2027
2028 return;
2029 };
2030
2031 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2032 }
2033 }});
2034
2035 __PACKAGE__->register_method({
2036 name => 'vm_suspend',
2037 path => '{vmid}/status/suspend',
2038 method => 'POST',
2039 protected => 1,
2040 proxyto => 'node',
2041 description => "Suspend virtual machine.",
2042 permissions => {
2043 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2044 },
2045 parameters => {
2046 additionalProperties => 0,
2047 properties => {
2048 node => get_standard_option('pve-node'),
2049 vmid => get_standard_option('pve-vmid',
2050 { completion => \&PVE::QemuServer::complete_vmid_running }),
2051 skiplock => get_standard_option('skiplock'),
2052 },
2053 },
2054 returns => {
2055 type => 'string',
2056 },
2057 code => sub {
2058 my ($param) = @_;
2059
2060 my $rpcenv = PVE::RPCEnvironment::get();
2061
2062 my $authuser = $rpcenv->get_user();
2063
2064 my $node = extract_param($param, 'node');
2065
2066 my $vmid = extract_param($param, 'vmid');
2067
2068 my $skiplock = extract_param($param, 'skiplock');
2069 raise_param_exc({ skiplock => "Only root may use this option." })
2070 if $skiplock && $authuser ne 'root@pam';
2071
2072 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2073
2074 my $realcmd = sub {
2075 my $upid = shift;
2076
2077 syslog('info', "suspend VM $vmid: $upid\n");
2078
2079 PVE::QemuServer::vm_suspend($vmid, $skiplock);
2080
2081 return;
2082 };
2083
2084 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
2085 }});
2086
2087 __PACKAGE__->register_method({
2088 name => 'vm_resume',
2089 path => '{vmid}/status/resume',
2090 method => 'POST',
2091 protected => 1,
2092 proxyto => 'node',
2093 description => "Resume virtual machine.",
2094 permissions => {
2095 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2096 },
2097 parameters => {
2098 additionalProperties => 0,
2099 properties => {
2100 node => get_standard_option('pve-node'),
2101 vmid => get_standard_option('pve-vmid',
2102 { completion => \&PVE::QemuServer::complete_vmid_running }),
2103 skiplock => get_standard_option('skiplock'),
2104 nocheck => { type => 'boolean', optional => 1 },
2105
2106 },
2107 },
2108 returns => {
2109 type => 'string',
2110 },
2111 code => sub {
2112 my ($param) = @_;
2113
2114 my $rpcenv = PVE::RPCEnvironment::get();
2115
2116 my $authuser = $rpcenv->get_user();
2117
2118 my $node = extract_param($param, 'node');
2119
2120 my $vmid = extract_param($param, 'vmid');
2121
2122 my $skiplock = extract_param($param, 'skiplock');
2123 raise_param_exc({ skiplock => "Only root may use this option." })
2124 if $skiplock && $authuser ne 'root@pam';
2125
2126 my $nocheck = extract_param($param, 'nocheck');
2127
2128 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid, $nocheck);
2129
2130 my $realcmd = sub {
2131 my $upid = shift;
2132
2133 syslog('info', "resume VM $vmid: $upid\n");
2134
2135 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2136
2137 return;
2138 };
2139
2140 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2141 }});
2142
2143 __PACKAGE__->register_method({
2144 name => 'vm_sendkey',
2145 path => '{vmid}/sendkey',
2146 method => 'PUT',
2147 protected => 1,
2148 proxyto => 'node',
2149 description => "Send key event to virtual machine.",
2150 permissions => {
2151 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2152 },
2153 parameters => {
2154 additionalProperties => 0,
2155 properties => {
2156 node => get_standard_option('pve-node'),
2157 vmid => get_standard_option('pve-vmid',
2158 { completion => \&PVE::QemuServer::complete_vmid_running }),
2159 skiplock => get_standard_option('skiplock'),
2160 key => {
2161 description => "The key (qemu monitor encoding).",
2162 type => 'string'
2163 }
2164 },
2165 },
2166 returns => { type => 'null'},
2167 code => sub {
2168 my ($param) = @_;
2169
2170 my $rpcenv = PVE::RPCEnvironment::get();
2171
2172 my $authuser = $rpcenv->get_user();
2173
2174 my $node = extract_param($param, 'node');
2175
2176 my $vmid = extract_param($param, 'vmid');
2177
2178 my $skiplock = extract_param($param, 'skiplock');
2179 raise_param_exc({ skiplock => "Only root may use this option." })
2180 if $skiplock && $authuser ne 'root@pam';
2181
2182 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2183
2184 return;
2185 }});
2186
2187 __PACKAGE__->register_method({
2188 name => 'vm_feature',
2189 path => '{vmid}/feature',
2190 method => 'GET',
2191 proxyto => 'node',
2192 protected => 1,
2193 description => "Check if feature for virtual machine is available.",
2194 permissions => {
2195 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2196 },
2197 parameters => {
2198 additionalProperties => 0,
2199 properties => {
2200 node => get_standard_option('pve-node'),
2201 vmid => get_standard_option('pve-vmid'),
2202 feature => {
2203 description => "Feature to check.",
2204 type => 'string',
2205 enum => [ 'snapshot', 'clone', 'copy' ],
2206 },
2207 snapname => get_standard_option('pve-snapshot-name', {
2208 optional => 1,
2209 }),
2210 },
2211 },
2212 returns => {
2213 type => "object",
2214 properties => {
2215 hasFeature => { type => 'boolean' },
2216 nodes => {
2217 type => 'array',
2218 items => { type => 'string' },
2219 }
2220 },
2221 },
2222 code => sub {
2223 my ($param) = @_;
2224
2225 my $node = extract_param($param, 'node');
2226
2227 my $vmid = extract_param($param, 'vmid');
2228
2229 my $snapname = extract_param($param, 'snapname');
2230
2231 my $feature = extract_param($param, 'feature');
2232
2233 my $running = PVE::QemuServer::check_running($vmid);
2234
2235 my $conf = PVE::QemuConfig->load_config($vmid);
2236
2237 if($snapname){
2238 my $snap = $conf->{snapshots}->{$snapname};
2239 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2240 $conf = $snap;
2241 }
2242 my $storecfg = PVE::Storage::config();
2243
2244 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2245 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
2246
2247 return {
2248 hasFeature => $hasFeature,
2249 nodes => [ keys %$nodelist ],
2250 };
2251 }});
2252
2253 __PACKAGE__->register_method({
2254 name => 'clone_vm',
2255 path => '{vmid}/clone',
2256 method => 'POST',
2257 protected => 1,
2258 proxyto => 'node',
2259 description => "Create a copy of virtual machine/template.",
2260 permissions => {
2261 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2262 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2263 "'Datastore.AllocateSpace' on any used storage.",
2264 check =>
2265 [ 'and',
2266 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2267 [ 'or',
2268 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2269 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2270 ],
2271 ]
2272 },
2273 parameters => {
2274 additionalProperties => 0,
2275 properties => {
2276 node => get_standard_option('pve-node'),
2277 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2278 newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }),
2279 name => {
2280 optional => 1,
2281 type => 'string', format => 'dns-name',
2282 description => "Set a name for the new VM.",
2283 },
2284 description => {
2285 optional => 1,
2286 type => 'string',
2287 description => "Description for the new VM.",
2288 },
2289 pool => {
2290 optional => 1,
2291 type => 'string', format => 'pve-poolid',
2292 description => "Add the new VM to the specified pool.",
2293 },
2294 snapname => get_standard_option('pve-snapshot-name', {
2295 optional => 1,
2296 }),
2297 storage => get_standard_option('pve-storage-id', {
2298 description => "Target storage for full clone.",
2299 requires => 'full',
2300 optional => 1,
2301 }),
2302 'format' => {
2303 description => "Target format for file storage.",
2304 requires => 'full',
2305 type => 'string',
2306 optional => 1,
2307 enum => [ 'raw', 'qcow2', 'vmdk'],
2308 },
2309 full => {
2310 optional => 1,
2311 type => 'boolean',
2312 description => "Create a full copy of all disk. This is always done when " .
2313 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2314 default => 0,
2315 },
2316 target => get_standard_option('pve-node', {
2317 description => "Target node. Only allowed if the original VM is on shared storage.",
2318 optional => 1,
2319 }),
2320 },
2321 },
2322 returns => {
2323 type => 'string',
2324 },
2325 code => sub {
2326 my ($param) = @_;
2327
2328 my $rpcenv = PVE::RPCEnvironment::get();
2329
2330 my $authuser = $rpcenv->get_user();
2331
2332 my $node = extract_param($param, 'node');
2333
2334 my $vmid = extract_param($param, 'vmid');
2335
2336 my $newid = extract_param($param, 'newid');
2337
2338 my $pool = extract_param($param, 'pool');
2339
2340 if (defined($pool)) {
2341 $rpcenv->check_pool_exist($pool);
2342 }
2343
2344 my $snapname = extract_param($param, 'snapname');
2345
2346 my $storage = extract_param($param, 'storage');
2347
2348 my $format = extract_param($param, 'format');
2349
2350 my $target = extract_param($param, 'target');
2351
2352 my $localnode = PVE::INotify::nodename();
2353
2354 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2355
2356 PVE::Cluster::check_node_exists($target) if $target;
2357
2358 my $storecfg = PVE::Storage::config();
2359
2360 if ($storage) {
2361 # check if storage is enabled on local node
2362 PVE::Storage::storage_check_enabled($storecfg, $storage);
2363 if ($target) {
2364 # check if storage is available on target node
2365 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2366 # clone only works if target storage is shared
2367 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2368 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2369 }
2370 }
2371
2372 PVE::Cluster::check_cfs_quorum();
2373
2374 my $running = PVE::QemuServer::check_running($vmid) || 0;
2375
2376 # exclusive lock if VM is running - else shared lock is enough;
2377 my $shared_lock = $running ? 0 : 1;
2378
2379 my $clonefn = sub {
2380
2381 # do all tests after lock
2382 # we also try to do all tests before we fork the worker
2383
2384 my $conf = PVE::QemuConfig->load_config($vmid);
2385
2386 PVE::QemuConfig->check_lock($conf);
2387
2388 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2389
2390 die "unexpected state change\n" if $verify_running != $running;
2391
2392 die "snapshot '$snapname' does not exist\n"
2393 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2394
2395 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2396
2397 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2398
2399 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2400
2401 my $conffile = PVE::QemuConfig->config_file($newid);
2402
2403 die "unable to create VM $newid: config file already exists\n"
2404 if -f $conffile;
2405
2406 my $newconf = { lock => 'clone' };
2407 my $drives = {};
2408 my $fullclone = {};
2409 my $vollist = [];
2410
2411 foreach my $opt (keys %$oldconf) {
2412 my $value = $oldconf->{$opt};
2413
2414 # do not copy snapshot related info
2415 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2416 $opt eq 'vmstate' || $opt eq 'snapstate';
2417
2418 # no need to copy unused images, because VMID(owner) changes anyways
2419 next if $opt =~ m/^unused\d+$/;
2420
2421 # always change MAC! address
2422 if ($opt =~ m/^net(\d+)$/) {
2423 my $net = PVE::QemuServer::parse_net($value);
2424 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2425 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
2426 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2427 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
2428 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2429 die "unable to parse drive options for '$opt'\n" if !$drive;
2430 if (PVE::QemuServer::drive_is_cdrom($drive)) {
2431 $newconf->{$opt} = $value; # simply copy configuration
2432 } else {
2433 if ($param->{full}) {
2434 die "Full clone feature is not available"
2435 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2436 $fullclone->{$opt} = 1;
2437 } else {
2438 # not full means clone instead of copy
2439 die "Linked clone feature is not available"
2440 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
2441 }
2442 $drives->{$opt} = $drive;
2443 push @$vollist, $drive->{file};
2444 }
2445 } else {
2446 # copy everything else
2447 $newconf->{$opt} = $value;
2448 }
2449 }
2450
2451 # auto generate a new uuid
2452 my ($uuid, $uuid_str);
2453 UUID::generate($uuid);
2454 UUID::unparse($uuid, $uuid_str);
2455 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
2456 $smbios1->{uuid} = $uuid_str;
2457 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2458
2459 delete $newconf->{template};
2460
2461 if ($param->{name}) {
2462 $newconf->{name} = $param->{name};
2463 } else {
2464 if ($oldconf->{name}) {
2465 $newconf->{name} = "Copy-of-$oldconf->{name}";
2466 } else {
2467 $newconf->{name} = "Copy-of-VM-$vmid";
2468 }
2469 }
2470
2471 if ($param->{description}) {
2472 $newconf->{description} = $param->{description};
2473 }
2474
2475 # create empty/temp config - this fails if VM already exists on other node
2476 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2477
2478 my $realcmd = sub {
2479 my $upid = shift;
2480
2481 my $newvollist = [];
2482 my $jobs = {};
2483
2484 eval {
2485 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2486
2487 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
2488
2489 my $total_jobs = scalar(keys %{$drives});
2490 my $i = 1;
2491
2492 foreach my $opt (keys %$drives) {
2493 my $drive = $drives->{$opt};
2494 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2495
2496 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2497 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2498 $jobs, $skipcomplete, $oldconf->{agent});
2499
2500 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2501
2502 PVE::QemuConfig->write_config($newid, $newconf);
2503 $i++;
2504 }
2505
2506 delete $newconf->{lock};
2507 PVE::QemuConfig->write_config($newid, $newconf);
2508
2509 if ($target) {
2510 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2511 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
2512 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
2513
2514 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
2515 die "Failed to move config to node '$target' - rename failed: $!\n"
2516 if !rename($conffile, $newconffile);
2517 }
2518
2519 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2520 };
2521 if (my $err = $@) {
2522 unlink $conffile;
2523
2524 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2525
2526 sleep 1; # some storage like rbd need to wait before release volume - really?
2527
2528 foreach my $volid (@$newvollist) {
2529 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2530 warn $@ if $@;
2531 }
2532 die "clone failed: $err";
2533 }
2534
2535 return;
2536 };
2537
2538 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2539
2540 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2541 };
2542
2543 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
2544 # Aquire exclusive lock lock for $newid
2545 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
2546 });
2547
2548 }});
2549
2550 __PACKAGE__->register_method({
2551 name => 'move_vm_disk',
2552 path => '{vmid}/move_disk',
2553 method => 'POST',
2554 protected => 1,
2555 proxyto => 'node',
2556 description => "Move volume to different storage.",
2557 permissions => {
2558 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2559 check => [ 'and',
2560 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2561 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2562 ],
2563 },
2564 parameters => {
2565 additionalProperties => 0,
2566 properties => {
2567 node => get_standard_option('pve-node'),
2568 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2569 disk => {
2570 type => 'string',
2571 description => "The disk you want to move.",
2572 enum => [ PVE::QemuServer::valid_drive_names() ],
2573 },
2574 storage => get_standard_option('pve-storage-id', {
2575 description => "Target storage.",
2576 completion => \&PVE::QemuServer::complete_storage,
2577 }),
2578 'format' => {
2579 type => 'string',
2580 description => "Target Format.",
2581 enum => [ 'raw', 'qcow2', 'vmdk' ],
2582 optional => 1,
2583 },
2584 delete => {
2585 type => 'boolean',
2586 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2587 optional => 1,
2588 default => 0,
2589 },
2590 digest => {
2591 type => 'string',
2592 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2593 maxLength => 40,
2594 optional => 1,
2595 },
2596 },
2597 },
2598 returns => {
2599 type => 'string',
2600 description => "the task ID.",
2601 },
2602 code => sub {
2603 my ($param) = @_;
2604
2605 my $rpcenv = PVE::RPCEnvironment::get();
2606
2607 my $authuser = $rpcenv->get_user();
2608
2609 my $node = extract_param($param, 'node');
2610
2611 my $vmid = extract_param($param, 'vmid');
2612
2613 my $digest = extract_param($param, 'digest');
2614
2615 my $disk = extract_param($param, 'disk');
2616
2617 my $storeid = extract_param($param, 'storage');
2618
2619 my $format = extract_param($param, 'format');
2620
2621 my $storecfg = PVE::Storage::config();
2622
2623 my $updatefn = sub {
2624
2625 my $conf = PVE::QemuConfig->load_config($vmid);
2626
2627 PVE::QemuConfig->check_lock($conf);
2628
2629 die "checksum missmatch (file change by other user?)\n"
2630 if $digest && $digest ne $conf->{digest};
2631
2632 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2633
2634 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2635
2636 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
2637
2638 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
2639
2640 my $oldfmt;
2641 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
2642 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2643 $oldfmt = $1;
2644 }
2645
2646 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2647 (!$format || !$oldfmt || $oldfmt eq $format);
2648
2649 # this only checks snapshots because $disk is passed!
2650 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
2651 die "you can't move a disk with snapshots and delete the source\n"
2652 if $snapshotted && $param->{delete};
2653
2654 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2655
2656 my $running = PVE::QemuServer::check_running($vmid);
2657
2658 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
2659
2660 my $realcmd = sub {
2661
2662 my $newvollist = [];
2663
2664 eval {
2665 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2666
2667 warn "moving disk with snapshots, snapshots will not be moved!\n"
2668 if $snapshotted;
2669
2670 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
2671 $vmid, $storeid, $format, 1, $newvollist);
2672
2673 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
2674
2675 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
2676
2677 # convert moved disk to base if part of template
2678 PVE::QemuServer::template_create($vmid, $conf, $disk)
2679 if PVE::QemuConfig->is_template($conf);
2680
2681 PVE::QemuConfig->write_config($vmid, $conf);
2682
2683 eval {
2684 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2685 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
2686 if !$running;
2687 };
2688 warn $@ if $@;
2689 };
2690 if (my $err = $@) {
2691
2692 foreach my $volid (@$newvollist) {
2693 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2694 warn $@ if $@;
2695 }
2696 die "storage migration failed: $err";
2697 }
2698
2699 if ($param->{delete}) {
2700 eval {
2701 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
2702 PVE::Storage::vdisk_free($storecfg, $old_volid);
2703 };
2704 warn $@ if $@;
2705 }
2706 };
2707
2708 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2709 };
2710
2711 return PVE::QemuConfig->lock_config($vmid, $updatefn);
2712 }});
2713
2714 __PACKAGE__->register_method({
2715 name => 'migrate_vm',
2716 path => '{vmid}/migrate',
2717 method => 'POST',
2718 protected => 1,
2719 proxyto => 'node',
2720 description => "Migrate virtual machine. Creates a new migration task.",
2721 permissions => {
2722 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2723 },
2724 parameters => {
2725 additionalProperties => 0,
2726 properties => {
2727 node => get_standard_option('pve-node'),
2728 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2729 target => get_standard_option('pve-node', {
2730 description => "Target node.",
2731 completion => \&PVE::Cluster::complete_migration_target,
2732 }),
2733 online => {
2734 type => 'boolean',
2735 description => "Use online/live migration.",
2736 optional => 1,
2737 },
2738 force => {
2739 type => 'boolean',
2740 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
2741 optional => 1,
2742 },
2743 migration_type => {
2744 type => 'string',
2745 enum => ['secure', 'insecure'],
2746 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2747 optional => 1,
2748 },
2749 migration_network => {
2750 type => 'string', format => 'CIDR',
2751 description => "CIDR of the (sub) network that is used for migration.",
2752 optional => 1,
2753 },
2754 "with-local-disks" => {
2755 type => 'boolean',
2756 description => "Enable live storage migration for local disk",
2757 optional => 1,
2758 },
2759 targetstorage => get_standard_option('pve-storage-id', {
2760 description => "Default target storage.",
2761 optional => 1,
2762 completion => \&PVE::QemuServer::complete_storage,
2763 }),
2764 },
2765 },
2766 returns => {
2767 type => 'string',
2768 description => "the task ID.",
2769 },
2770 code => sub {
2771 my ($param) = @_;
2772
2773 my $rpcenv = PVE::RPCEnvironment::get();
2774
2775 my $authuser = $rpcenv->get_user();
2776
2777 my $target = extract_param($param, 'target');
2778
2779 my $localnode = PVE::INotify::nodename();
2780 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
2781
2782 PVE::Cluster::check_cfs_quorum();
2783
2784 PVE::Cluster::check_node_exists($target);
2785
2786 my $targetip = PVE::Cluster::remote_node_ip($target);
2787
2788 my $vmid = extract_param($param, 'vmid');
2789
2790 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
2791 if !$param->{online} && $param->{targetstorage};
2792
2793 raise_param_exc({ force => "Only root may use this option." })
2794 if $param->{force} && $authuser ne 'root@pam';
2795
2796 raise_param_exc({ migration_type => "Only root may use this option." })
2797 if $param->{migration_type} && $authuser ne 'root@pam';
2798
2799 # allow root only until better network permissions are available
2800 raise_param_exc({ migration_network => "Only root may use this option." })
2801 if $param->{migration_network} && $authuser ne 'root@pam';
2802
2803 # test if VM exists
2804 my $conf = PVE::QemuConfig->load_config($vmid);
2805
2806 # try to detect errors early
2807
2808 PVE::QemuConfig->check_lock($conf);
2809
2810 if (PVE::QemuServer::check_running($vmid)) {
2811 die "cant migrate running VM without --online\n"
2812 if !$param->{online};
2813 }
2814
2815 my $storecfg = PVE::Storage::config();
2816 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
2817
2818 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2819
2820 my $hacmd = sub {
2821 my $upid = shift;
2822
2823 my $service = "vm:$vmid";
2824
2825 my $cmd = ['ha-manager', 'migrate', $service, $target];
2826
2827 print "Executing HA migrate for VM $vmid to node $target\n";
2828
2829 PVE::Tools::run_command($cmd);
2830
2831 return;
2832 };
2833
2834 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2835
2836 } else {
2837
2838 my $realcmd = sub {
2839 my $upid = shift;
2840
2841 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
2842 };
2843
2844 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd);
2845 }
2846
2847 }});
2848
2849 __PACKAGE__->register_method({
2850 name => 'monitor',
2851 path => '{vmid}/monitor',
2852 method => 'POST',
2853 protected => 1,
2854 proxyto => 'node',
2855 description => "Execute Qemu monitor commands.",
2856 permissions => {
2857 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
2858 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2859 },
2860 parameters => {
2861 additionalProperties => 0,
2862 properties => {
2863 node => get_standard_option('pve-node'),
2864 vmid => get_standard_option('pve-vmid'),
2865 command => {
2866 type => 'string',
2867 description => "The monitor command.",
2868 }
2869 },
2870 },
2871 returns => { type => 'string'},
2872 code => sub {
2873 my ($param) = @_;
2874
2875 my $rpcenv = PVE::RPCEnvironment::get();
2876 my $authuser = $rpcenv->get_user();
2877
2878 my $is_ro = sub {
2879 my $command = shift;
2880 return $command =~ m/^\s*info(\s+|$)/
2881 || $command =~ m/^\s*help\s*$/;
2882 };
2883
2884 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
2885 if !&$is_ro($param->{command});
2886
2887 my $vmid = $param->{vmid};
2888
2889 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
2890
2891 my $res = '';
2892 eval {
2893 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
2894 };
2895 $res = "ERROR: $@" if $@;
2896
2897 return $res;
2898 }});
2899
2900 my $guest_agent_commands = [
2901 'ping',
2902 'get-time',
2903 'info',
2904 'fsfreeze-status',
2905 'fsfreeze-freeze',
2906 'fsfreeze-thaw',
2907 'fstrim',
2908 'network-get-interfaces',
2909 'get-vcpus',
2910 'get-fsinfo',
2911 'get-memory-blocks',
2912 'get-memory-block-info',
2913 'suspend-hybrid',
2914 'suspend-ram',
2915 'suspend-disk',
2916 'shutdown',
2917 ];
2918
2919 __PACKAGE__->register_method({
2920 name => 'agent',
2921 path => '{vmid}/agent',
2922 method => 'POST',
2923 protected => 1,
2924 proxyto => 'node',
2925 description => "Execute Qemu Guest Agent commands.",
2926 permissions => {
2927 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2928 },
2929 parameters => {
2930 additionalProperties => 0,
2931 properties => {
2932 node => get_standard_option('pve-node'),
2933 vmid => get_standard_option('pve-vmid', {
2934 completion => \&PVE::QemuServer::complete_vmid_running }),
2935 command => {
2936 type => 'string',
2937 description => "The QGA command.",
2938 enum => $guest_agent_commands,
2939 },
2940 },
2941 },
2942 returns => {
2943 type => 'object',
2944 description => "Returns an object with a single `result` property. The type of that
2945 property depends on the executed command.",
2946 },
2947 code => sub {
2948 my ($param) = @_;
2949
2950 my $vmid = $param->{vmid};
2951
2952 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
2953
2954 die "No Qemu Guest Agent\n" if !defined($conf->{agent});
2955 die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid);
2956
2957 my $cmd = $param->{command};
2958
2959 my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd");
2960
2961 return { result => $res };
2962 }});
2963
2964 __PACKAGE__->register_method({
2965 name => 'resize_vm',
2966 path => '{vmid}/resize',
2967 method => 'PUT',
2968 protected => 1,
2969 proxyto => 'node',
2970 description => "Extend volume size.",
2971 permissions => {
2972 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2973 },
2974 parameters => {
2975 additionalProperties => 0,
2976 properties => {
2977 node => get_standard_option('pve-node'),
2978 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2979 skiplock => get_standard_option('skiplock'),
2980 disk => {
2981 type => 'string',
2982 description => "The disk you want to resize.",
2983 enum => [PVE::QemuServer::valid_drive_names()],
2984 },
2985 size => {
2986 type => 'string',
2987 pattern => '\+?\d+(\.\d+)?[KMGT]?',
2988 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.",
2989 },
2990 digest => {
2991 type => 'string',
2992 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2993 maxLength => 40,
2994 optional => 1,
2995 },
2996 },
2997 },
2998 returns => { type => 'null'},
2999 code => sub {
3000 my ($param) = @_;
3001
3002 my $rpcenv = PVE::RPCEnvironment::get();
3003
3004 my $authuser = $rpcenv->get_user();
3005
3006 my $node = extract_param($param, 'node');
3007
3008 my $vmid = extract_param($param, 'vmid');
3009
3010 my $digest = extract_param($param, 'digest');
3011
3012 my $disk = extract_param($param, 'disk');
3013
3014 my $sizestr = extract_param($param, 'size');
3015
3016 my $skiplock = extract_param($param, 'skiplock');
3017 raise_param_exc({ skiplock => "Only root may use this option." })
3018 if $skiplock && $authuser ne 'root@pam';
3019
3020 my $storecfg = PVE::Storage::config();
3021
3022 my $updatefn = sub {
3023
3024 my $conf = PVE::QemuConfig->load_config($vmid);
3025
3026 die "checksum missmatch (file change by other user?)\n"
3027 if $digest && $digest ne $conf->{digest};
3028 PVE::QemuConfig->check_lock($conf) if !$skiplock;
3029
3030 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3031
3032 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3033
3034 my (undef, undef, undef, undef, undef, undef, $format) =
3035 PVE::Storage::parse_volname($storecfg, $drive->{file});
3036
3037 die "can't resize volume: $disk if snapshot exists\n"
3038 if %{$conf->{snapshots}} && $format eq 'qcow2';
3039
3040 my $volid = $drive->{file};
3041
3042 die "disk '$disk' has no associated volume\n" if !$volid;
3043
3044 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3045
3046 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3047
3048 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3049
3050 PVE::Storage::activate_volumes($storecfg, [$volid]);
3051 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3052
3053 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3054 my ($ext, $newsize, $unit) = ($1, $2, $4);
3055 if ($unit) {
3056 if ($unit eq 'K') {
3057 $newsize = $newsize * 1024;
3058 } elsif ($unit eq 'M') {
3059 $newsize = $newsize * 1024 * 1024;
3060 } elsif ($unit eq 'G') {
3061 $newsize = $newsize * 1024 * 1024 * 1024;
3062 } elsif ($unit eq 'T') {
3063 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3064 }
3065 }
3066 $newsize += $size if $ext;
3067 $newsize = int($newsize);
3068
3069 die "shrinking disks is not supported\n" if $newsize < $size;
3070
3071 return if $size == $newsize;
3072
3073 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3074
3075 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3076
3077 $drive->{size} = $newsize;
3078 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3079
3080 PVE::QemuConfig->write_config($vmid, $conf);
3081 };
3082
3083 PVE::QemuConfig->lock_config($vmid, $updatefn);
3084 return undef;
3085 }});
3086
3087 __PACKAGE__->register_method({
3088 name => 'snapshot_list',
3089 path => '{vmid}/snapshot',
3090 method => 'GET',
3091 description => "List all snapshots.",
3092 permissions => {
3093 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3094 },
3095 proxyto => 'node',
3096 protected => 1, # qemu pid files are only readable by root
3097 parameters => {
3098 additionalProperties => 0,
3099 properties => {
3100 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3101 node => get_standard_option('pve-node'),
3102 },
3103 },
3104 returns => {
3105 type => 'array',
3106 items => {
3107 type => "object",
3108 properties => {},
3109 },
3110 links => [ { rel => 'child', href => "{name}" } ],
3111 },
3112 code => sub {
3113 my ($param) = @_;
3114
3115 my $vmid = $param->{vmid};
3116
3117 my $conf = PVE::QemuConfig->load_config($vmid);
3118 my $snaphash = $conf->{snapshots} || {};
3119
3120 my $res = [];
3121
3122 foreach my $name (keys %$snaphash) {
3123 my $d = $snaphash->{$name};
3124 my $item = {
3125 name => $name,
3126 snaptime => $d->{snaptime} || 0,
3127 vmstate => $d->{vmstate} ? 1 : 0,
3128 description => $d->{description} || '',
3129 };
3130 $item->{parent} = $d->{parent} if $d->{parent};
3131 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
3132 push @$res, $item;
3133 }
3134
3135 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
3136 my $current = { name => 'current', digest => $conf->{digest}, running => $running };
3137 $current->{parent} = $conf->{parent} if $conf->{parent};
3138
3139 push @$res, $current;
3140
3141 return $res;
3142 }});
3143
3144 __PACKAGE__->register_method({
3145 name => 'snapshot',
3146 path => '{vmid}/snapshot',
3147 method => 'POST',
3148 protected => 1,
3149 proxyto => 'node',
3150 description => "Snapshot a VM.",
3151 permissions => {
3152 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3153 },
3154 parameters => {
3155 additionalProperties => 0,
3156 properties => {
3157 node => get_standard_option('pve-node'),
3158 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3159 snapname => get_standard_option('pve-snapshot-name'),
3160 vmstate => {
3161 optional => 1,
3162 type => 'boolean',
3163 description => "Save the vmstate",
3164 },
3165 description => {
3166 optional => 1,
3167 type => 'string',
3168 description => "A textual description or comment.",
3169 },
3170 },
3171 },
3172 returns => {
3173 type => 'string',
3174 description => "the task ID.",
3175 },
3176 code => sub {
3177 my ($param) = @_;
3178
3179 my $rpcenv = PVE::RPCEnvironment::get();
3180
3181 my $authuser = $rpcenv->get_user();
3182
3183 my $node = extract_param($param, 'node');
3184
3185 my $vmid = extract_param($param, 'vmid');
3186
3187 my $snapname = extract_param($param, 'snapname');
3188
3189 die "unable to use snapshot name 'current' (reserved name)\n"
3190 if $snapname eq 'current';
3191
3192 my $realcmd = sub {
3193 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
3194 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
3195 $param->{description});
3196 };
3197
3198 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3199 }});
3200
3201 __PACKAGE__->register_method({
3202 name => 'snapshot_cmd_idx',
3203 path => '{vmid}/snapshot/{snapname}',
3204 description => '',
3205 method => 'GET',
3206 permissions => {
3207 user => 'all',
3208 },
3209 parameters => {
3210 additionalProperties => 0,
3211 properties => {
3212 vmid => get_standard_option('pve-vmid'),
3213 node => get_standard_option('pve-node'),
3214 snapname => get_standard_option('pve-snapshot-name'),
3215 },
3216 },
3217 returns => {
3218 type => 'array',
3219 items => {
3220 type => "object",
3221 properties => {},
3222 },
3223 links => [ { rel => 'child', href => "{cmd}" } ],
3224 },
3225 code => sub {
3226 my ($param) = @_;
3227
3228 my $res = [];
3229
3230 push @$res, { cmd => 'rollback' };
3231 push @$res, { cmd => 'config' };
3232
3233 return $res;
3234 }});
3235
3236 __PACKAGE__->register_method({
3237 name => 'update_snapshot_config',
3238 path => '{vmid}/snapshot/{snapname}/config',
3239 method => 'PUT',
3240 protected => 1,
3241 proxyto => 'node',
3242 description => "Update snapshot metadata.",
3243 permissions => {
3244 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3245 },
3246 parameters => {
3247 additionalProperties => 0,
3248 properties => {
3249 node => get_standard_option('pve-node'),
3250 vmid => get_standard_option('pve-vmid'),
3251 snapname => get_standard_option('pve-snapshot-name'),
3252 description => {
3253 optional => 1,
3254 type => 'string',
3255 description => "A textual description or comment.",
3256 },
3257 },
3258 },
3259 returns => { type => 'null' },
3260 code => sub {
3261 my ($param) = @_;
3262
3263 my $rpcenv = PVE::RPCEnvironment::get();
3264
3265 my $authuser = $rpcenv->get_user();
3266
3267 my $vmid = extract_param($param, 'vmid');
3268
3269 my $snapname = extract_param($param, 'snapname');
3270
3271 return undef if !defined($param->{description});
3272
3273 my $updatefn = sub {
3274
3275 my $conf = PVE::QemuConfig->load_config($vmid);
3276
3277 PVE::QemuConfig->check_lock($conf);
3278
3279 my $snap = $conf->{snapshots}->{$snapname};
3280
3281 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3282
3283 $snap->{description} = $param->{description} if defined($param->{description});
3284
3285 PVE::QemuConfig->write_config($vmid, $conf);
3286 };
3287
3288 PVE::QemuConfig->lock_config($vmid, $updatefn);
3289
3290 return undef;
3291 }});
3292
3293 __PACKAGE__->register_method({
3294 name => 'get_snapshot_config',
3295 path => '{vmid}/snapshot/{snapname}/config',
3296 method => 'GET',
3297 proxyto => 'node',
3298 description => "Get snapshot configuration",
3299 permissions => {
3300 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3301 },
3302 parameters => {
3303 additionalProperties => 0,
3304 properties => {
3305 node => get_standard_option('pve-node'),
3306 vmid => get_standard_option('pve-vmid'),
3307 snapname => get_standard_option('pve-snapshot-name'),
3308 },
3309 },
3310 returns => { type => "object" },
3311 code => sub {
3312 my ($param) = @_;
3313
3314 my $rpcenv = PVE::RPCEnvironment::get();
3315
3316 my $authuser = $rpcenv->get_user();
3317
3318 my $vmid = extract_param($param, 'vmid');
3319
3320 my $snapname = extract_param($param, 'snapname');
3321
3322 my $conf = PVE::QemuConfig->load_config($vmid);
3323
3324 my $snap = $conf->{snapshots}->{$snapname};
3325
3326 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3327
3328 return $snap;
3329 }});
3330
3331 __PACKAGE__->register_method({
3332 name => 'rollback',
3333 path => '{vmid}/snapshot/{snapname}/rollback',
3334 method => 'POST',
3335 protected => 1,
3336 proxyto => 'node',
3337 description => "Rollback VM state to specified snapshot.",
3338 permissions => {
3339 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3340 },
3341 parameters => {
3342 additionalProperties => 0,
3343 properties => {
3344 node => get_standard_option('pve-node'),
3345 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3346 snapname => get_standard_option('pve-snapshot-name'),
3347 },
3348 },
3349 returns => {
3350 type => 'string',
3351 description => "the task ID.",
3352 },
3353 code => sub {
3354 my ($param) = @_;
3355
3356 my $rpcenv = PVE::RPCEnvironment::get();
3357
3358 my $authuser = $rpcenv->get_user();
3359
3360 my $node = extract_param($param, 'node');
3361
3362 my $vmid = extract_param($param, 'vmid');
3363
3364 my $snapname = extract_param($param, 'snapname');
3365
3366 my $realcmd = sub {
3367 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3368 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
3369 };
3370
3371 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd);
3372 }});
3373
3374 __PACKAGE__->register_method({
3375 name => 'delsnapshot',
3376 path => '{vmid}/snapshot/{snapname}',
3377 method => 'DELETE',
3378 protected => 1,
3379 proxyto => 'node',
3380 description => "Delete a VM snapshot.",
3381 permissions => {
3382 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3383 },
3384 parameters => {
3385 additionalProperties => 0,
3386 properties => {
3387 node => get_standard_option('pve-node'),
3388 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3389 snapname => get_standard_option('pve-snapshot-name'),
3390 force => {
3391 optional => 1,
3392 type => 'boolean',
3393 description => "For removal from config file, even if removing disk snapshots fails.",
3394 },
3395 },
3396 },
3397 returns => {
3398 type => 'string',
3399 description => "the task ID.",
3400 },
3401 code => sub {
3402 my ($param) = @_;
3403
3404 my $rpcenv = PVE::RPCEnvironment::get();
3405
3406 my $authuser = $rpcenv->get_user();
3407
3408 my $node = extract_param($param, 'node');
3409
3410 my $vmid = extract_param($param, 'vmid');
3411
3412 my $snapname = extract_param($param, 'snapname');
3413
3414 my $realcmd = sub {
3415 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3416 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
3417 };
3418
3419 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3420 }});
3421
3422 __PACKAGE__->register_method({
3423 name => 'template',
3424 path => '{vmid}/template',
3425 method => 'POST',
3426 protected => 1,
3427 proxyto => 'node',
3428 description => "Create a Template.",
3429 permissions => {
3430 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3431 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3432 },
3433 parameters => {
3434 additionalProperties => 0,
3435 properties => {
3436 node => get_standard_option('pve-node'),
3437 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3438 disk => {
3439 optional => 1,
3440 type => 'string',
3441 description => "If you want to convert only 1 disk to base image.",
3442 enum => [PVE::QemuServer::valid_drive_names()],
3443 },
3444
3445 },
3446 },
3447 returns => { type => 'null'},
3448 code => sub {
3449 my ($param) = @_;
3450
3451 my $rpcenv = PVE::RPCEnvironment::get();
3452
3453 my $authuser = $rpcenv->get_user();
3454
3455 my $node = extract_param($param, 'node');
3456
3457 my $vmid = extract_param($param, 'vmid');
3458
3459 my $disk = extract_param($param, 'disk');
3460
3461 my $updatefn = sub {
3462
3463 my $conf = PVE::QemuConfig->load_config($vmid);
3464
3465 PVE::QemuConfig->check_lock($conf);
3466
3467 die "unable to create template, because VM contains snapshots\n"
3468 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3469
3470 die "you can't convert a template to a template\n"
3471 if PVE::QemuConfig->is_template($conf) && !$disk;
3472
3473 die "you can't convert a VM to template if VM is running\n"
3474 if PVE::QemuServer::check_running($vmid);
3475
3476 my $realcmd = sub {
3477 PVE::QemuServer::template_create($vmid, $conf, $disk);
3478 };
3479
3480 $conf->{template} = 1;
3481 PVE::QemuConfig->write_config($vmid, $conf);
3482
3483 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3484 };
3485
3486 PVE::QemuConfig->lock_config($vmid, $updatefn);
3487 return undef;
3488 }});
3489
3490 1;