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