]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
clone disk: move check against cloning TPM state of running VM to beginning
[qemu-server.git] / PVE / API2 / Qemu.pm
CommitLineData
1e3baf05
DM
1package PVE::API2::Qemu;
2
3use strict;
4use warnings;
5b9d692a 5use Cwd 'abs_path';
451b2b81 6use Net::SSLeay;
655d7462
WB
7use POSIX;
8use IO::Socket::IP;
0c9a7596 9use URI::Escape;
3c5bdde8 10use Crypt::OpenSSL::Random;
1e3baf05 11
502d18a2 12use PVE::Cluster qw (cfs_read_file cfs_write_file);;
95896f80 13use PVE::RRD;
1e3baf05
DM
14use PVE::SafeSyslog;
15use PVE::Tools qw(extract_param);
f9bfceef 16use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
1e3baf05
DM
17use PVE::Storage;
18use PVE::JSONSchema qw(get_standard_option);
19use PVE::RESTHandler;
628bb7f2 20use PVE::ReplicationConfig;
95f42d61 21use PVE::GuestHelpers;
ffda963f 22use PVE::QemuConfig;
1e3baf05 23use PVE::QemuServer;
e0fd2b2f 24use PVE::QemuServer::Drive;
6e72f90b 25use PVE::QemuServer::CPUConfig;
0a13e08e 26use PVE::QemuServer::Monitor qw(mon_cmd);
90b20b15 27use PVE::QemuServer::Machine;
3ea94c60 28use PVE::QemuMigrate;
1e3baf05
DM
29use PVE::RPCEnvironment;
30use PVE::AccessControl;
31use PVE::INotify;
de8f60b2 32use PVE::Network;
e9abcde6 33use PVE::Firewall;
228a998b 34use PVE::API2::Firewall::VM;
b8158701 35use PVE::API2::Qemu::Agent;
d9123ef5 36use PVE::VZDump::Plugin;
48cf040f 37use PVE::DataCenterConfig;
f42ea29b 38use PVE::SSHInfo;
a9453218 39use PVE::Replication;
9f11fc5f
WB
40
41BEGIN {
42 if (!$ENV{PVE_GENERATING_DOCS}) {
43 require PVE::HA::Env::PVE2;
44 import PVE::HA::Env::PVE2;
45 require PVE::HA::Config;
46 import PVE::HA::Config;
47 }
48}
1e3baf05
DM
49
50use Data::Dumper; # fixme: remove
51
52use base qw(PVE::RESTHandler);
53
54my $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.";
55
56my $resolve_cdrom_alias = sub {
57 my $param = shift;
58
59 if (my $value = $param->{cdrom}) {
60 $value .= ",media=cdrom" if $value !~ m/media=/;
61 $param->{ide2} = $value;
62 delete $param->{cdrom};
63 }
64};
65
7979bbcd
FE
66my $check_drive_param = sub {
67 my ($param, $storecfg, $extra_checks) = @_;
68
69 for my $opt (sort keys $param->%*) {
70 next if !PVE::QemuServer::is_valid_drivename($opt);
71
72 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
73 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
74
75 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
76
77 $extra_checks->($drive) if $extra_checks;
78
79 $param->{$opt} = PVE::QemuServer::print_drive($drive);
80 }
81};
82
bf1312d8 83my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
ae57f6b3 84my $check_storage_access = sub {
fcbb753e 85 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
a0d1b1a2 86
912792e2 87 PVE::QemuConfig->foreach_volume($settings, sub {
ae57f6b3 88 my ($ds, $drive) = @_;
a0d1b1a2 89
ae57f6b3 90 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
a0d1b1a2 91
ae57f6b3 92 my $volid = $drive->{file};
21e1ee7b 93 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
a0d1b1a2 94
8f2c9019 95 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit' || (defined($volname) && $volname eq 'cloudinit'))) {
09d0ee64 96 # nothing to check
f5782fd0
DM
97 } elsif ($isCDROM && ($volid eq 'cdrom')) {
98 $rpcenv->check($authuser, "/", ['Sys.Console']);
bf1312d8 99 } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
a0d1b1a2
DM
100 my ($storeid, $size) = ($2 || $default_storage, $3);
101 die "no storage ID specified (and no default storage)\n" if !$storeid;
fcbb753e 102 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
c46366fd
DC
103 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
104 raise_param_exc({ storage => "storage '$storeid' does not support vm images"})
105 if !$scfg->{content}->{images};
a0d1b1a2 106 } else {
f9be9137
FE
107 PVE::Storage::check_volume_access(
108 $rpcenv,
109 $authuser,
110 $storecfg,
111 $vmid,
112 $volid,
113 'images',
114 );
a0d1b1a2
DM
115 }
116 });
253624c7
FG
117
118 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
119 if defined($settings->{vmstatestorage});
ae57f6b3 120};
a0d1b1a2 121
9418baad 122my $check_storage_access_clone = sub {
81f043eb 123 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
6116f729 124
55173c6b
DM
125 my $sharedvm = 1;
126
912792e2 127 PVE::QemuConfig->foreach_volume($conf, sub {
6116f729
DM
128 my ($ds, $drive) = @_;
129
130 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
131
132 my $volid = $drive->{file};
133
134 return if !$volid || $volid eq 'none';
135
136 if ($isCDROM) {
137 if ($volid eq 'cdrom') {
138 $rpcenv->check($authuser, "/", ['Sys.Console']);
139 } else {
75466c4f 140 # we simply allow access
55173c6b
DM
141 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
142 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
143 $sharedvm = 0 if !$scfg->{shared};
144
6116f729
DM
145 }
146 } else {
55173c6b
DM
147 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
148 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
149 $sharedvm = 0 if !$scfg->{shared};
150
81f043eb 151 $sid = $storage if $storage;
6116f729
DM
152 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
153 }
154 });
55173c6b 155
253624c7
FG
156 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
157 if defined($conf->{vmstatestorage});
158
55173c6b 159 return $sharedvm;
6116f729
DM
160};
161
9fb295d0
FG
162my $check_storage_access_migrate = sub {
163 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
164
165 PVE::Storage::storage_check_enabled($storecfg, $storage, $node);
166
167 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
168
169 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
170 die "storage '$storage' does not support vm images\n"
171 if !$scfg->{content}->{images};
172};
173
ae57f6b3
DM
174# Note: $pool is only needed when creating a VM, because pool permissions
175# are automatically inherited if VM already exists inside a pool.
176my $create_disks = sub {
96ed3574 177 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
a0d1b1a2
DM
178
179 my $vollist = [];
a0d1b1a2 180
ae57f6b3 181 my $res = {};
64932aeb
DM
182
183 my $code = sub {
ae57f6b3
DM
184 my ($ds, $disk) = @_;
185
186 my $volid = $disk->{file};
21e1ee7b 187 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
ae57f6b3 188
f5782fd0 189 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
628e9a2b 190 delete $disk->{size};
71c58bb7 191 $res->{$ds} = PVE::QemuServer::print_drive($disk);
8f2c9019 192 } elsif (defined($volname) && $volname eq 'cloudinit') {
21e1ee7b 193 $storeid = $storeid // $default_storage;
0c9a7596
AD
194 die "no storage ID specified (and no default storage)\n" if !$storeid;
195 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
196 my $name = "vm-$vmid-cloudinit";
64d1a6ae 197
0c9a7596
AD
198 my $fmt = undef;
199 if ($scfg->{path}) {
c152600b 200 $fmt = $disk->{format} // "qcow2";
64d1a6ae
WL
201 $name .= ".$fmt";
202 } else {
c152600b 203 $fmt = $disk->{format} // "raw";
0c9a7596 204 }
64d1a6ae 205
4fdc1d3d 206 # Initial disk created with 4 MB and aligned to 4MB on regeneration
7d761a01
ML
207 my $ci_size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
208 my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
0c9a7596
AD
209 $disk->{file} = $volid;
210 $disk->{media} = 'cdrom';
211 push @$vollist, $volid;
212 delete $disk->{format}; # no longer needed
71c58bb7 213 $res->{$ds} = PVE::QemuServer::print_drive($disk);
17677004 214 } elsif ($volid =~ $NEW_DISK_RE) {
ae57f6b3
DM
215 my ($storeid, $size) = ($2 || $default_storage, $3);
216 die "no storage ID specified (and no default storage)\n" if !$storeid;
217 my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
218 my $fmt = $disk->{format} || $defformat;
1a35631a 219
a1d8c038
TL
220 $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
221
1a35631a
DC
222 my $volid;
223 if ($ds eq 'efidisk0') {
90b20b15 224 my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
b5099b4f 225 ($volid, $size) = PVE::QemuServer::create_efidisk(
90b20b15 226 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
f9dde219
SR
227 } elsif ($ds eq 'tpmstate0') {
228 # swtpm can only use raw volumes, and uses a fixed size
229 $size = PVE::Tools::convert_size(PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE, 'b' => 'kb');
2b9ee944 230 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, "raw", undef, $size);
1a35631a 231 } else {
a1d8c038 232 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
1a35631a 233 }
a0d1b1a2 234 push @$vollist, $volid;
a1d8c038
TL
235 $disk->{file} = $volid;
236 $disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
ae57f6b3 237 delete $disk->{format}; # no longer needed
71c58bb7 238 $res->{$ds} = PVE::QemuServer::print_drive($disk);
ae57f6b3 239 } else {
f9be9137
FE
240 PVE::Storage::check_volume_access(
241 $rpcenv,
242 $authuser,
243 $storecfg,
244 $vmid,
245 $volid,
246 'images',
247 );
75466c4f 248
fe19840a 249 PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid;
09a89895 250
fe19840a
FE
251 my $size = PVE::Storage::volume_size_info($storecfg, $volid);
252 die "volume $volid does not exist\n" if !$size;
253 $disk->{size} = $size;
24afaca0 254
71c58bb7 255 $res->{$ds} = PVE::QemuServer::print_drive($disk);
a0d1b1a2 256 }
64932aeb
DM
257 };
258
912792e2 259 eval { PVE::QemuConfig->foreach_volume($settings, $code); };
a0d1b1a2
DM
260
261 # free allocated images on error
262 if (my $err = $@) {
263 syslog('err', "VM $vmid creating disks failed");
264 foreach my $volid (@$vollist) {
265 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
266 warn $@ if $@;
267 }
268 die $err;
269 }
270
367e6bf4 271 return ($vollist, $res);
a0d1b1a2
DM
272};
273
6e72f90b
SR
274my $check_cpu_model_access = sub {
275 my ($rpcenv, $authuser, $new, $existing) = @_;
276
277 return if !defined($new->{cpu});
278
279 my $cpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $new->{cpu});
280 return if !$cpu || !$cpu->{cputype}; # always allow default
281 my $cputype = $cpu->{cputype};
282
283 if ($existing && $existing->{cpu}) {
284 # changing only other settings doesn't require permissions for CPU model
285 my $existingCpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $existing->{cpu});
286 return if $existingCpu->{cputype} eq $cputype;
287 }
288
289 if (PVE::QemuServer::CPUConfig::is_custom_model($cputype)) {
290 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
291 }
292};
293
58cb690b
DC
294my $cpuoptions = {
295 'cores' => 1,
296 'cpu' => 1,
297 'cpulimit' => 1,
298 'cpuunits' => 1,
299 'numa' => 1,
300 'smp' => 1,
301 'sockets' => 1,
84b31f48 302 'vcpus' => 1,
58cb690b
DC
303};
304
305my $memoryoptions = {
306 'memory' => 1,
307 'balloon' => 1,
308 'shares' => 1,
309};
310
311my $hwtypeoptions = {
312 'acpi' => 1,
313 'hotplug' => 1,
314 'kvm' => 1,
315 'machine' => 1,
316 'scsihw' => 1,
317 'smbios1' => 1,
318 'tablet' => 1,
319 'vga' => 1,
320 'watchdog' => 1,
b2dd61a0 321 'audio0' => 1,
58cb690b
DC
322};
323
6bcacc21 324my $generaloptions = {
58cb690b
DC
325 'agent' => 1,
326 'autostart' => 1,
327 'bios' => 1,
328 'description' => 1,
329 'keyboard' => 1,
330 'localtime' => 1,
331 'migrate_downtime' => 1,
332 'migrate_speed' => 1,
333 'name' => 1,
334 'onboot' => 1,
335 'ostype' => 1,
336 'protection' => 1,
337 'reboot' => 1,
338 'startdate' => 1,
339 'startup' => 1,
340 'tdf' => 1,
341 'template' => 1,
b8e7068a 342 'tags' => 1,
58cb690b
DC
343};
344
345my $vmpoweroptions = {
346 'freeze' => 1,
347};
348
349my $diskoptions = {
350 'boot' => 1,
351 'bootdisk' => 1,
253624c7 352 'vmstatestorage' => 1,
58cb690b
DC
353};
354
7ee990cd 355my $cloudinitoptions = {
cb702ebe 356 cicustom => 1,
7ee990cd
DM
357 cipassword => 1,
358 citype => 1,
359 ciuser => 1,
360 nameserver => 1,
361 searchdomain => 1,
362 sshkeys => 1,
363};
364
0761ee01
FE
365my $check_vm_create_serial_perm = sub {
366 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
367
368 return 1 if $authuser eq 'root@pam';
369
370 foreach my $opt (keys %{$param}) {
371 next if $opt !~ m/^serial\d+$/;
372
373 if ($param->{$opt} eq 'socket') {
374 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
375 } else {
376 die "only root can set '$opt' config for real devices\n";
377 }
378 }
379
380 return 1;
381};
382
383my $check_vm_create_usb_perm = sub {
384 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
385
386 return 1 if $authuser eq 'root@pam';
387
388 foreach my $opt (keys %{$param}) {
389 next if $opt !~ m/^usb\d+$/;
390
391 if ($param->{$opt} =~ m/spice/) {
392 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
393 } else {
394 die "only root can set '$opt' config for real devices\n";
395 }
396 }
397
398 return 1;
399};
400
a0d1b1a2 401my $check_vm_modify_config_perm = sub {
e30f75c5 402 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
a0d1b1a2 403
6e5c4da7 404 return 1 if $authuser eq 'root@pam';
a0d1b1a2 405
ae57f6b3 406 foreach my $opt (@$key_list) {
97415261
TL
407 # some checks (e.g., disk, serial port, usb) need to be done somewhere
408 # else, as there the permission can be value dependend
74479ee9 409 next if PVE::QemuServer::is_valid_drivename($opt);
58cb690b 410 next if $opt eq 'cdrom';
165be267
DC
411 next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
412
a0d1b1a2 413
6bcacc21 414 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
a0d1b1a2 415 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
58cb690b 416 } elsif ($memoryoptions->{$opt}) {
a0d1b1a2 417 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
58cb690b 418 } elsif ($hwtypeoptions->{$opt}) {
a0d1b1a2 419 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
6bcacc21 420 } elsif ($generaloptions->{$opt}) {
58cb690b
DC
421 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
422 # special case for startup since it changes host behaviour
423 if ($opt eq 'startup') {
424 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
425 }
426 } elsif ($vmpoweroptions->{$opt}) {
427 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
428 } elsif ($diskoptions->{$opt}) {
429 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
fc701af7 430 } elsif ($opt =~ m/^(?:net|ipconfig)\d+$/) {
a0d1b1a2 431 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
fc701af7
ML
432 } elsif ($cloudinitoptions->{$opt}) {
433 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
5661a681
DC
434 } elsif ($opt eq 'vmstate') {
435 # the user needs Disk and PowerMgmt privileges to change the vmstate
436 # also needs privileges on the storage, that will be checked later
437 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
a0d1b1a2 438 } else {
165be267 439 # catches hostpci\d+, args, lock, etc.
58cb690b
DC
440 # new options will be checked here
441 die "only root can set '$opt' config\n";
a0d1b1a2
DM
442 }
443 }
444
445 return 1;
446};
447
1e3baf05 448__PACKAGE__->register_method({
afdb31d5
DM
449 name => 'vmlist',
450 path => '',
1e3baf05
DM
451 method => 'GET',
452 description => "Virtual machine index (per node).",
a0d1b1a2
DM
453 permissions => {
454 description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
455 user => 'all',
456 },
1e3baf05
DM
457 proxyto => 'node',
458 protected => 1, # qemu pid files are only readable by root
459 parameters => {
3326ae19 460 additionalProperties => 0,
1e3baf05
DM
461 properties => {
462 node => get_standard_option('pve-node'),
12612b09
WB
463 full => {
464 type => 'boolean',
465 optional => 1,
466 description => "Determine the full status of active VMs.",
467 },
1e3baf05
DM
468 },
469 },
470 returns => {
471 type => 'array',
472 items => {
473 type => "object",
b1a70cab 474 properties => $PVE::QemuServer::vmstatus_return_properties,
1e3baf05
DM
475 },
476 links => [ { rel => 'child', href => "{vmid}" } ],
477 },
478 code => sub {
479 my ($param) = @_;
480
a0d1b1a2
DM
481 my $rpcenv = PVE::RPCEnvironment::get();
482 my $authuser = $rpcenv->get_user();
483
12612b09 484 my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full});
1e3baf05 485
a0d1b1a2
DM
486 my $res = [];
487 foreach my $vmid (keys %$vmstatus) {
488 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
489
490 my $data = $vmstatus->{$vmid};
a0d1b1a2
DM
491 push @$res, $data;
492 }
1e3baf05 493
a0d1b1a2 494 return $res;
1e3baf05
DM
495 }});
496
d1e92cf6
DM
497my $parse_restore_archive = sub {
498 my ($storecfg, $archive) = @_;
499
e5fd1c65 500 my ($archive_storeid, $archive_volname) = PVE::Storage::parse_volume_id($archive, 1);
d1e92cf6
DM
501
502 if (defined($archive_storeid)) {
503 my $scfg = PVE::Storage::storage_config($storecfg, $archive_storeid);
504 if ($scfg->{type} eq 'pbs') {
505 return {
506 type => 'pbs',
507 volid => $archive,
508 };
509 }
510 }
511 my $path = PVE::Storage::abs_filesystem_path($storecfg, $archive);
512 return {
513 type => 'file',
514 path => $path,
515 };
516};
d703d4c0 517
d703d4c0 518
1e3baf05 519__PACKAGE__->register_method({
afdb31d5
DM
520 name => 'create_vm',
521 path => '',
1e3baf05 522 method => 'POST',
3e16d5fc 523 description => "Create or restore a virtual machine.",
a0d1b1a2 524 permissions => {
f9bfceef
DM
525 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
526 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
527 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
528 user => 'all', # check inside
a0d1b1a2 529 },
1e3baf05
DM
530 protected => 1,
531 proxyto => 'node',
532 parameters => {
3326ae19 533 additionalProperties => 0,
1e3baf05
DM
534 properties => PVE::QemuServer::json_config_properties(
535 {
536 node => get_standard_option('pve-node'),
65e866e5 537 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
3e16d5fc 538 archive => {
d1e92cf6 539 description => "The backup archive. Either the file system path to a .tar or .vma file (use '-' to pipe data from stdin) or a proxmox storage backup volume identifier.",
3e16d5fc
DM
540 type => 'string',
541 optional => 1,
542 maxLength => 255,
65e866e5 543 completion => \&PVE::QemuServer::complete_backup_archives,
3e16d5fc
DM
544 },
545 storage => get_standard_option('pve-storage-id', {
546 description => "Default storage.",
547 optional => 1,
335af808 548 completion => \&PVE::QemuServer::complete_storage,
3e16d5fc
DM
549 }),
550 force => {
afdb31d5 551 optional => 1,
3e16d5fc
DM
552 type => 'boolean',
553 description => "Allow to overwrite existing VM.",
51586c3a
DM
554 requires => 'archive',
555 },
556 unique => {
afdb31d5 557 optional => 1,
51586c3a
DM
558 type => 'boolean',
559 description => "Assign a unique random ethernet address.",
560 requires => 'archive',
3e16d5fc 561 },
26731a3c
SR
562 'live-restore' => {
563 optional => 1,
564 type => 'boolean',
565 description => "Start the VM immediately from the backup and restore in background. PBS only.",
566 requires => 'archive',
567 },
75466c4f 568 pool => {
a0d1b1a2
DM
569 optional => 1,
570 type => 'string', format => 'pve-poolid',
571 description => "Add the VM to the specified pool.",
572 },
7c536e11 573 bwlimit => {
0aab5a16 574 description => "Override I/O bandwidth limit (in KiB/s).",
7c536e11
WB
575 optional => 1,
576 type => 'integer',
577 minimum => '0',
41756a3b 578 default => 'restore limit from datacenter or storage config',
e33f774d
TL
579 },
580 start => {
581 optional => 1,
582 type => 'boolean',
583 default => 0,
584 description => "Start VM after it was created successfully.",
585 },
1e3baf05
DM
586 }),
587 },
afdb31d5 588 returns => {
5fdbe4f0
DM
589 type => 'string',
590 },
1e3baf05
DM
591 code => sub {
592 my ($param) = @_;
593
5fdbe4f0 594 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 595 my $authuser = $rpcenv->get_user();
5fdbe4f0 596
1e3baf05 597 my $node = extract_param($param, 'node');
1e3baf05
DM
598 my $vmid = extract_param($param, 'vmid');
599
3e16d5fc 600 my $archive = extract_param($param, 'archive');
8ba8418c 601 my $is_restore = !!$archive;
3e16d5fc 602
b924c435 603 my $bwlimit = extract_param($param, 'bwlimit');
51586c3a 604 my $force = extract_param($param, 'force');
a0d1b1a2 605 my $pool = extract_param($param, 'pool');
e33f774d 606 my $start_after_create = extract_param($param, 'start');
b924c435
TL
607 my $storage = extract_param($param, 'storage');
608 my $unique = extract_param($param, 'unique');
26731a3c
SR
609 my $live_restore = extract_param($param, 'live-restore');
610
0c9a7596
AD
611 if (defined(my $ssh_keys = $param->{sshkeys})) {
612 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
613 PVE::Tools::validate_ssh_public_keys($ssh_keys);
614 }
615
3e16d5fc 616 PVE::Cluster::check_cfs_quorum();
1e3baf05 617
b924c435
TL
618 my $filename = PVE::QemuConfig->config_file($vmid);
619 my $storecfg = PVE::Storage::config();
620
a0d1b1a2
DM
621 if (defined($pool)) {
622 $rpcenv->check_pool_exist($pool);
75466c4f 623 }
a0d1b1a2 624
fcbb753e 625 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
a0d1b1a2
DM
626 if defined($storage);
627
f9bfceef
DM
628 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
629 # OK
630 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
631 # OK
632 } elsif ($archive && $force && (-f $filename) &&
633 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
634 # OK: user has VM.Backup permissions, and want to restore an existing VM
635 } else {
636 raise_perm_exc();
637 }
638
afdb31d5 639 if (!$archive) {
3e16d5fc 640 &$resolve_cdrom_alias($param);
1e3baf05 641
fcbb753e 642 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
ae57f6b3 643
e30f75c5 644 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
ae57f6b3 645
0761ee01
FE
646 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
647 &$check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
648
6e72f90b
SR
649 &$check_cpu_model_access($rpcenv, $authuser, $param);
650
7979bbcd 651 $check_drive_param->($param, $storecfg);
3e16d5fc
DM
652
653 PVE::QemuServer::add_random_macs($param);
51586c3a
DM
654 } else {
655 my $keystr = join(' ', keys %$param);
bc4dcb99
DM
656 raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr;
657
5b9d692a 658 if ($archive eq '-') {
3326ae19 659 die "pipe requires cli environment\n" if $rpcenv->{type} ne 'cli';
d1e92cf6 660 $archive = { type => 'pipe' };
5b9d692a 661 } else {
f9be9137
FE
662 PVE::Storage::check_volume_access(
663 $rpcenv,
664 $authuser,
665 $storecfg,
666 $vmid,
667 $archive,
668 'backup',
669 );
d1e92cf6
DM
670
671 $archive = $parse_restore_archive->($storecfg, $archive);
971f27c4 672 }
1e3baf05
DM
673 }
674
4fedc13b 675 my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -";
3e16d5fc 676
4fedc13b
TL
677 eval { PVE::QemuConfig->create_and_lock_config($vmid, $force) };
678 die "$emsg $@" if $@;
3e16d5fc 679
b973806e 680 my $restored_data = 0;
4fedc13b
TL
681 my $restorefn = sub {
682 my $conf = PVE::QemuConfig->load_config($vmid);
4d8d55f1 683
4fedc13b 684 PVE::QemuConfig->check_protection($conf, $emsg);
3a07a8a9 685
4fedc13b 686 die "$emsg vm is running\n" if PVE::QemuServer::check_running($vmid);
3e16d5fc
DM
687
688 my $realcmd = sub {
d1e92cf6 689 my $restore_options = {
51586c3a 690 storage => $storage,
a0d1b1a2 691 pool => $pool,
7c536e11 692 unique => $unique,
5294c110 693 bwlimit => $bwlimit,
26731a3c 694 live => $live_restore,
d1e92cf6
DM
695 };
696 if ($archive->{type} eq 'file' || $archive->{type} eq 'pipe') {
b973806e
TL
697 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
698 if $live_restore;
d1e92cf6
DM
699 PVE::QemuServer::restore_file_archive($archive->{path} // '-', $vmid, $authuser, $restore_options);
700 } elsif ($archive->{type} eq 'pbs') {
701 PVE::QemuServer::restore_proxmox_backup_archive($archive->{volid}, $vmid, $authuser, $restore_options);
702 } else {
703 die "unknown backup archive type\n";
704 }
b973806e
TL
705 $restored_data = 1;
706
5294c110
CE
707 my $restored_conf = PVE::QemuConfig->load_config($vmid);
708 # Convert restored VM to template if backup was VM template
709 if (PVE::QemuConfig->is_template($restored_conf)) {
710 warn "Convert to template.\n";
711 eval { PVE::QemuServer::template_create($vmid, $restored_conf) };
712 warn $@ if $@;
713 }
3e16d5fc
DM
714 };
715
223e032b
WL
716 # ensure no old replication state are exists
717 PVE::ReplicationState::delete_guest_states($vmid);
718
0c97024d
TL
719 PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
720
a0e27afb 721 if ($start_after_create && !$live_restore) {
0c97024d
TL
722 print "Execute autostart\n";
723 eval { PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node }) };
724 warn $@ if $@;
725 }
3e16d5fc 726 };
1e3baf05 727
1e3baf05 728 my $createfn = sub {
223e032b
WL
729 # ensure no old replication state are exists
730 PVE::ReplicationState::delete_guest_states($vmid);
731
5fdbe4f0 732 my $realcmd = sub {
1858638f 733 my $conf = $param;
de64f101 734 my $arch = PVE::QemuServer::get_vm_arch($conf);
40c3bcf8 735
26b443c8
TL
736 $conf->{meta} = PVE::QemuServer::new_meta_info_string();
737
a85ff91b 738 my $vollist = [];
5fdbe4f0 739 eval {
367e6bf4
FE
740 ($vollist, my $created_opts) = $create_disks->(
741 $rpcenv,
742 $authuser,
743 $conf,
744 $arch,
745 $storecfg,
746 $vmid,
747 $pool,
748 $param,
749 $storage,
750 );
751 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1e3baf05 752
2141a802
SR
753 if (!$conf->{boot}) {
754 my $devs = PVE::QemuServer::get_default_bootdevices($conf);
755 $conf->{boot} = PVE::QemuServer::print_bootorder($devs);
5fdbe4f0 756 }
1e3baf05 757
47314bf5
DM
758 # auto generate uuid if user did not specify smbios1 option
759 if (!$conf->{smbios1}) {
ae2fcb3b 760 $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
47314bf5
DM
761 }
762
40c3bcf8 763 if ((!defined($conf->{vmgenid}) || $conf->{vmgenid} eq '1') && $arch ne 'aarch64') {
6ee499ff
DC
764 $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
765 }
766
4dd1e83c
TL
767 my $machine = $conf->{machine};
768 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
769 # always pin Windows' machine version on create, they get to easily confused
770 if (PVE::QemuServer::windows_version($conf->{ostype})) {
0761e619 771 $conf->{machine} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
4dd1e83c
TL
772 }
773 }
774
ffda963f 775 PVE::QemuConfig->write_config($vmid, $conf);
ae9ca91d 776
5fdbe4f0
DM
777 };
778 my $err = $@;
1e3baf05 779
5fdbe4f0
DM
780 if ($err) {
781 foreach my $volid (@$vollist) {
782 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
783 warn $@ if $@;
784 }
4fedc13b 785 die "$emsg $err";
5fdbe4f0 786 }
502d18a2 787
be517049 788 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
5fdbe4f0
DM
789 };
790
e33f774d
TL
791 PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
792
793 if ($start_after_create) {
794 print "Execute autostart\n";
5bf96183
WB
795 eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
796 warn $@ if $@;
e33f774d 797 }
5fdbe4f0
DM
798 };
799
5bf96183
WB
800 my ($code, $worker_name);
801 if ($is_restore) {
802 $worker_name = 'qmrestore';
803 $code = sub {
804 eval { $restorefn->() };
805 if (my $err = $@) {
806 eval { PVE::QemuConfig->remove_lock($vmid, 'create') };
807 warn $@ if $@;
b973806e
TL
808 if ($restored_data) {
809 warn "error after data was restored, VM disks should be OK but config may "
810 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
811 } else {
812 warn "error before or during data restore, some or all disks were not "
813 ."completely restored. VM $vmid state is NOT cleaned up.\n";
814 }
5bf96183
WB
815 die $err;
816 }
817 };
818 } else {
819 $worker_name = 'qmcreate';
820 $code = sub {
821 eval { $createfn->() };
822 if (my $err = $@) {
823 eval {
824 my $conffile = PVE::QemuConfig->config_file($vmid);
f1e277cd 825 unlink($conffile) or die "failed to remove config file: $!\n";
5bf96183
WB
826 };
827 warn $@ if $@;
828 die $err;
829 }
830 };
831 }
8ba8418c
TL
832
833 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1e3baf05
DM
834 }});
835
836__PACKAGE__->register_method({
837 name => 'vmdiridx',
afdb31d5 838 path => '{vmid}',
1e3baf05
DM
839 method => 'GET',
840 proxyto => 'node',
841 description => "Directory index",
a0d1b1a2
DM
842 permissions => {
843 user => 'all',
844 },
1e3baf05 845 parameters => {
3326ae19 846 additionalProperties => 0,
1e3baf05
DM
847 properties => {
848 node => get_standard_option('pve-node'),
849 vmid => get_standard_option('pve-vmid'),
850 },
851 },
852 returns => {
853 type => 'array',
854 items => {
855 type => "object",
856 properties => {
857 subdir => { type => 'string' },
858 },
859 },
860 links => [ { rel => 'child', href => "{subdir}" } ],
861 },
862 code => sub {
863 my ($param) = @_;
864
865 my $res = [
866 { subdir => 'config' },
df2a2dbb 867 { subdir => 'pending' },
1e3baf05
DM
868 { subdir => 'status' },
869 { subdir => 'unlink' },
870 { subdir => 'vncproxy' },
87302002 871 { subdir => 'termproxy' },
3ea94c60 872 { subdir => 'migrate' },
2f48a4f5 873 { subdir => 'resize' },
586bfa78 874 { subdir => 'move' },
1e3baf05
DM
875 { subdir => 'rrd' },
876 { subdir => 'rrddata' },
91c94f0a 877 { subdir => 'monitor' },
d1a47427 878 { subdir => 'agent' },
7e7d7b61 879 { subdir => 'snapshot' },
288eeea8 880 { subdir => 'spiceproxy' },
7aa608d6 881 { subdir => 'sendkey' },
228a998b 882 { subdir => 'firewall' },
1e3baf05 883 ];
afdb31d5 884
1e3baf05
DM
885 return $res;
886 }});
887
228a998b 888__PACKAGE__->register_method ({
f34ebd52 889 subclass => "PVE::API2::Firewall::VM",
228a998b
DM
890 path => '{vmid}/firewall',
891});
892
b8158701
DC
893__PACKAGE__->register_method ({
894 subclass => "PVE::API2::Qemu::Agent",
895 path => '{vmid}/agent',
896});
897
1e3baf05 898__PACKAGE__->register_method({
afdb31d5
DM
899 name => 'rrd',
900 path => '{vmid}/rrd',
1e3baf05
DM
901 method => 'GET',
902 protected => 1, # fixme: can we avoid that?
903 permissions => {
378b359e 904 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1e3baf05
DM
905 },
906 description => "Read VM RRD statistics (returns PNG)",
907 parameters => {
3326ae19 908 additionalProperties => 0,
1e3baf05
DM
909 properties => {
910 node => get_standard_option('pve-node'),
911 vmid => get_standard_option('pve-vmid'),
912 timeframe => {
913 description => "Specify the time frame you are interested in.",
914 type => 'string',
915 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
916 },
917 ds => {
918 description => "The list of datasources you want to display.",
919 type => 'string', format => 'pve-configid-list',
920 },
921 cf => {
922 description => "The RRD consolidation function",
923 type => 'string',
924 enum => [ 'AVERAGE', 'MAX' ],
925 optional => 1,
926 },
927 },
928 },
929 returns => {
930 type => "object",
931 properties => {
932 filename => { type => 'string' },
933 },
934 },
935 code => sub {
936 my ($param) = @_;
937
95896f80 938 return PVE::RRD::create_rrd_graph(
afdb31d5 939 "pve2-vm/$param->{vmid}", $param->{timeframe},
1e3baf05 940 $param->{ds}, $param->{cf});
afdb31d5 941
1e3baf05
DM
942 }});
943
944__PACKAGE__->register_method({
afdb31d5
DM
945 name => 'rrddata',
946 path => '{vmid}/rrddata',
1e3baf05
DM
947 method => 'GET',
948 protected => 1, # fixme: can we avoid that?
949 permissions => {
378b359e 950 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1e3baf05
DM
951 },
952 description => "Read VM RRD statistics",
953 parameters => {
3326ae19 954 additionalProperties => 0,
1e3baf05
DM
955 properties => {
956 node => get_standard_option('pve-node'),
957 vmid => get_standard_option('pve-vmid'),
958 timeframe => {
959 description => "Specify the time frame you are interested in.",
960 type => 'string',
961 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
962 },
963 cf => {
964 description => "The RRD consolidation function",
965 type => 'string',
966 enum => [ 'AVERAGE', 'MAX' ],
967 optional => 1,
968 },
969 },
970 },
971 returns => {
972 type => "array",
973 items => {
974 type => "object",
975 properties => {},
976 },
977 },
978 code => sub {
979 my ($param) = @_;
980
95896f80 981 return PVE::RRD::create_rrd_data(
1e3baf05
DM
982 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
983 }});
984
985
986__PACKAGE__->register_method({
afdb31d5
DM
987 name => 'vm_config',
988 path => '{vmid}/config',
1e3baf05
DM
989 method => 'GET',
990 proxyto => 'node',
86ea0ed0
FE
991 description => "Get the virtual machine configuration with pending configuration " .
992 "changes applied. Set the 'current' parameter to get the current configuration instead.",
a0d1b1a2
DM
993 permissions => {
994 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
995 },
1e3baf05 996 parameters => {
3326ae19 997 additionalProperties => 0,
1e3baf05
DM
998 properties => {
999 node => get_standard_option('pve-node'),
335af808 1000 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2a68ec78
TL
1001 current => {
1002 description => "Get current values (instead of pending values).",
1003 optional => 1,
6d89b548
DM
1004 default => 0,
1005 type => 'boolean',
2a68ec78 1006 },
b14477e7
RV
1007 snapshot => get_standard_option('pve-snapshot-name', {
1008 description => "Fetch config values from given snapshot.",
1009 optional => 1,
1010 completion => sub {
1011 my ($cmd, $pname, $cur, $args) = @_;
1012 PVE::QemuConfig->snapshot_list($args->[0]);
1013 },
1014 }),
1e3baf05
DM
1015 },
1016 },
afdb31d5 1017 returns => {
86ea0ed0 1018 description => "The VM configuration.",
1e3baf05 1019 type => "object",
ce9b0a38 1020 properties => PVE::QemuServer::json_config_properties({
554ac7e7
DM
1021 digest => {
1022 type => 'string',
1023 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1024 }
ce9b0a38 1025 }),
1e3baf05
DM
1026 },
1027 code => sub {
1028 my ($param) = @_;
1029
d3179e1c
OB
1030 raise_param_exc({ snapshot => "cannot use 'snapshot' parameter with 'current'",
1031 current => "cannot use 'snapshot' parameter with 'current'"})
1032 if ($param->{snapshot} && $param->{current});
1e3baf05 1033
d3179e1c
OB
1034 my $conf;
1035 if ($param->{snapshot}) {
1036 $conf = PVE::QemuConfig->load_snapshot_config($param->{vmid}, $param->{snapshot});
1037 } else {
1038 $conf = PVE::QemuConfig->load_current_config($param->{vmid}, $param->{current});
2254ffcf 1039 }
d3179e1c 1040 $conf->{cipassword} = '**********' if $conf->{cipassword};
1e3baf05 1041 return $conf;
d3179e1c 1042
1e3baf05
DM
1043 }});
1044
1e7f2726
DM
1045__PACKAGE__->register_method({
1046 name => 'vm_pending',
1047 path => '{vmid}/pending',
1048 method => 'GET',
1049 proxyto => 'node',
86ea0ed0 1050 description => "Get the virtual machine configuration with both current and pending values.",
1e7f2726
DM
1051 permissions => {
1052 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1053 },
1054 parameters => {
1055 additionalProperties => 0,
1056 properties => {
1057 node => get_standard_option('pve-node'),
335af808 1058 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1e7f2726
DM
1059 },
1060 },
1061 returns => {
1062 type => "array",
1063 items => {
1064 type => "object",
1065 properties => {
1066 key => {
1067 description => "Configuration option name.",
1068 type => 'string',
1069 },
1070 value => {
1071 description => "Current value.",
1072 type => 'string',
1073 optional => 1,
1074 },
1075 pending => {
1076 description => "Pending value.",
1077 type => 'string',
1078 optional => 1,
1079 },
1080 delete => {
1bc483f6
WB
1081 description => "Indicates a pending delete request if present and not 0. " .
1082 "The value 2 indicates a force-delete request.",
1083 type => 'integer',
1084 minimum => 0,
1085 maximum => 2,
1e7f2726
DM
1086 optional => 1,
1087 },
1088 },
1089 },
1090 },
1091 code => sub {
1092 my ($param) = @_;
1093
ffda963f 1094 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1e7f2726 1095
98bc3aeb 1096 my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
1e7f2726 1097
59ef7003
OB
1098 $conf->{cipassword} = '**********' if defined($conf->{cipassword});
1099 $conf->{pending}->{cipassword} = '********** ' if defined($conf->{pending}->{cipassword});
1e7f2726 1100
69f2907c 1101 return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
59ef7003 1102 }});
1e7f2726 1103
5555edea
DM
1104# POST/PUT {vmid}/config implementation
1105#
1106# The original API used PUT (idempotent) an we assumed that all operations
1107# are fast. But it turned out that almost any configuration change can
1108# involve hot-plug actions, or disk alloc/free. Such actions can take long
1109# time to complete and have side effects (not idempotent).
1110#
7043d946 1111# The new implementation uses POST and forks a worker process. We added
5555edea 1112# a new option 'background_delay'. If specified we wait up to
7043d946 1113# 'background_delay' second for the worker task to complete. It returns null
5555edea 1114# if the task is finished within that time, else we return the UPID.
7043d946 1115
5555edea
DM
1116my $update_vm_api = sub {
1117 my ($param, $sync) = @_;
a0d1b1a2 1118
5555edea 1119 my $rpcenv = PVE::RPCEnvironment::get();
1e3baf05 1120
5555edea 1121 my $authuser = $rpcenv->get_user();
1e3baf05 1122
5555edea 1123 my $node = extract_param($param, 'node');
1e3baf05 1124
5555edea 1125 my $vmid = extract_param($param, 'vmid');
1e3baf05 1126
5555edea 1127 my $digest = extract_param($param, 'digest');
1e3baf05 1128
5555edea 1129 my $background_delay = extract_param($param, 'background_delay');
1e3baf05 1130
cefb41fa
WB
1131 if (defined(my $cipassword = $param->{cipassword})) {
1132 # Same logic as in cloud-init (but with the regex fixed...)
1133 $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
1134 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1135 }
0c9a7596 1136
5555edea 1137 my @paramarr = (); # used for log message
edd48c32 1138 foreach my $key (sort keys %$param) {
cefb41fa
WB
1139 my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
1140 push @paramarr, "-$key", $value;
5555edea 1141 }
0532bc63 1142
5555edea
DM
1143 my $skiplock = extract_param($param, 'skiplock');
1144 raise_param_exc({ skiplock => "Only root may use this option." })
1145 if $skiplock && $authuser ne 'root@pam';
1e3baf05 1146
5555edea 1147 my $delete_str = extract_param($param, 'delete');
0532bc63 1148
d3df8cf3
DM
1149 my $revert_str = extract_param($param, 'revert');
1150
5555edea 1151 my $force = extract_param($param, 'force');
1e68cb19 1152
0c9a7596 1153 if (defined(my $ssh_keys = $param->{sshkeys})) {
231f824b
WB
1154 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
1155 PVE::Tools::validate_ssh_public_keys($ssh_keys);
0c9a7596
AD
1156 }
1157
d3df8cf3 1158 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
7bfdeb5f 1159
5555edea 1160 my $storecfg = PVE::Storage::config();
1e68cb19 1161
5555edea 1162 my $defaults = PVE::QemuServer::load_defaults();
1e68cb19 1163
5555edea 1164 &$resolve_cdrom_alias($param);
0532bc63 1165
5555edea 1166 # now try to verify all parameters
ae57f6b3 1167
d3df8cf3
DM
1168 my $revert = {};
1169 foreach my $opt (PVE::Tools::split_list($revert_str)) {
1170 if (!PVE::QemuServer::option_exists($opt)) {
1171 raise_param_exc({ revert => "unknown option '$opt'" });
1172 }
1173
1174 raise_param_exc({ delete => "you can't use '-$opt' and " .
1175 "-revert $opt' at the same time" })
1176 if defined($param->{$opt});
1177
1178 $revert->{$opt} = 1;
1179 }
1180
5555edea
DM
1181 my @delete = ();
1182 foreach my $opt (PVE::Tools::split_list($delete_str)) {
1183 $opt = 'ide2' if $opt eq 'cdrom';
d3df8cf3 1184
5555edea
DM
1185 raise_param_exc({ delete => "you can't use '-$opt' and " .
1186 "-delete $opt' at the same time" })
1187 if defined($param->{$opt});
7043d946 1188
d3df8cf3
DM
1189 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
1190 "-revert $opt' at the same time" })
1191 if $revert->{$opt};
1192
5555edea
DM
1193 if (!PVE::QemuServer::option_exists($opt)) {
1194 raise_param_exc({ delete => "unknown option '$opt'" });
0532bc63 1195 }
1e3baf05 1196
5555edea
DM
1197 push @delete, $opt;
1198 }
1199
17677004
WB
1200 my $repl_conf = PVE::ReplicationConfig->new();
1201 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1202 my $check_replication = sub {
1203 my ($drive) = @_;
1204 return if !$is_replicated;
1205 my $volid = $drive->{file};
1206 return if !$volid || !($drive->{replicate}//1);
1207 return if PVE::QemuServer::drive_is_cdrom($drive);
21e1ee7b
ML
1208
1209 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
35171ddb
FG
1210 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1211 if !defined($storeid);
1212
e3d31944 1213 return if defined($volname) && $volname eq 'cloudinit';
21e1ee7b
ML
1214
1215 my $format;
17677004
WB
1216 if ($volid =~ $NEW_DISK_RE) {
1217 $storeid = $2;
1218 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
1219 } else {
17677004
WB
1220 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
1221 }
1222 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
9b1396ed
WB
1223 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1224 return if $scfg->{shared};
17677004
WB
1225 die "cannot add non-replicatable volume to a replicated VM\n";
1226 };
1227
7979bbcd
FE
1228 $check_drive_param->($param, $storecfg, $check_replication);
1229
5555edea 1230 foreach my $opt (keys %$param) {
7979bbcd 1231 if ($opt =~ m/^net(\d+)$/) {
5555edea
DM
1232 # add macaddr
1233 my $net = PVE::QemuServer::parse_net($param->{$opt});
1234 $param->{$opt} = PVE::QemuServer::print_net($net);
6ee499ff
DC
1235 } elsif ($opt eq 'vmgenid') {
1236 if ($param->{$opt} eq '1') {
1237 $param->{$opt} = PVE::QemuServer::generate_uuid();
1238 }
9e784b11
DC
1239 } elsif ($opt eq 'hookscript') {
1240 eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); };
1241 raise_param_exc({ $opt => $@ }) if $@;
1e68cb19 1242 }
5555edea 1243 }
1e3baf05 1244
5555edea 1245 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
ae57f6b3 1246
e30f75c5 1247 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
ae57f6b3 1248
5555edea 1249 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1e3baf05 1250
5555edea 1251 my $updatefn = sub {
1e3baf05 1252
ffda963f 1253 my $conf = PVE::QemuConfig->load_config($vmid);
1e3baf05 1254
5555edea
DM
1255 die "checksum missmatch (file change by other user?)\n"
1256 if $digest && $digest ne $conf->{digest};
1257
6e72f90b
SR
1258 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1259
546644e2
TL
1260 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1261 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1262 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1263 delete $conf->{lock}; # for check lock check, not written out
1264 push @delete, 'lock'; # this is the real deal to write it out
1265 }
1266 push @delete, 'runningmachine' if $conf->{runningmachine};
ea1c2110 1267 push @delete, 'runningcpu' if $conf->{runningcpu};
546644e2
TL
1268 }
1269
ffda963f 1270 PVE::QemuConfig->check_lock($conf) if !$skiplock;
7043d946 1271
d3df8cf3
DM
1272 foreach my $opt (keys %$revert) {
1273 if (defined($conf->{$opt})) {
1274 $param->{$opt} = $conf->{$opt};
1275 } elsif (defined($conf->{pending}->{$opt})) {
1276 push @delete, $opt;
1277 }
1278 }
1279
5555edea 1280 if ($param->{memory} || defined($param->{balloon})) {
6ca8b698
DM
1281 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
1282 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
7043d946 1283
5555edea
DM
1284 die "balloon value too large (must be smaller than assigned memory)\n"
1285 if $balloon && $balloon > $maxmem;
1286 }
1e3baf05 1287
5555edea 1288 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1e3baf05 1289
5555edea 1290 my $worker = sub {
7bfdeb5f 1291
5555edea 1292 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
c2a64aa7 1293
202d1f45
DM
1294 # write updates to pending section
1295
3a11fadb
DM
1296 my $modified = {}; # record what $option we modify
1297
11c601e9
TL
1298 my @bootorder;
1299 if (my $boot = $conf->{boot}) {
1300 my $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $boot);
1301 @bootorder = PVE::Tools::split_list($bootcfg->{order}) if $bootcfg && $bootcfg->{order};
1302 }
078c109f
SR
1303 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1304
bb660bc3
DC
1305 my $check_drive_perms = sub {
1306 my ($opt, $val) = @_;
1307 my $drive = PVE::QemuServer::parse_drive($opt, $val);
1308 # FIXME: cloudinit: CDROM or Disk?
1309 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1310 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1311 } else {
1312 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1313 }
1314 };
1315
202d1f45 1316 foreach my $opt (@delete) {
3a11fadb 1317 $modified->{$opt} = 1;
ffda963f 1318 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
f70a6ea9
TL
1319
1320 # value of what we want to delete, independent if pending or not
1321 my $val = $conf->{$opt} // $conf->{pending}->{$opt};
1322 if (!defined($val)) {
d2c6bf93
FG
1323 warn "cannot delete '$opt' - not set in current configuration!\n";
1324 $modified->{$opt} = 0;
1325 next;
1326 }
f70a6ea9 1327 my $is_pending_val = defined($conf->{pending}->{$opt});
6aa43f92 1328 delete $conf->{pending}->{$opt};
d2c6bf93 1329
078c109f
SR
1330 # remove from bootorder if necessary
1331 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1332 @bootorder = grep {$_ ne $opt} @bootorder;
1333 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1334 $modified->{boot} = 1;
1335 }
1336
202d1f45 1337 if ($opt =~ m/^unused/) {
f70a6ea9 1338 my $drive = PVE::QemuServer::parse_drive($opt, $val);
ffda963f 1339 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
4d8d55f1 1340 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3dc38fbb
WB
1341 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1342 delete $conf->{$opt};
ffda963f 1343 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1344 }
6afb6794
DC
1345 } elsif ($opt eq 'vmstate') {
1346 PVE::QemuConfig->check_protection($conf, "can't remove vmstate '$val'");
6afb6794
DC
1347 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, { file => $val }, $rpcenv, $authuser, 1)) {
1348 delete $conf->{$opt};
1349 PVE::QemuConfig->write_config($vmid, $conf);
1350 }
74479ee9 1351 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
ffda963f 1352 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
bb660bc3 1353 $check_drive_perms->($opt, $val);
f70a6ea9
TL
1354 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $val))
1355 if $is_pending_val;
98bc3aeb 1356 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1357 PVE::QemuConfig->write_config($vmid, $conf);
e30f75c5 1358 } elsif ($opt =~ m/^serial\d+$/) {
f70a6ea9 1359 if ($val eq 'socket') {
e5453043
DC
1360 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1361 } elsif ($authuser ne 'root@pam') {
1362 die "only root can delete '$opt' config for real devices\n";
1363 }
98bc3aeb 1364 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
e5453043 1365 PVE::QemuConfig->write_config($vmid, $conf);
165be267 1366 } elsif ($opt =~ m/^usb\d+$/) {
f70a6ea9 1367 if ($val =~ m/spice/) {
165be267
DC
1368 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1369 } elsif ($authuser ne 'root@pam') {
1370 die "only root can delete '$opt' config for real devices\n";
1371 }
98bc3aeb 1372 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
165be267 1373 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1374 } else {
98bc3aeb 1375 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1376 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1377 }
5d39a182 1378 }
1e3baf05 1379
202d1f45 1380 foreach my $opt (keys %$param) { # add/change
3a11fadb 1381 $modified->{$opt} = 1;
ffda963f 1382 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
202d1f45
DM
1383 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1384
de64f101 1385 my $arch = PVE::QemuServer::get_vm_arch($conf);
96ed3574 1386
74479ee9 1387 if (PVE::QemuServer::is_valid_drivename($opt)) {
2c44ec49
DC
1388 # old drive
1389 if ($conf->{$opt}) {
1390 $check_drive_perms->($opt, $conf->{$opt});
1391 }
1392
1393 # new drive
bb660bc3 1394 $check_drive_perms->($opt, $param->{$opt});
055d554d 1395 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
202d1f45
DM
1396 if defined($conf->{pending}->{$opt});
1397
367e6bf4
FE
1398 my (undef, $created_opts) = $create_disks->(
1399 $rpcenv,
1400 $authuser,
1401 $conf,
1402 $arch,
1403 $storecfg,
1404 $vmid,
1405 undef,
1406 {$opt => $param->{$opt}},
1407 );
1408 $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
deb734e3 1409
a2e22f9f
DC
1410 # default legacy boot order implies all cdroms anyway
1411 if (@bootorder) {
1412 # append new CD drives to bootorder to mark them bootable
1413 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1414 if (PVE::QemuServer::drive_is_cdrom($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1415 push @bootorder, $opt;
1416 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1417 $modified->{boot} = 1;
1418 }
deb734e3 1419 }
e30f75c5
DC
1420 } elsif ($opt =~ m/^serial\d+/) {
1421 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1422 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1423 } elsif ($authuser ne 'root@pam') {
1424 die "only root can modify '$opt' config for real devices\n";
1425 }
1426 $conf->{pending}->{$opt} = $param->{$opt};
165be267
DC
1427 } elsif ($opt =~ m/^usb\d+/) {
1428 if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
1429 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1430 } elsif ($authuser ne 'root@pam') {
1431 die "only root can modify '$opt' config for real devices\n";
1432 }
1433 $conf->{pending}->{$opt} = $param->{$opt};
202d1f45
DM
1434 } else {
1435 $conf->{pending}->{$opt} = $param->{$opt};
078c109f
SR
1436
1437 if ($opt eq 'boot') {
1438 my $new_bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $param->{$opt});
1439 if ($new_bootcfg->{order}) {
1440 my @devs = PVE::Tools::split_list($new_bootcfg->{order});
1441 for my $dev (@devs) {
c9c32c1b 1442 my $exists = $conf->{$dev} || $conf->{pending}->{$dev} || $param->{$dev};
078c109f
SR
1443 my $deleted = grep {$_ eq $dev} @delete;
1444 die "invalid bootorder: device '$dev' does not exist'\n"
1445 if !$exists || $deleted;
1446 }
1447
1448 # remove legacy boot order settings if new one set
1449 $conf->{pending}->{$opt} = PVE::QemuServer::print_bootorder(\@devs);
1450 PVE::QemuConfig->add_to_pending_delete($conf, "bootdisk")
1451 if $conf->{bootdisk};
1452 }
1453 }
202d1f45 1454 }
98bc3aeb 1455 PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
ffda963f 1456 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45
DM
1457 }
1458
1459 # remove pending changes when nothing changed
ffda963f 1460 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
98bc3aeb 1461 my $changes = PVE::QemuConfig->cleanup_pending($conf);
ffda963f 1462 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
202d1f45
DM
1463
1464 return if !scalar(keys %{$conf->{pending}});
1465
7bfdeb5f 1466 my $running = PVE::QemuServer::check_running($vmid);
39001640
DM
1467
1468 # apply pending changes
1469
ffda963f 1470 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
39001640 1471
eb5e482d 1472 my $errors = {};
3a11fadb 1473 if ($running) {
3a11fadb 1474 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
3a11fadb 1475 } else {
4a5cb613 1476 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $errors);
3a11fadb 1477 }
eb5e482d 1478 raise_param_exc($errors) if scalar(keys %$errors);
1e68cb19 1479
915d3481 1480 return;
5d39a182
DM
1481 };
1482
5555edea
DM
1483 if ($sync) {
1484 &$worker();
d1c1af4b 1485 return;
5555edea
DM
1486 } else {
1487 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
fcdb0117 1488
5555edea
DM
1489 if ($background_delay) {
1490
1491 # Note: It would be better to do that in the Event based HTTPServer
7043d946 1492 # to avoid blocking call to sleep.
5555edea
DM
1493
1494 my $end_time = time() + $background_delay;
1495
1496 my $task = PVE::Tools::upid_decode($upid);
1497
1498 my $running = 1;
1499 while (time() < $end_time) {
1500 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1501 last if !$running;
1502 sleep(1); # this gets interrupted when child process ends
1503 }
1504
1505 if (!$running) {
1506 my $status = PVE::Tools::upid_read_status($upid);
872cfcf5 1507 return if !PVE::Tools::upid_status_is_error($status);
ce3fbcd4 1508 die "failed to update VM $vmid: $status\n";
5555edea 1509 }
7043d946 1510 }
5555edea
DM
1511
1512 return $upid;
1513 }
1514 };
1515
ffda963f 1516 return PVE::QemuConfig->lock_config($vmid, $updatefn);
5555edea
DM
1517};
1518
1519my $vm_config_perm_list = [
1520 'VM.Config.Disk',
1521 'VM.Config.CDROM',
1522 'VM.Config.CPU',
1523 'VM.Config.Memory',
1524 'VM.Config.Network',
1525 'VM.Config.HWType',
1526 'VM.Config.Options',
fc701af7 1527 'VM.Config.Cloudinit',
5555edea
DM
1528 ];
1529
1530__PACKAGE__->register_method({
1531 name => 'update_vm_async',
1532 path => '{vmid}/config',
1533 method => 'POST',
1534 protected => 1,
1535 proxyto => 'node',
1536 description => "Set virtual machine options (asynchrounous API).",
1537 permissions => {
1538 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1539 },
1540 parameters => {
3326ae19 1541 additionalProperties => 0,
5555edea
DM
1542 properties => PVE::QemuServer::json_config_properties(
1543 {
1544 node => get_standard_option('pve-node'),
1545 vmid => get_standard_option('pve-vmid'),
1546 skiplock => get_standard_option('skiplock'),
1547 delete => {
1548 type => 'string', format => 'pve-configid-list',
1549 description => "A list of settings you want to delete.",
1550 optional => 1,
1551 },
4c8365fa
DM
1552 revert => {
1553 type => 'string', format => 'pve-configid-list',
1554 description => "Revert a pending change.",
1555 optional => 1,
1556 },
5555edea
DM
1557 force => {
1558 type => 'boolean',
1559 description => $opt_force_description,
1560 optional => 1,
1561 requires => 'delete',
1562 },
1563 digest => {
1564 type => 'string',
1565 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1566 maxLength => 40,
1567 optional => 1,
1568 },
1569 background_delay => {
1570 type => 'integer',
1571 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1572 minimum => 1,
1573 maximum => 30,
1574 optional => 1,
1575 },
1576 }),
1577 },
1578 returns => {
1579 type => 'string',
1580 optional => 1,
1581 },
1582 code => $update_vm_api,
1583});
1584
1585__PACKAGE__->register_method({
1586 name => 'update_vm',
1587 path => '{vmid}/config',
1588 method => 'PUT',
1589 protected => 1,
1590 proxyto => 'node',
1591 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1592 permissions => {
1593 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1594 },
1595 parameters => {
3326ae19 1596 additionalProperties => 0,
5555edea
DM
1597 properties => PVE::QemuServer::json_config_properties(
1598 {
1599 node => get_standard_option('pve-node'),
335af808 1600 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5555edea
DM
1601 skiplock => get_standard_option('skiplock'),
1602 delete => {
1603 type => 'string', format => 'pve-configid-list',
1604 description => "A list of settings you want to delete.",
1605 optional => 1,
1606 },
4c8365fa
DM
1607 revert => {
1608 type => 'string', format => 'pve-configid-list',
1609 description => "Revert a pending change.",
1610 optional => 1,
1611 },
5555edea
DM
1612 force => {
1613 type => 'boolean',
1614 description => $opt_force_description,
1615 optional => 1,
1616 requires => 'delete',
1617 },
1618 digest => {
1619 type => 'string',
1620 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1621 maxLength => 40,
1622 optional => 1,
1623 },
1624 }),
1625 },
1626 returns => { type => 'null' },
1627 code => sub {
1628 my ($param) = @_;
1629 &$update_vm_api($param, 1);
d1c1af4b 1630 return;
5555edea
DM
1631 }
1632});
1e3baf05 1633
1e3baf05 1634__PACKAGE__->register_method({
afdb31d5
DM
1635 name => 'destroy_vm',
1636 path => '{vmid}',
1e3baf05
DM
1637 method => 'DELETE',
1638 protected => 1,
1639 proxyto => 'node',
e00319af
TL
1640 description => "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
1641 ." and firewall rules",
a0d1b1a2
DM
1642 permissions => {
1643 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1644 },
1e3baf05 1645 parameters => {
3326ae19 1646 additionalProperties => 0,
1e3baf05
DM
1647 properties => {
1648 node => get_standard_option('pve-node'),
335af808 1649 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60 1650 skiplock => get_standard_option('skiplock'),
d9123ef5
CE
1651 purge => {
1652 type => 'boolean',
e00319af 1653 description => "Remove VMID from configurations, like backup & replication jobs and HA.",
d9123ef5
CE
1654 optional => 1,
1655 },
75854662
TL
1656 'destroy-unreferenced-disks' => {
1657 type => 'boolean',
99676a6c
TL
1658 description => "If set, destroy additionally all disks not referenced in the config"
1659 ." but with a matching VMID from all enabled storages.",
75854662 1660 optional => 1,
16e66777 1661 default => 0,
47f9f50b 1662 },
1e3baf05
DM
1663 },
1664 },
afdb31d5 1665 returns => {
5fdbe4f0
DM
1666 type => 'string',
1667 },
1e3baf05
DM
1668 code => sub {
1669 my ($param) = @_;
1670
1671 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 1672 my $authuser = $rpcenv->get_user();
1e3baf05
DM
1673 my $vmid = $param->{vmid};
1674
1675 my $skiplock = $param->{skiplock};
afdb31d5 1676 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 1677 if $skiplock && $authuser ne 'root@pam';
1e3baf05 1678
a4c7029d
FG
1679 my $early_checks = sub {
1680 # test if VM exists
1681 my $conf = PVE::QemuConfig->load_config($vmid);
1682 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
7c4351f7 1683
a4c7029d 1684 my $ha_managed = PVE::HA::Config::service_is_configured("vm:$vmid");
e9f2f8e5 1685
a4c7029d
FG
1686 if (!$param->{purge}) {
1687 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
1688 if $ha_managed;
1689 # don't allow destroy if with replication jobs but no purge param
1690 my $repl_conf = PVE::ReplicationConfig->new();
1691 $repl_conf->check_for_existing_jobs($vmid);
1692 }
628bb7f2 1693
a4c7029d
FG
1694 die "VM $vmid is running - destroy failed\n"
1695 if PVE::QemuServer::check_running($vmid);
1696
1697 return $ha_managed;
1698 };
1699
1700 $early_checks->();
db593da2 1701
5fdbe4f0 1702 my $realcmd = sub {
ff1a2432
DM
1703 my $upid = shift;
1704
a4c7029d
FG
1705 my $storecfg = PVE::Storage::config();
1706
ff1a2432 1707 syslog('info', "destroy VM $vmid: $upid\n");
3e8e214d 1708 PVE::QemuConfig->lock_config($vmid, sub {
a4c7029d
FG
1709 # repeat, config might have changed
1710 my $ha_managed = $early_checks->();
d9123ef5 1711
16e66777 1712 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
75854662
TL
1713
1714 PVE::QemuServer::destroy_vm(
1715 $storecfg,
1716 $vmid,
1717 $skiplock, { lock => 'destroyed' },
1718 $purge_unreferenced,
1719 );
d9123ef5 1720
3e8e214d
DJ
1721 PVE::AccessControl::remove_vm_access($vmid);
1722 PVE::Firewall::remove_vmfw_conf($vmid);
d9123ef5 1723 if ($param->{purge}) {
7c4351f7 1724 print "purging VM $vmid from related configurations..\n";
d9123ef5
CE
1725 PVE::ReplicationConfig::remove_vmid_jobs($vmid);
1726 PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid);
7c4351f7
TL
1727
1728 if ($ha_managed) {
1729 PVE::HA::Config::delete_service_from_config("vm:$vmid");
1730 print "NOTE: removed VM $vmid from HA resource configuration.\n";
1731 }
d9123ef5 1732 }
5172770d
TL
1733
1734 # only now remove the zombie config, else we can have reuse race
1735 PVE::QemuConfig->destroy_config($vmid);
3e8e214d 1736 });
5fdbe4f0 1737 };
1e3baf05 1738
a0d1b1a2 1739 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1e3baf05
DM
1740 }});
1741
1742__PACKAGE__->register_method({
afdb31d5
DM
1743 name => 'unlink',
1744 path => '{vmid}/unlink',
1e3baf05
DM
1745 method => 'PUT',
1746 protected => 1,
1747 proxyto => 'node',
1748 description => "Unlink/delete disk images.",
a0d1b1a2
DM
1749 permissions => {
1750 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1751 },
1e3baf05 1752 parameters => {
3326ae19 1753 additionalProperties => 0,
1e3baf05
DM
1754 properties => {
1755 node => get_standard_option('pve-node'),
335af808 1756 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1e3baf05
DM
1757 idlist => {
1758 type => 'string', format => 'pve-configid-list',
1759 description => "A list of disk IDs you want to delete.",
1760 },
1761 force => {
1762 type => 'boolean',
1763 description => $opt_force_description,
1764 optional => 1,
1765 },
1766 },
1767 },
1768 returns => { type => 'null'},
1769 code => sub {
1770 my ($param) = @_;
1771
1772 $param->{delete} = extract_param($param, 'idlist');
1773
1774 __PACKAGE__->update_vm($param);
1775
d1c1af4b 1776 return;
1e3baf05
DM
1777 }});
1778
3c5bdde8
TL
1779# uses good entropy, each char is limited to 6 bit to get printable chars simply
1780my $gen_rand_chars = sub {
1781 my ($length) = @_;
1782
1783 die "invalid length $length" if $length < 1;
1784
1785 my $min = ord('!'); # first printable ascii
a4e128a9
FG
1786
1787 my $rand_bytes = Crypt::OpenSSL::Random::random_bytes($length);
1788 die "failed to generate random bytes!\n"
1789 if !$rand_bytes;
1790
1791 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
3c5bdde8
TL
1792
1793 return $str;
1794};
1795
1e3baf05
DM
1796my $sslcert;
1797
1798__PACKAGE__->register_method({
afdb31d5
DM
1799 name => 'vncproxy',
1800 path => '{vmid}/vncproxy',
1e3baf05
DM
1801 method => 'POST',
1802 protected => 1,
1803 permissions => {
378b359e 1804 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1e3baf05
DM
1805 },
1806 description => "Creates a TCP VNC proxy connections.",
1807 parameters => {
3326ae19 1808 additionalProperties => 0,
1e3baf05
DM
1809 properties => {
1810 node => get_standard_option('pve-node'),
1811 vmid => get_standard_option('pve-vmid'),
b4d5c000
SP
1812 websocket => {
1813 optional => 1,
1814 type => 'boolean',
1815 description => "starts websockify instead of vncproxy",
1816 },
3c5bdde8
TL
1817 'generate-password' => {
1818 optional => 1,
1819 type => 'boolean',
1820 default => 0,
1821 description => "Generates a random password to be used as ticket instead of the API ticket.",
1822 },
1e3baf05
DM
1823 },
1824 },
afdb31d5 1825 returns => {
3326ae19 1826 additionalProperties => 0,
1e3baf05
DM
1827 properties => {
1828 user => { type => 'string' },
1829 ticket => { type => 'string' },
3c5bdde8
TL
1830 password => {
1831 optional => 1,
1832 description => "Returned if requested with 'generate-password' param."
1833 ." Consists of printable ASCII characters ('!' .. '~').",
1834 type => 'string',
1835 },
1e3baf05
DM
1836 cert => { type => 'string' },
1837 port => { type => 'integer' },
1838 upid => { type => 'string' },
1839 },
1840 },
1841 code => sub {
1842 my ($param) = @_;
1843
1844 my $rpcenv = PVE::RPCEnvironment::get();
1845
a0d1b1a2 1846 my $authuser = $rpcenv->get_user();
1e3baf05
DM
1847
1848 my $vmid = $param->{vmid};
1849 my $node = $param->{node};
983d4582 1850 my $websocket = $param->{websocket};
1e3baf05 1851
ffda963f 1852 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
326007b2 1853
d3efae29
FG
1854 my $serial;
1855 if ($conf->{vga}) {
1856 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
1857 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
1858 }
ef5e2be2 1859
b6f39da2
DM
1860 my $authpath = "/vms/$vmid";
1861
a0d1b1a2 1862 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
3c5bdde8
TL
1863 my $password = $ticket;
1864 if ($param->{'generate-password'}) {
1865 $password = $gen_rand_chars->(8);
1866 }
b6f39da2 1867
1e3baf05
DM
1868 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1869 if !$sslcert;
1870
414b42d8 1871 my $family;
ef5e2be2 1872 my $remcmd = [];
afdb31d5 1873
4f1be36c 1874 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 1875 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b 1876 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
b4d5c000 1877 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
d3efae29 1878 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, defined($serial) ? '-t' : '-T');
af0eba7e
WB
1879 } else {
1880 $family = PVE::Tools::get_host_address_family($node);
1e3baf05
DM
1881 }
1882
af0eba7e
WB
1883 my $port = PVE::Tools::next_vnc_port($family);
1884
afdb31d5 1885 my $timeout = 10;
1e3baf05
DM
1886
1887 my $realcmd = sub {
1888 my $upid = shift;
1889
1890 syslog('info', "starting vnc proxy $upid\n");
1891
ef5e2be2 1892 my $cmd;
1e3baf05 1893
d3efae29 1894 if (defined($serial)) {
b4d5c000 1895
d3efae29 1896 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
9e6d6e97 1897
ef5e2be2 1898 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
fa8ea931 1899 '-timeout', $timeout, '-authpath', $authpath,
9e6d6e97
DC
1900 '-perm', 'Sys.Console'];
1901
1902 if ($param->{websocket}) {
3c5bdde8 1903 $ENV{PVE_VNC_TICKET} = $password; # pass ticket to vncterm
9e6d6e97
DC
1904 push @$cmd, '-notls', '-listen', 'localhost';
1905 }
1906
1907 push @$cmd, '-c', @$remcmd, @$termcmd;
1908
655d7462 1909 PVE::Tools::run_command($cmd);
9e6d6e97 1910
ef5e2be2 1911 } else {
1e3baf05 1912
3c5bdde8 1913 $ENV{LC_PVE_TICKET} = $password if $websocket; # set ticket with "qm vncproxy"
3e7567e0 1914
655d7462
WB
1915 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1916
1917 my $sock = IO::Socket::IP->new(
dd32a466 1918 ReuseAddr => 1,
655d7462
WB
1919 Listen => 1,
1920 LocalPort => $port,
1921 Proto => 'tcp',
1922 GetAddrInfoFlags => 0,
b63f34b8 1923 ) or die "failed to create socket: $!\n";
655d7462
WB
1924 # Inside the worker we shouldn't have any previous alarms
1925 # running anyway...:
1926 alarm(0);
1927 local $SIG{ALRM} = sub { die "connection timed out\n" };
1928 alarm $timeout;
1929 accept(my $cli, $sock) or die "connection failed: $!\n";
058ff55b 1930 alarm(0);
655d7462
WB
1931 close($sock);
1932 if (PVE::Tools::run_command($cmd,
1933 output => '>&'.fileno($cli),
1934 input => '<&'.fileno($cli),
1935 noerr => 1) != 0)
1936 {
1937 die "Failed to run vncproxy.\n";
1938 }
ef5e2be2 1939 }
1e3baf05 1940
1e3baf05
DM
1941 return;
1942 };
1943
2c7fc947 1944 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1e3baf05 1945
3da85107
DM
1946 PVE::Tools::wait_for_vnc_port($port);
1947
3c5bdde8 1948 my $res = {
a0d1b1a2 1949 user => $authuser,
1e3baf05 1950 ticket => $ticket,
afdb31d5
DM
1951 port => $port,
1952 upid => $upid,
1953 cert => $sslcert,
1e3baf05 1954 };
3c5bdde8
TL
1955 $res->{password} = $password if $param->{'generate-password'};
1956
1957 return $res;
1e3baf05
DM
1958 }});
1959
87302002
DC
1960__PACKAGE__->register_method({
1961 name => 'termproxy',
1962 path => '{vmid}/termproxy',
1963 method => 'POST',
1964 protected => 1,
1965 permissions => {
1966 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1967 },
1968 description => "Creates a TCP proxy connections.",
1969 parameters => {
1970 additionalProperties => 0,
1971 properties => {
1972 node => get_standard_option('pve-node'),
1973 vmid => get_standard_option('pve-vmid'),
1974 serial=> {
1975 optional => 1,
1976 type => 'string',
1977 enum => [qw(serial0 serial1 serial2 serial3)],
1978 description => "opens a serial terminal (defaults to display)",
1979 },
1980 },
1981 },
1982 returns => {
1983 additionalProperties => 0,
1984 properties => {
1985 user => { type => 'string' },
1986 ticket => { type => 'string' },
1987 port => { type => 'integer' },
1988 upid => { type => 'string' },
1989 },
1990 },
1991 code => sub {
1992 my ($param) = @_;
1993
1994 my $rpcenv = PVE::RPCEnvironment::get();
1995
1996 my $authuser = $rpcenv->get_user();
1997
1998 my $vmid = $param->{vmid};
1999 my $node = $param->{node};
2000 my $serial = $param->{serial};
2001
2002 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
2003
2004 if (!defined($serial)) {
d7856be5
FG
2005 if ($conf->{vga}) {
2006 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
2007 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
87302002
DC
2008 }
2009 }
2010
2011 my $authpath = "/vms/$vmid";
2012
2013 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
2014
414b42d8
DC
2015 my $family;
2016 my $remcmd = [];
87302002
DC
2017
2018 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 2019 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b
FG
2020 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
2021 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, '-t');
414b42d8 2022 push @$remcmd, '--';
87302002
DC
2023 } else {
2024 $family = PVE::Tools::get_host_address_family($node);
2025 }
2026
2027 my $port = PVE::Tools::next_vnc_port($family);
2028
ccb88f45 2029 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
87302002
DC
2030 push @$termcmd, '-iface', $serial if $serial;
2031
2032 my $realcmd = sub {
2033 my $upid = shift;
2034
2035 syslog('info', "starting qemu termproxy $upid\n");
2036
2037 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2038 '--perm', 'VM.Console', '--'];
2039 push @$cmd, @$remcmd, @$termcmd;
2040
2041 PVE::Tools::run_command($cmd);
2042 };
2043
2044 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2045
2046 PVE::Tools::wait_for_vnc_port($port);
2047
2048 return {
2049 user => $authuser,
2050 ticket => $ticket,
2051 port => $port,
2052 upid => $upid,
2053 };
2054 }});
2055
3e7567e0
DM
2056__PACKAGE__->register_method({
2057 name => 'vncwebsocket',
2058 path => '{vmid}/vncwebsocket',
2059 method => 'GET',
3e7567e0 2060 permissions => {
c422ce93 2061 description => "You also need to pass a valid ticket (vncticket).",
3e7567e0
DM
2062 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2063 },
4d00f52f 2064 description => "Opens a weksocket for VNC traffic.",
3e7567e0 2065 parameters => {
3326ae19 2066 additionalProperties => 0,
3e7567e0
DM
2067 properties => {
2068 node => get_standard_option('pve-node'),
2069 vmid => get_standard_option('pve-vmid'),
c422ce93
DM
2070 vncticket => {
2071 description => "Ticket from previous call to vncproxy.",
2072 type => 'string',
2073 maxLength => 512,
2074 },
3e7567e0
DM
2075 port => {
2076 description => "Port number returned by previous vncproxy call.",
2077 type => 'integer',
2078 minimum => 5900,
2079 maximum => 5999,
2080 },
2081 },
2082 },
2083 returns => {
2084 type => "object",
2085 properties => {
2086 port => { type => 'string' },
2087 },
2088 },
2089 code => sub {
2090 my ($param) = @_;
2091
2092 my $rpcenv = PVE::RPCEnvironment::get();
2093
2094 my $authuser = $rpcenv->get_user();
2095
2096 my $vmid = $param->{vmid};
2097 my $node = $param->{node};
2098
c422ce93
DM
2099 my $authpath = "/vms/$vmid";
2100
2101 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
2102
ffda963f 2103 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
3e7567e0
DM
2104
2105 # Note: VNC ports are acessible from outside, so we do not gain any
2106 # security if we verify that $param->{port} belongs to VM $vmid. This
2107 # check is done by verifying the VNC ticket (inside VNC protocol).
2108
2109 my $port = $param->{port};
f34ebd52 2110
3e7567e0
DM
2111 return { port => $port };
2112 }});
2113
288eeea8
DM
2114__PACKAGE__->register_method({
2115 name => 'spiceproxy',
2116 path => '{vmid}/spiceproxy',
78252ce7 2117 method => 'POST',
288eeea8 2118 protected => 1,
78252ce7 2119 proxyto => 'node',
288eeea8
DM
2120 permissions => {
2121 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2122 },
2123 description => "Returns a SPICE configuration to connect to the VM.",
2124 parameters => {
3326ae19 2125 additionalProperties => 0,
288eeea8
DM
2126 properties => {
2127 node => get_standard_option('pve-node'),
2128 vmid => get_standard_option('pve-vmid'),
dd25eecf 2129 proxy => get_standard_option('spice-proxy', { optional => 1 }),
288eeea8
DM
2130 },
2131 },
dd25eecf 2132 returns => get_standard_option('remote-viewer-config'),
288eeea8
DM
2133 code => sub {
2134 my ($param) = @_;
2135
2136 my $rpcenv = PVE::RPCEnvironment::get();
2137
2138 my $authuser = $rpcenv->get_user();
2139
2140 my $vmid = $param->{vmid};
2141 my $node = $param->{node};
fb6c7260 2142 my $proxy = $param->{proxy};
288eeea8 2143
ffda963f 2144 my $conf = PVE::QemuConfig->load_config($vmid, $node);
7f9e28e4
TL
2145 my $title = "VM $vmid";
2146 $title .= " - ". $conf->{name} if $conf->{name};
288eeea8 2147
943340a6 2148 my $port = PVE::QemuServer::spice_port($vmid);
dd25eecf 2149
f34ebd52 2150 my ($ticket, undef, $remote_viewer_config) =
dd25eecf 2151 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
f34ebd52 2152
0a13e08e
SR
2153 mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
2154 mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
f34ebd52 2155
dd25eecf 2156 return $remote_viewer_config;
288eeea8
DM
2157 }});
2158
5fdbe4f0
DM
2159__PACKAGE__->register_method({
2160 name => 'vmcmdidx',
afdb31d5 2161 path => '{vmid}/status',
5fdbe4f0
DM
2162 method => 'GET',
2163 proxyto => 'node',
2164 description => "Directory index",
a0d1b1a2
DM
2165 permissions => {
2166 user => 'all',
2167 },
5fdbe4f0 2168 parameters => {
3326ae19 2169 additionalProperties => 0,
5fdbe4f0
DM
2170 properties => {
2171 node => get_standard_option('pve-node'),
2172 vmid => get_standard_option('pve-vmid'),
2173 },
2174 },
2175 returns => {
2176 type => 'array',
2177 items => {
2178 type => "object",
2179 properties => {
2180 subdir => { type => 'string' },
2181 },
2182 },
2183 links => [ { rel => 'child', href => "{subdir}" } ],
2184 },
2185 code => sub {
2186 my ($param) = @_;
2187
2188 # test if VM exists
ffda963f 2189 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5fdbe4f0
DM
2190
2191 my $res = [
2192 { subdir => 'current' },
2193 { subdir => 'start' },
2194 { subdir => 'stop' },
58f9db6a
DC
2195 { subdir => 'reset' },
2196 { subdir => 'shutdown' },
2197 { subdir => 'suspend' },
165411f0 2198 { subdir => 'reboot' },
5fdbe4f0 2199 ];
afdb31d5 2200
5fdbe4f0
DM
2201 return $res;
2202 }});
2203
1e3baf05 2204__PACKAGE__->register_method({
afdb31d5 2205 name => 'vm_status',
5fdbe4f0 2206 path => '{vmid}/status/current',
1e3baf05
DM
2207 method => 'GET',
2208 proxyto => 'node',
2209 protected => 1, # qemu pid files are only readable by root
2210 description => "Get virtual machine status.",
a0d1b1a2
DM
2211 permissions => {
2212 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2213 },
1e3baf05 2214 parameters => {
3326ae19 2215 additionalProperties => 0,
1e3baf05
DM
2216 properties => {
2217 node => get_standard_option('pve-node'),
2218 vmid => get_standard_option('pve-vmid'),
2219 },
2220 },
b1a70cab
DM
2221 returns => {
2222 type => 'object',
2223 properties => {
2224 %$PVE::QemuServer::vmstatus_return_properties,
2225 ha => {
2226 description => "HA manager service status.",
2227 type => 'object',
2228 },
2229 spice => {
2230 description => "Qemu VGA configuration supports spice.",
2231 type => 'boolean',
2232 optional => 1,
2233 },
2234 agent => {
2235 description => "Qemu GuestAgent enabled in config.",
2236 type => 'boolean',
2237 optional => 1,
2238 },
2239 },
2240 },
1e3baf05
DM
2241 code => sub {
2242 my ($param) = @_;
2243
2244 # test if VM exists
ffda963f 2245 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1e3baf05 2246
03a33f30 2247 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
8610701a 2248 my $status = $vmstatus->{$param->{vmid}};
1e3baf05 2249
4d2a734e 2250 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
8610701a 2251
86b8228b 2252 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
a2af1bbe 2253 $status->{agent} = 1 if PVE::QemuServer::get_qga_key($conf, 'enabled');
c9a074b8 2254
8610701a 2255 return $status;
1e3baf05
DM
2256 }});
2257
2258__PACKAGE__->register_method({
afdb31d5 2259 name => 'vm_start',
5fdbe4f0
DM
2260 path => '{vmid}/status/start',
2261 method => 'POST',
1e3baf05
DM
2262 protected => 1,
2263 proxyto => 'node',
5fdbe4f0 2264 description => "Start virtual machine.",
a0d1b1a2
DM
2265 permissions => {
2266 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2267 },
1e3baf05 2268 parameters => {
3326ae19 2269 additionalProperties => 0,
1e3baf05
DM
2270 properties => {
2271 node => get_standard_option('pve-node'),
ab5904f7
TL
2272 vmid => get_standard_option('pve-vmid',
2273 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60
DM
2274 skiplock => get_standard_option('skiplock'),
2275 stateuri => get_standard_option('pve-qm-stateuri'),
7e8dcf2c 2276 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
2de2d6f7
TL
2277 migration_type => {
2278 type => 'string',
2279 enum => ['secure', 'insecure'],
2280 description => "Migration traffic is encrypted using an SSH " .
2281 "tunnel by default. On secure, completely private networks " .
2282 "this can be disabled to increase performance.",
2283 optional => 1,
2284 },
2285 migration_network => {
29ddbe70 2286 type => 'string', format => 'CIDR',
2de2d6f7
TL
2287 description => "CIDR of the (sub) network that is used for migration.",
2288 optional => 1,
2289 },
d58b93a8 2290 machine => get_standard_option('pve-qemu-machine'),
58c64ad5
SR
2291 'force-cpu' => {
2292 description => "Override QEMU's -cpu argument with the given string.",
2293 type => 'string',
2294 optional => 1,
2295 },
bf8fc5a3 2296 targetstorage => get_standard_option('pve-targetstorage'),
ef3f4293
TM
2297 timeout => {
2298 description => "Wait maximal timeout seconds.",
2299 type => 'integer',
2300 minimum => 0,
5a7f7b99 2301 default => 'max(30, vm memory in GiB)',
ef3f4293
TM
2302 optional => 1,
2303 },
1e3baf05
DM
2304 },
2305 },
afdb31d5 2306 returns => {
5fdbe4f0
DM
2307 type => 'string',
2308 },
1e3baf05
DM
2309 code => sub {
2310 my ($param) = @_;
2311
2312 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2313 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2314
2315 my $node = extract_param($param, 'node');
1e3baf05 2316 my $vmid = extract_param($param, 'vmid');
ef3f4293 2317 my $timeout = extract_param($param, 'timeout');
952958bc
DM
2318 my $machine = extract_param($param, 'machine');
2319
736c92f6
TL
2320 my $get_root_param = sub {
2321 my $value = extract_param($param, $_[0]);
2322 raise_param_exc({ "$_[0]" => "Only root may use this option." })
2323 if $value && $authuser ne 'root@pam';
2324 return $value;
2325 };
2de2d6f7 2326
736c92f6
TL
2327 my $stateuri = $get_root_param->('stateuri');
2328 my $skiplock = $get_root_param->('skiplock');
2329 my $migratedfrom = $get_root_param->('migratedfrom');
2330 my $migration_type = $get_root_param->('migration_type');
2331 my $migration_network = $get_root_param->('migration_network');
2332 my $targetstorage = $get_root_param->('targetstorage');
6ab41628 2333 my $force_cpu = $get_root_param->('force-cpu');
2189246c 2334
bf8fc5a3
FG
2335 my $storagemap;
2336
2337 if ($targetstorage) {
2338 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
2339 if !$migratedfrom;
2340 $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 2341 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
2342 if $@;
2343 }
2189246c 2344
7c14dcae
DM
2345 # read spice ticket from STDIN
2346 my $spice_ticket;
c4ac8f71 2347 my $nbd_protocol_version = 0;
88126be3 2348 my $replicated_volumes = {};
fd95d780 2349 my $tpmstate_vol;
ccab68c2 2350 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
c4ac8f71 2351 while (defined(my $line = <STDIN>)) {
760fb3c8 2352 chomp $line;
c4ac8f71
ML
2353 if ($line =~ m/^spice_ticket: (.+)$/) {
2354 $spice_ticket = $1;
2355 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2356 $nbd_protocol_version = $1;
88126be3
FG
2357 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2358 $replicated_volumes->{$1} = 1;
fd95d780
FG
2359 } elsif ($line =~ m/^tpmstate0: (.*)$/) {
2360 $tpmstate_vol = $1;
399ca0d6 2361 } elsif (!$spice_ticket) {
c4ac8f71
ML
2362 # fallback for old source node
2363 $spice_ticket = $line;
399ca0d6
FG
2364 } else {
2365 warn "unknown 'start' parameter on STDIN: '$line'\n";
c4ac8f71 2366 }
760fb3c8 2367 }
7c14dcae
DM
2368 }
2369
98cbd0f4
WB
2370 PVE::Cluster::check_cfs_quorum();
2371
afdb31d5 2372 my $storecfg = PVE::Storage::config();
5fdbe4f0 2373
a4262553 2374 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') {
88fc87b4
DM
2375 my $hacmd = sub {
2376 my $upid = shift;
5fdbe4f0 2377
02765844 2378 print "Requesting HA start for VM $vmid\n";
88fc87b4 2379
a4262553 2380 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
88fc87b4 2381 PVE::Tools::run_command($cmd);
88fc87b4
DM
2382 return;
2383 };
2384
2385 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2386
2387 } else {
2388
2389 my $realcmd = sub {
2390 my $upid = shift;
2391
2392 syslog('info', "start VM $vmid: $upid\n");
2393
0c498cca
FG
2394 my $migrate_opts = {
2395 migratedfrom => $migratedfrom,
2396 spice_ticket => $spice_ticket,
2397 network => $migration_network,
2398 type => $migration_type,
bf8fc5a3 2399 storagemap => $storagemap,
0c498cca
FG
2400 nbd_proto_version => $nbd_protocol_version,
2401 replicated_volumes => $replicated_volumes,
fd95d780 2402 tpmstate_vol => $tpmstate_vol,
0c498cca
FG
2403 };
2404
2405 my $params = {
2406 statefile => $stateuri,
2407 skiplock => $skiplock,
2408 forcemachine => $machine,
2409 timeout => $timeout,
58c64ad5 2410 forcecpu => $force_cpu,
0c498cca
FG
2411 };
2412
2413 PVE::QemuServer::vm_start($storecfg, $vmid, $params, $migrate_opts);
88fc87b4
DM
2414 return;
2415 };
5fdbe4f0 2416
88fc87b4
DM
2417 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2418 }
5fdbe4f0
DM
2419 }});
2420
2421__PACKAGE__->register_method({
afdb31d5 2422 name => 'vm_stop',
5fdbe4f0
DM
2423 path => '{vmid}/status/stop',
2424 method => 'POST',
2425 protected => 1,
2426 proxyto => 'node',
346130b2 2427 description => "Stop virtual machine. The qemu process will exit immediately. This" .
d6c747ff 2428 "is akin to pulling the power plug of a running computer and may damage the VM data",
a0d1b1a2
DM
2429 permissions => {
2430 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2431 },
5fdbe4f0 2432 parameters => {
3326ae19 2433 additionalProperties => 0,
5fdbe4f0
DM
2434 properties => {
2435 node => get_standard_option('pve-node'),
ab5904f7
TL
2436 vmid => get_standard_option('pve-vmid',
2437 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2438 skiplock => get_standard_option('skiplock'),
debe8882 2439 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
c6bb9502
DM
2440 timeout => {
2441 description => "Wait maximal timeout seconds.",
2442 type => 'integer',
2443 minimum => 0,
2444 optional => 1,
254575e9
DM
2445 },
2446 keepActive => {
94a17e1d 2447 description => "Do not deactivate storage volumes.",
254575e9
DM
2448 type => 'boolean',
2449 optional => 1,
2450 default => 0,
c6bb9502 2451 }
5fdbe4f0
DM
2452 },
2453 },
afdb31d5 2454 returns => {
5fdbe4f0
DM
2455 type => 'string',
2456 },
2457 code => sub {
2458 my ($param) = @_;
2459
2460 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2461 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2462
2463 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2464 my $vmid = extract_param($param, 'vmid');
2465
2466 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2467 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2468 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2469
254575e9 2470 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2471 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2472 if $keepActive && $authuser ne 'root@pam';
254575e9 2473
af30308f
DM
2474 my $migratedfrom = extract_param($param, 'migratedfrom');
2475 raise_param_exc({ migratedfrom => "Only root may use this option." })
2476 if $migratedfrom && $authuser ne 'root@pam';
2477
2478
ff1a2432
DM
2479 my $storecfg = PVE::Storage::config();
2480
2003f0f8 2481 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
5fdbe4f0 2482
88fc87b4
DM
2483 my $hacmd = sub {
2484 my $upid = shift;
5fdbe4f0 2485
02765844 2486 print "Requesting HA stop for VM $vmid\n";
88fc87b4 2487
1805fac3 2488 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
88fc87b4 2489 PVE::Tools::run_command($cmd);
88fc87b4
DM
2490 return;
2491 };
2492
2493 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2494
2495 } else {
2496 my $realcmd = sub {
2497 my $upid = shift;
2498
2499 syslog('info', "stop VM $vmid: $upid\n");
2500
2501 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
af30308f 2502 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
88fc87b4
DM
2503 return;
2504 };
2505
2506 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2507 }
5fdbe4f0
DM
2508 }});
2509
2510__PACKAGE__->register_method({
afdb31d5 2511 name => 'vm_reset',
5fdbe4f0
DM
2512 path => '{vmid}/status/reset',
2513 method => 'POST',
2514 protected => 1,
2515 proxyto => 'node',
2516 description => "Reset virtual machine.",
a0d1b1a2
DM
2517 permissions => {
2518 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2519 },
5fdbe4f0 2520 parameters => {
3326ae19 2521 additionalProperties => 0,
5fdbe4f0
DM
2522 properties => {
2523 node => get_standard_option('pve-node'),
ab5904f7
TL
2524 vmid => get_standard_option('pve-vmid',
2525 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
2526 skiplock => get_standard_option('skiplock'),
2527 },
2528 },
afdb31d5 2529 returns => {
5fdbe4f0
DM
2530 type => 'string',
2531 },
2532 code => sub {
2533 my ($param) = @_;
2534
2535 my $rpcenv = PVE::RPCEnvironment::get();
2536
a0d1b1a2 2537 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2538
2539 my $node = extract_param($param, 'node');
2540
2541 my $vmid = extract_param($param, 'vmid');
2542
2543 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2544 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2545 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2546
ff1a2432
DM
2547 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2548
5fdbe4f0
DM
2549 my $realcmd = sub {
2550 my $upid = shift;
2551
1e3baf05 2552 PVE::QemuServer::vm_reset($vmid, $skiplock);
5fdbe4f0
DM
2553
2554 return;
2555 };
2556
a0d1b1a2 2557 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2558 }});
2559
2560__PACKAGE__->register_method({
afdb31d5 2561 name => 'vm_shutdown',
5fdbe4f0
DM
2562 path => '{vmid}/status/shutdown',
2563 method => 'POST',
2564 protected => 1,
2565 proxyto => 'node',
d6c747ff
EK
2566 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2567 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
a0d1b1a2
DM
2568 permissions => {
2569 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2570 },
5fdbe4f0 2571 parameters => {
3326ae19 2572 additionalProperties => 0,
5fdbe4f0
DM
2573 properties => {
2574 node => get_standard_option('pve-node'),
ab5904f7
TL
2575 vmid => get_standard_option('pve-vmid',
2576 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2577 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
2578 timeout => {
2579 description => "Wait maximal timeout seconds.",
2580 type => 'integer',
2581 minimum => 0,
2582 optional => 1,
9269013a
DM
2583 },
2584 forceStop => {
2585 description => "Make sure the VM stops.",
2586 type => 'boolean',
2587 optional => 1,
2588 default => 0,
254575e9
DM
2589 },
2590 keepActive => {
94a17e1d 2591 description => "Do not deactivate storage volumes.",
254575e9
DM
2592 type => 'boolean',
2593 optional => 1,
2594 default => 0,
c6bb9502 2595 }
5fdbe4f0
DM
2596 },
2597 },
afdb31d5 2598 returns => {
5fdbe4f0
DM
2599 type => 'string',
2600 },
2601 code => sub {
2602 my ($param) = @_;
2603
2604 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2605 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2606
2607 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2608 my $vmid = extract_param($param, 'vmid');
2609
2610 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2611 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2612 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2613
254575e9 2614 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2615 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2616 if $keepActive && $authuser ne 'root@pam';
254575e9 2617
02d07cf5
DM
2618 my $storecfg = PVE::Storage::config();
2619
89897367
DC
2620 my $shutdown = 1;
2621
2622 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2623 # otherwise, we will infer a shutdown command, but run into the timeout,
2624 # then when the vm is resumed, it will instantly shutdown
2625 #
2626 # checking the qmp status here to get feedback to the gui/cli/api
2627 # and the status query should not take too long
b08c37c3 2628 if (PVE::QemuServer::vm_is_paused($vmid)) {
89897367
DC
2629 if ($param->{forceStop}) {
2630 warn "VM is paused - stop instead of shutdown\n";
2631 $shutdown = 0;
2632 } else {
2633 die "VM is paused - cannot shutdown\n";
2634 }
2635 }
2636
a4262553 2637 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
5fdbe4f0 2638
1805fac3 2639 my $timeout = $param->{timeout} // 60;
ae849692
DM
2640 my $hacmd = sub {
2641 my $upid = shift;
5fdbe4f0 2642
02765844 2643 print "Requesting HA stop for VM $vmid\n";
ae849692 2644
1805fac3 2645 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
ae849692 2646 PVE::Tools::run_command($cmd);
ae849692
DM
2647 return;
2648 };
2649
2650 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2651
2652 } else {
2653
2654 my $realcmd = sub {
2655 my $upid = shift;
2656
2657 syslog('info', "shutdown VM $vmid: $upid\n");
5fdbe4f0 2658
ae849692
DM
2659 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2660 $shutdown, $param->{forceStop}, $keepActive);
ae849692
DM
2661 return;
2662 };
2663
2664 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2665 }
5fdbe4f0
DM
2666 }});
2667
165411f0
DC
2668__PACKAGE__->register_method({
2669 name => 'vm_reboot',
2670 path => '{vmid}/status/reboot',
2671 method => 'POST',
2672 protected => 1,
2673 proxyto => 'node',
2674 description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
2675 permissions => {
2676 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2677 },
2678 parameters => {
2679 additionalProperties => 0,
2680 properties => {
2681 node => get_standard_option('pve-node'),
2682 vmid => get_standard_option('pve-vmid',
2683 { completion => \&PVE::QemuServer::complete_vmid_running }),
2684 timeout => {
2685 description => "Wait maximal timeout seconds for the shutdown.",
2686 type => 'integer',
2687 minimum => 0,
2688 optional => 1,
2689 },
2690 },
2691 },
2692 returns => {
2693 type => 'string',
2694 },
2695 code => sub {
2696 my ($param) = @_;
2697
2698 my $rpcenv = PVE::RPCEnvironment::get();
2699 my $authuser = $rpcenv->get_user();
2700
2701 my $node = extract_param($param, 'node');
2702 my $vmid = extract_param($param, 'vmid');
2703
b08c37c3 2704 die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid);
165411f0
DC
2705
2706 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2707
2708 my $realcmd = sub {
2709 my $upid = shift;
2710
2711 syslog('info', "requesting reboot of VM $vmid: $upid\n");
2712 PVE::QemuServer::vm_reboot($vmid, $param->{timeout});
2713 return;
2714 };
2715
2716 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
2717 }});
2718
5fdbe4f0 2719__PACKAGE__->register_method({
afdb31d5 2720 name => 'vm_suspend',
5fdbe4f0
DM
2721 path => '{vmid}/status/suspend',
2722 method => 'POST',
2723 protected => 1,
2724 proxyto => 'node',
2725 description => "Suspend virtual machine.",
a0d1b1a2 2726 permissions => {
75c24bba
DC
2727 description => "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
2728 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
2729 " on the storage for the vmstate.",
a0d1b1a2
DM
2730 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2731 },
5fdbe4f0 2732 parameters => {
3326ae19 2733 additionalProperties => 0,
5fdbe4f0
DM
2734 properties => {
2735 node => get_standard_option('pve-node'),
ab5904f7
TL
2736 vmid => get_standard_option('pve-vmid',
2737 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2738 skiplock => get_standard_option('skiplock'),
22371fe0
DC
2739 todisk => {
2740 type => 'boolean',
2741 default => 0,
2742 optional => 1,
2743 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
2744 },
48b4cdc2
DC
2745 statestorage => get_standard_option('pve-storage-id', {
2746 description => "The storage for the VM state",
2747 requires => 'todisk',
2748 optional => 1,
2749 completion => \&PVE::Storage::complete_storage_enabled,
2750 }),
5fdbe4f0
DM
2751 },
2752 },
afdb31d5 2753 returns => {
5fdbe4f0
DM
2754 type => 'string',
2755 },
2756 code => sub {
2757 my ($param) = @_;
2758
2759 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2760 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2761
2762 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2763 my $vmid = extract_param($param, 'vmid');
2764
22371fe0
DC
2765 my $todisk = extract_param($param, 'todisk') // 0;
2766
48b4cdc2
DC
2767 my $statestorage = extract_param($param, 'statestorage');
2768
5fdbe4f0 2769 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2770 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2771 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2772
ff1a2432
DM
2773 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2774
22371fe0
DC
2775 die "Cannot suspend HA managed VM to disk\n"
2776 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
2777
75c24bba
DC
2778 # early check for storage permission, for better user feedback
2779 if ($todisk) {
2780 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
2781
2782 if (!$statestorage) {
2783 # get statestorage from config if none is given
2784 my $conf = PVE::QemuConfig->load_config($vmid);
2785 my $storecfg = PVE::Storage::config();
2786 $statestorage = PVE::QemuServer::find_vmstate_storage($conf, $storecfg);
2787 }
2788
2789 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
2790 }
2791
5fdbe4f0
DM
2792 my $realcmd = sub {
2793 my $upid = shift;
2794
2795 syslog('info', "suspend VM $vmid: $upid\n");
2796
48b4cdc2 2797 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
5fdbe4f0
DM
2798
2799 return;
2800 };
2801
a4262553 2802 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
f17fb184 2803 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2804 }});
2805
2806__PACKAGE__->register_method({
afdb31d5 2807 name => 'vm_resume',
5fdbe4f0
DM
2808 path => '{vmid}/status/resume',
2809 method => 'POST',
2810 protected => 1,
2811 proxyto => 'node',
2812 description => "Resume virtual machine.",
a0d1b1a2
DM
2813 permissions => {
2814 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2815 },
5fdbe4f0 2816 parameters => {
3326ae19 2817 additionalProperties => 0,
5fdbe4f0
DM
2818 properties => {
2819 node => get_standard_option('pve-node'),
ab5904f7
TL
2820 vmid => get_standard_option('pve-vmid',
2821 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2822 skiplock => get_standard_option('skiplock'),
289e0b85
AD
2823 nocheck => { type => 'boolean', optional => 1 },
2824
5fdbe4f0
DM
2825 },
2826 },
afdb31d5 2827 returns => {
5fdbe4f0
DM
2828 type => 'string',
2829 },
2830 code => sub {
2831 my ($param) = @_;
2832
2833 my $rpcenv = PVE::RPCEnvironment::get();
2834
a0d1b1a2 2835 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2836
2837 my $node = extract_param($param, 'node');
2838
2839 my $vmid = extract_param($param, 'vmid');
2840
2841 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2842 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2843 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2844
289e0b85 2845 my $nocheck = extract_param($param, 'nocheck');
4fb85adc
FG
2846 raise_param_exc({ nocheck => "Only root may use this option." })
2847 if $nocheck && $authuser ne 'root@pam';
289e0b85 2848
cd9a035b
TL
2849 my $to_disk_suspended;
2850 eval {
2851 PVE::QemuConfig->lock_config($vmid, sub {
2852 my $conf = PVE::QemuConfig->load_config($vmid);
2853 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
2854 });
2855 };
2856
2857 die "VM $vmid not running\n"
2858 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
ff1a2432 2859
5fdbe4f0
DM
2860 my $realcmd = sub {
2861 my $upid = shift;
2862
2863 syslog('info', "resume VM $vmid: $upid\n");
2864
cd9a035b
TL
2865 if (!$to_disk_suspended) {
2866 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2867 } else {
2868 my $storecfg = PVE::Storage::config();
0c498cca 2869 PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
cd9a035b 2870 }
1e3baf05 2871
5fdbe4f0
DM
2872 return;
2873 };
2874
a0d1b1a2 2875 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2876 }});
2877
2878__PACKAGE__->register_method({
afdb31d5 2879 name => 'vm_sendkey',
5fdbe4f0
DM
2880 path => '{vmid}/sendkey',
2881 method => 'PUT',
2882 protected => 1,
2883 proxyto => 'node',
2884 description => "Send key event to virtual machine.",
a0d1b1a2
DM
2885 permissions => {
2886 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2887 },
5fdbe4f0 2888 parameters => {
3326ae19 2889 additionalProperties => 0,
5fdbe4f0
DM
2890 properties => {
2891 node => get_standard_option('pve-node'),
ab5904f7
TL
2892 vmid => get_standard_option('pve-vmid',
2893 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
2894 skiplock => get_standard_option('skiplock'),
2895 key => {
2896 description => "The key (qemu monitor encoding).",
2897 type => 'string'
2898 }
2899 },
2900 },
2901 returns => { type => 'null'},
2902 code => sub {
2903 my ($param) = @_;
2904
2905 my $rpcenv = PVE::RPCEnvironment::get();
2906
a0d1b1a2 2907 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2908
2909 my $node = extract_param($param, 'node');
2910
2911 my $vmid = extract_param($param, 'vmid');
2912
2913 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2914 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2915 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0
DM
2916
2917 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2918
2919 return;
1e3baf05
DM
2920 }});
2921
1ac0d2ee
AD
2922__PACKAGE__->register_method({
2923 name => 'vm_feature',
2924 path => '{vmid}/feature',
2925 method => 'GET',
2926 proxyto => 'node',
75466c4f 2927 protected => 1,
1ac0d2ee
AD
2928 description => "Check if feature for virtual machine is available.",
2929 permissions => {
2930 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2931 },
2932 parameters => {
3326ae19 2933 additionalProperties => 0,
1ac0d2ee
AD
2934 properties => {
2935 node => get_standard_option('pve-node'),
2936 vmid => get_standard_option('pve-vmid'),
2937 feature => {
2938 description => "Feature to check.",
2939 type => 'string',
7758ce86 2940 enum => [ 'snapshot', 'clone', 'copy' ],
1ac0d2ee
AD
2941 },
2942 snapname => get_standard_option('pve-snapshot-name', {
2943 optional => 1,
2944 }),
2945 },
1ac0d2ee
AD
2946 },
2947 returns => {
719893a9
DM
2948 type => "object",
2949 properties => {
2950 hasFeature => { type => 'boolean' },
7043d946 2951 nodes => {
719893a9
DM
2952 type => 'array',
2953 items => { type => 'string' },
2954 }
2955 },
1ac0d2ee
AD
2956 },
2957 code => sub {
2958 my ($param) = @_;
2959
2960 my $node = extract_param($param, 'node');
2961
2962 my $vmid = extract_param($param, 'vmid');
2963
2964 my $snapname = extract_param($param, 'snapname');
2965
2966 my $feature = extract_param($param, 'feature');
2967
2968 my $running = PVE::QemuServer::check_running($vmid);
2969
ffda963f 2970 my $conf = PVE::QemuConfig->load_config($vmid);
1ac0d2ee
AD
2971
2972 if($snapname){
2973 my $snap = $conf->{snapshots}->{$snapname};
2974 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2975 $conf = $snap;
2976 }
2977 my $storecfg = PVE::Storage::config();
2978
719893a9 2979 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
b2c9558d 2980 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
7043d946 2981
719893a9
DM
2982 return {
2983 hasFeature => $hasFeature,
2984 nodes => [ keys %$nodelist ],
7043d946 2985 };
1ac0d2ee
AD
2986 }});
2987
6116f729 2988__PACKAGE__->register_method({
9418baad
DM
2989 name => 'clone_vm',
2990 path => '{vmid}/clone',
6116f729
DM
2991 method => 'POST',
2992 protected => 1,
2993 proxyto => 'node',
37329185 2994 description => "Create a copy of virtual machine/template.",
6116f729 2995 permissions => {
9418baad 2996 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
6116f729
DM
2997 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2998 "'Datastore.AllocateSpace' on any used storage.",
75466c4f
DM
2999 check =>
3000 [ 'and',
9418baad 3001 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
75466c4f 3002 [ 'or',
6116f729
DM
3003 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3004 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
3005 ],
3006 ]
3007 },
3008 parameters => {
3326ae19 3009 additionalProperties => 0,
6116f729 3010 properties => {
6116f729 3011 node => get_standard_option('pve-node'),
335af808 3012 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1ae43f8c
DM
3013 newid => get_standard_option('pve-vmid', {
3014 completion => \&PVE::Cluster::complete_next_vmid,
3015 description => 'VMID for the clone.' }),
a60ab1a6
DM
3016 name => {
3017 optional => 1,
3018 type => 'string', format => 'dns-name',
3019 description => "Set a name for the new VM.",
3020 },
3021 description => {
3022 optional => 1,
3023 type => 'string',
3024 description => "Description for the new VM.",
3025 },
75466c4f 3026 pool => {
6116f729
DM
3027 optional => 1,
3028 type => 'string', format => 'pve-poolid',
3029 description => "Add the new VM to the specified pool.",
3030 },
9076d880 3031 snapname => get_standard_option('pve-snapshot-name', {
9076d880
DM
3032 optional => 1,
3033 }),
81f043eb 3034 storage => get_standard_option('pve-storage-id', {
9418baad 3035 description => "Target storage for full clone.",
81f043eb
AD
3036 optional => 1,
3037 }),
55173c6b 3038 'format' => {
fd13b1d0 3039 description => "Target format for file storage. Only valid for full clone.",
42a19c87
AD
3040 type => 'string',
3041 optional => 1,
55173c6b 3042 enum => [ 'raw', 'qcow2', 'vmdk'],
42a19c87 3043 },
6116f729
DM
3044 full => {
3045 optional => 1,
55173c6b 3046 type => 'boolean',
fd13b1d0 3047 description => "Create a full copy of all disks. This is always done when " .
9418baad 3048 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
6116f729 3049 },
75466c4f 3050 target => get_standard_option('pve-node', {
55173c6b
DM
3051 description => "Target node. Only allowed if the original VM is on shared storage.",
3052 optional => 1,
3053 }),
0aab5a16
SI
3054 bwlimit => {
3055 description => "Override I/O bandwidth limit (in KiB/s).",
3056 optional => 1,
3057 type => 'integer',
3058 minimum => '0',
41756a3b 3059 default => 'clone limit from datacenter or storage config',
0aab5a16 3060 },
55173c6b 3061 },
6116f729
DM
3062 },
3063 returns => {
3064 type => 'string',
3065 },
3066 code => sub {
3067 my ($param) = @_;
3068
3069 my $rpcenv = PVE::RPCEnvironment::get();
a85ff91b 3070 my $authuser = $rpcenv->get_user();
6116f729
DM
3071
3072 my $node = extract_param($param, 'node');
6116f729 3073 my $vmid = extract_param($param, 'vmid');
6116f729 3074 my $newid = extract_param($param, 'newid');
6116f729 3075 my $pool = extract_param($param, 'pool');
6116f729 3076
55173c6b 3077 my $snapname = extract_param($param, 'snapname');
81f043eb 3078 my $storage = extract_param($param, 'storage');
42a19c87 3079 my $format = extract_param($param, 'format');
55173c6b
DM
3080 my $target = extract_param($param, 'target');
3081
3082 my $localnode = PVE::INotify::nodename();
3083
e099bad4 3084 if ($target && ($target eq $localnode || $target eq 'localhost')) {
a85ff91b 3085 undef $target;
a85ff91b 3086 }
55173c6b 3087
4df8fe45 3088 my $running = PVE::QemuServer::check_running($vmid) || 0;
d069275f 3089
4df8fe45
FG
3090 my $load_and_check = sub {
3091 $rpcenv->check_pool_exist($pool) if defined($pool);
3092 PVE::Cluster::check_node_exists($target) if $target;
6116f729 3093
4df8fe45 3094 my $storecfg = PVE::Storage::config();
4a5a2590 3095
4df8fe45
FG
3096 if ($storage) {
3097 # check if storage is enabled on local node
3098 PVE::Storage::storage_check_enabled($storecfg, $storage);
3099 if ($target) {
3100 # check if storage is available on target node
3101 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
3102 # clone only works if target storage is shared
3103 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
3104 die "can't clone to non-shared storage '$storage'\n"
3105 if !$scfg->{shared};
3106 }
3107 }
6116f729 3108
4df8fe45 3109 PVE::Cluster::check_cfs_quorum();
4e4f83fe 3110
ffda963f 3111 my $conf = PVE::QemuConfig->load_config($vmid);
ffda963f 3112 PVE::QemuConfig->check_lock($conf);
6116f729 3113
4e4f83fe 3114 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
4e4f83fe 3115 die "unexpected state change\n" if $verify_running != $running;
6116f729 3116
75466c4f
DM
3117 die "snapshot '$snapname' does not exist\n"
3118 if $snapname && !defined( $conf->{snapshots}->{$snapname});
6116f729 3119
dbecb46f 3120 my $full = $param->{full} // !PVE::QemuConfig->is_template($conf);
fd13b1d0
DM
3121
3122 die "parameter 'storage' not allowed for linked clones\n"
3123 if defined($storage) && !$full;
3124
3125 die "parameter 'format' not allowed for linked clones\n"
3126 if defined($format) && !$full;
3127
75466c4f 3128 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
9076d880 3129
9418baad 3130 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
6116f729 3131
a85ff91b
TL
3132 die "can't clone VM to node '$target' (VM uses local storage)\n"
3133 if $target && !$sharedvm;
75466c4f 3134
ffda963f 3135 my $conffile = PVE::QemuConfig->config_file($newid);
6116f729
DM
3136 die "unable to create VM $newid: config file already exists\n"
3137 if -f $conffile;
3138
9418baad 3139 my $newconf = { lock => 'clone' };
829967a9 3140 my $drives = {};
34456bf0 3141 my $fullclone = {};
829967a9
DM
3142 my $vollist = [];
3143
3144 foreach my $opt (keys %$oldconf) {
3145 my $value = $oldconf->{$opt};
3146
3147 # do not copy snapshot related info
3148 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3149 $opt eq 'vmstate' || $opt eq 'snapstate';
3150
a78ea5df
WL
3151 # no need to copy unused images, because VMID(owner) changes anyways
3152 next if $opt =~ m/^unused\d+$/;
3153
829967a9
DM
3154 # always change MAC! address
3155 if ($opt =~ m/^net(\d+)$/) {
3156 my $net = PVE::QemuServer::parse_net($value);
b5b99790
WB
3157 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
3158 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
829967a9 3159 $newconf->{$opt} = PVE::QemuServer::print_net($net);
74479ee9 3160 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1f1412d1
DM
3161 my $drive = PVE::QemuServer::parse_drive($opt, $value);
3162 die "unable to parse drive options for '$opt'\n" if !$drive;
7fe8b44c 3163 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
829967a9
DM
3164 $newconf->{$opt} = $value; # simply copy configuration
3165 } else {
7fe8b44c 3166 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
6318daca 3167 die "Full clone feature is not supported for drive '$opt'\n"
dba198b0 3168 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
34456bf0 3169 $fullclone->{$opt} = 1;
64ff6fe4
SP
3170 } else {
3171 # not full means clone instead of copy
6318daca 3172 die "Linked clone feature is not supported for drive '$opt'\n"
64ff6fe4 3173 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
dba198b0 3174 }
829967a9 3175 $drives->{$opt} = $drive;
f8c4b2c5 3176 next if PVE::QemuServer::drive_is_cloudinit($drive);
829967a9
DM
3177 push @$vollist, $drive->{file};
3178 }
3179 } else {
3180 # copy everything else
3181 $newconf->{$opt} = $value;
3182 }
3183 }
3184
dbecb46f
FE
3185 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3186 };
3187
3188 my $clonefn = sub {
3189 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
4df8fe45 3190 my $storecfg = PVE::Storage::config();
dbecb46f
FE
3191
3192 # auto generate a new uuid
cd11416f 3193 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
6ee499ff 3194 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
cd11416f 3195 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
a85ff91b 3196 # auto generate a new vmgenid only if the option was set for template
6ee499ff
DC
3197 if ($newconf->{vmgenid}) {
3198 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
3199 }
3200
829967a9
DM
3201 delete $newconf->{template};
3202
3203 if ($param->{name}) {
3204 $newconf->{name} = $param->{name};
3205 } else {
a85ff91b 3206 $newconf->{name} = "Copy-of-VM-" . ($oldconf->{name} // $vmid);
829967a9 3207 }
2dd53043 3208
829967a9
DM
3209 if ($param->{description}) {
3210 $newconf->{description} = $param->{description};
3211 }
3212
6116f729 3213 # create empty/temp config - this fails if VM already exists on other node
a85ff91b 3214 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
9418baad 3215 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
6116f729 3216
dbecb46f 3217 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
68e46b84 3218
dbecb46f
FE
3219 my $newvollist = [];
3220 my $jobs = {};
3221
3222 eval {
3223 local $SIG{INT} =
3224 local $SIG{TERM} =
3225 local $SIG{QUIT} =
3226 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3227
3228 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
3229
3230 my $bwlimit = extract_param($param, 'bwlimit');
3231
3232 my $total_jobs = scalar(keys %{$drives});
3233 my $i = 1;
3234
3235 foreach my $opt (sort keys %$drives) {
3236 my $drive = $drives->{$opt};
3237 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3238 my $completion = $skipcomplete ? 'skip' : 'complete';
3239
3240 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
3241 my $storage_list = [ $src_sid ];
3242 push @$storage_list, $storage if defined($storage);
3243 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
3244
1196086f
FE
3245 my $source_info = {
3246 vmid => $vmid,
3247 running => $running,
3248 drivename => $opt,
3249 drive => $drive,
3250 snapname => $snapname,
3251 };
3252
3253 my $dest_info = {
3254 vmid => $newid,
25166060 3255 drivename => $opt,
1196086f
FE
3256 storage => $storage,
3257 format => $format,
3258 };
3259
7344af7b
FE
3260 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($oldconf)
3261 if $opt eq 'efidisk0';
3262
dbecb46f
FE
3263 my $newdrive = PVE::QemuServer::clone_disk(
3264 $storecfg,
1196086f
FE
3265 $source_info,
3266 $dest_info,
dbecb46f
FE
3267 $fullclone->{$opt},
3268 $newvollist,
3269 $jobs,
3270 $completion,
3271 $oldconf->{agent},
3272 $clonelimit,
dbecb46f
FE
3273 );
3274
3275 $newconf->{$opt} = PVE::QemuServer::print_drive($newdrive);
68e46b84 3276
ffda963f 3277 PVE::QemuConfig->write_config($newid, $newconf);
dbecb46f
FE
3278 $i++;
3279 }
55173c6b 3280
dbecb46f 3281 delete $newconf->{lock};
baca276d 3282
dbecb46f
FE
3283 # do not write pending changes
3284 if (my @changes = keys %{$newconf->{pending}}) {
3285 my $pending = join(',', @changes);
3286 warn "found pending changes for '$pending', discarding for clone\n";
3287 delete $newconf->{pending};
3288 }
d703d4c0 3289
dbecb46f 3290 PVE::QemuConfig->write_config($newid, $newconf);
b83e0181 3291
dbecb46f
FE
3292 if ($target) {
3293 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3294 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
3295 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
c05c90a1 3296
dbecb46f
FE
3297 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
3298 die "Failed to move config to node '$target' - rename failed: $!\n"
3299 if !rename($conffile, $newconffile);
3300 }
c05c90a1 3301
dbecb46f
FE
3302 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
3303 };
3304 if (my $err = $@) {
3305 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
3306 sleep 1; # some storage like rbd need to wait before release volume - really?
990b65ab 3307
dbecb46f
FE
3308 foreach my $volid (@$newvollist) {
3309 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3310 warn $@ if $@;
6116f729
DM
3311 }
3312
dbecb46f 3313 PVE::Firewall::remove_vmfw_conf($newid);
6116f729 3314
dbecb46f
FE
3315 unlink $conffile; # avoid races -> last thing before die
3316
3317 die "clone failed: $err";
3318 }
457010cc 3319
dbecb46f 3320 return;
6116f729
DM
3321 };
3322
45fd77bb
FG
3323 # Aquire exclusive lock lock for $newid
3324 my $lock_target_vm = sub {
ffda963f 3325 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
45fd77bb 3326 };
6116f729 3327
dbecb46f
FE
3328 my $lock_source_vm = sub {
3329 # exclusive lock if VM is running - else shared lock is enough;
3330 if ($running) {
3331 return PVE::QemuConfig->lock_config_full($vmid, 1, $lock_target_vm);
3332 } else {
3333 return PVE::QemuConfig->lock_config_shared($vmid, 1, $lock_target_vm);
3334 }
3335 };
3336
3337 $load_and_check->(); # early checks before forking/locking
3338
3339 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
6116f729
DM
3340 }});
3341
586bfa78 3342__PACKAGE__->register_method({
43bc02a9
DM
3343 name => 'move_vm_disk',
3344 path => '{vmid}/move_disk',
e2cd75fa 3345 method => 'POST',
586bfa78
AD
3346 protected => 1,
3347 proxyto => 'node',
a9453218 3348 description => "Move volume to different storage or to a different VM.",
586bfa78 3349 permissions => {
a9453218
AL
3350 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3351 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3352 "a disk to another VM, you need the permissions on the target VM as well.",
44102492 3353 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
586bfa78
AD
3354 },
3355 parameters => {
3326ae19 3356 additionalProperties => 0,
c07a9e3d 3357 properties => {
586bfa78 3358 node => get_standard_option('pve-node'),
335af808 3359 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
a9453218
AL
3360 'target-vmid' => get_standard_option('pve-vmid', {
3361 completion => \&PVE::QemuServer::complete_vmid,
3362 optional => 1,
3363 }),
586bfa78
AD
3364 disk => {
3365 type => 'string',
3366 description => "The disk you want to move.",
a9453218 3367 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
586bfa78 3368 },
335af808
DM
3369 storage => get_standard_option('pve-storage-id', {
3370 description => "Target storage.",
3371 completion => \&PVE::QemuServer::complete_storage,
a9453218 3372 optional => 1,
335af808 3373 }),
f519ab0b
TL
3374 'format' => {
3375 type => 'string',
3376 description => "Target Format.",
3377 enum => [ 'raw', 'qcow2', 'vmdk' ],
3378 optional => 1,
3379 },
70d45e33
DM
3380 delete => {
3381 type => 'boolean',
f519ab0b
TL
3382 description => "Delete the original disk after successful copy. By default the"
3383 ." original disk is kept as unused disk.",
70d45e33
DM
3384 optional => 1,
3385 default => 0,
3386 },
586bfa78
AD
3387 digest => {
3388 type => 'string',
f519ab0b
TL
3389 description => 'Prevent changes if current configuration file has different SHA1"
3390 ." digest. This can be used to prevent concurrent modifications.',
586bfa78
AD
3391 maxLength => 40,
3392 optional => 1,
3393 },
0aab5a16
SI
3394 bwlimit => {
3395 description => "Override I/O bandwidth limit (in KiB/s).",
3396 optional => 1,
3397 type => 'integer',
3398 minimum => '0',
41756a3b 3399 default => 'move limit from datacenter or storage config',
0aab5a16 3400 },
a9453218
AL
3401 'target-disk' => {
3402 type => 'string',
f519ab0b
TL
3403 description => "The config key the disk will be moved to on the target VM"
3404 ." (for example, ide0 or scsi1). Default is the source disk key.",
a9453218
AL
3405 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
3406 optional => 1,
3407 },
3408 'target-digest' => {
3409 type => 'string',
f519ab0b
TL
3410 description => 'Prevent changes if the current config file of the target VM has a"
3411 ." different SHA1 digest. This can be used to detect concurrent modifications.',
a9453218
AL
3412 maxLength => 40,
3413 optional => 1,
3414 },
586bfa78
AD
3415 },
3416 },
e2cd75fa
DM
3417 returns => {
3418 type => 'string',
3419 description => "the task ID.",
3420 },
586bfa78
AD
3421 code => sub {
3422 my ($param) = @_;
3423
3424 my $rpcenv = PVE::RPCEnvironment::get();
586bfa78
AD
3425 my $authuser = $rpcenv->get_user();
3426
3427 my $node = extract_param($param, 'node');
586bfa78 3428 my $vmid = extract_param($param, 'vmid');
a9453218 3429 my $target_vmid = extract_param($param, 'target-vmid');
586bfa78 3430 my $digest = extract_param($param, 'digest');
a9453218 3431 my $target_digest = extract_param($param, 'target-digest');
586bfa78 3432 my $disk = extract_param($param, 'disk');
a9453218 3433 my $target_disk = extract_param($param, 'target-disk') // $disk;
586bfa78 3434 my $storeid = extract_param($param, 'storage');
586bfa78
AD
3435 my $format = extract_param($param, 'format');
3436
586bfa78
AD
3437 my $storecfg = PVE::Storage::config();
3438
bdf6ba1e 3439 my $load_and_check_move = sub {
ffda963f 3440 my $conf = PVE::QemuConfig->load_config($vmid);
dcce9b46
FG
3441 PVE::QemuConfig->check_lock($conf);
3442
44102492 3443 PVE::Tools::assert_if_modified($digest, $conf->{digest});
586bfa78
AD
3444
3445 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3446
3447 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3448
a85ff91b 3449 die "disk '$disk' has no associated volume\n" if !$drive->{file};
931432bd 3450 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 3451
a85ff91b 3452 my $old_volid = $drive->{file};
e2cd75fa 3453 my $oldfmt;
70d45e33 3454 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
3455 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3456 $oldfmt = $1;
3457 }
3458
bdf6ba1e
FE
3459 die "you can't move to the same storage with same format\n"
3460 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 3461
9dbf9b54 3462 # this only checks snapshots because $disk is passed!
70c0ad66
AL
3463 my $snapshotted = PVE::QemuServer::Drive::is_volume_in_use(
3464 $storecfg,
3465 $conf,
3466 $disk,
3467 $old_volid
3468 );
9dbf9b54
FG
3469 die "you can't move a disk with snapshots and delete the source\n"
3470 if $snapshotted && $param->{delete};
3471
bdf6ba1e
FE
3472 return ($conf, $drive, $oldstoreid, $snapshotted);
3473 };
3474
3475 my $move_updatefn = sub {
3476 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3477 my $old_volid = $drive->{file};
3478
70c0ad66
AL
3479 PVE::Cluster::log_msg(
3480 'info',
3481 $authuser,
3482 "move disk VM $vmid: move --disk $disk --storage $storeid"
3483 );
586bfa78
AD
3484
3485 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
3486
3487 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
3488
bdf6ba1e 3489 my $newvollist = [];
586bfa78 3490
bdf6ba1e
FE
3491 eval {
3492 local $SIG{INT} =
3493 local $SIG{TERM} =
3494 local $SIG{QUIT} =
3495 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
0aab5a16 3496
bdf6ba1e
FE
3497 warn "moving disk with snapshots, snapshots will not be moved!\n"
3498 if $snapshotted;
e2cd75fa 3499
bdf6ba1e
FE
3500 my $bwlimit = extract_param($param, 'bwlimit');
3501 my $movelimit = PVE::Storage::get_bandwidth_limit(
3502 'move',
3503 [$oldstoreid, $storeid],
3504 $bwlimit
3505 );
7043d946 3506
1196086f
FE
3507 my $source_info = {
3508 vmid => $vmid,
3509 running => $running,
3510 drivename => $disk,
3511 drive => $drive,
3512 snapname => undef,
3513 };
3514
3515 my $dest_info = {
3516 vmid => $vmid,
25166060 3517 drivename => $disk,
1196086f
FE
3518 storage => $storeid,
3519 format => $format,
3520 };
3521
7344af7b
FE
3522 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf)
3523 if $disk eq 'efidisk0';
3524
bdf6ba1e
FE
3525 my $newdrive = PVE::QemuServer::clone_disk(
3526 $storecfg,
1196086f
FE
3527 $source_info,
3528 $dest_info,
bdf6ba1e
FE
3529 1,
3530 $newvollist,
3531 undef,
3532 undef,
3533 undef,
3534 $movelimit,
bdf6ba1e
FE
3535 );
3536 $conf->{$disk} = PVE::QemuServer::print_drive($newdrive);
fbd7dcce 3537
bdf6ba1e 3538 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
73272365 3539
bdf6ba1e
FE
3540 # convert moved disk to base if part of template
3541 PVE::QemuServer::template_create($vmid, $conf, $disk)
3542 if PVE::QemuConfig->is_template($conf);
ca662131 3543
bdf6ba1e 3544 PVE::QemuConfig->write_config($vmid, $conf);
70d45e33 3545
bdf6ba1e
FE
3546 my $do_trim = PVE::QemuServer::get_qga_key($conf, 'fstrim_cloned_disks');
3547 if ($running && $do_trim && PVE::QemuServer::qga_check_running($vmid)) {
3548 eval { mon_cmd($vmid, "guest-fstrim") };
70d45e33 3549 }
bdf6ba1e
FE
3550
3551 eval {
3552 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3553 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
3554 if !$running;
3555 };
3556 warn $@ if $@;
586bfa78 3557 };
bdf6ba1e
FE
3558 if (my $err = $@) {
3559 foreach my $volid (@$newvollist) {
3560 eval { PVE::Storage::vdisk_free($storecfg, $volid) };
3561 warn $@ if $@;
3562 }
3563 die "storage migration failed: $err";
3564 }
586bfa78 3565
bdf6ba1e
FE
3566 if ($param->{delete}) {
3567 eval {
3568 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
3569 PVE::Storage::vdisk_free($storecfg, $old_volid);
3570 };
3571 warn $@ if $@;
3572 }
586bfa78 3573 };
e2cd75fa 3574
a9453218
AL
3575 my $load_and_check_reassign_configs = sub {
3576 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
3577
3578 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
dbc817ba 3579 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
a9453218 3580
dbc817ba
FG
3581 my $source_node = $vmlist->{$vmid}->{node};
3582 my $target_node = $vmlist->{$target_vmid}->{node};
3583
3584 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
3585 if $source_node ne $target_node;
a9453218
AL
3586
3587 my $source_conf = PVE::QemuConfig->load_config($vmid);
3588 PVE::QemuConfig->check_lock($source_conf);
3589 my $target_conf = PVE::QemuConfig->load_config($target_vmid);
3590 PVE::QemuConfig->check_lock($target_conf);
3591
3592 die "Can't move disks from or to template VMs\n"
3593 if ($source_conf->{template} || $target_conf->{template});
3594
3595 if ($digest) {
3596 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
3597 die "VM ${vmid}: $@" if $@;
3598 }
3599
3600 if ($target_digest) {
3601 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
3602 die "VM ${target_vmid}: $@" if $@;
3603 }
3604
3605 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
3606
3607 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
dbc817ba 3608 if $target_conf->{$target_disk};
a9453218
AL
3609
3610 my $drive = PVE::QemuServer::parse_drive(
3611 $disk,
3612 $source_conf->{$disk},
3613 );
dbc817ba
FG
3614 die "failed to parse source disk - $@\n" if !$drive;
3615
3616 my $source_volid = $drive->{file};
a9453218
AL
3617
3618 die "disk '${disk}' has no associated volume\n" if !$source_volid;
3619 die "CD drive contents can't be moved to another VM\n"
3620 if PVE::QemuServer::drive_is_cdrom($drive, 1);
dbc817ba
FG
3621
3622 my $storeid = PVE::Storage::parse_volume_id($source_volid, 1);
3623 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
3624
a9453218
AL
3625 die "Can't move disk used by a snapshot to another VM\n"
3626 if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $source_conf, $disk, $source_volid);
3627 die "Storage does not support moving of this disk to another VM\n"
3628 if (!PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid));
dbc817ba 3629 die "Cannot move disk to another VM while the source VM is running - detach first\n"
a9453218
AL
3630 if PVE::QemuServer::check_running($vmid) && $disk !~ m/^unused\d+$/;
3631
dbc817ba 3632 # now re-parse using target disk slot format
f4e4c779
FG
3633 if ($target_disk =~ /^unused\d+$/) {
3634 $drive = PVE::QemuServer::parse_drive(
3635 $target_disk,
3636 $source_volid,
3637 );
3638 } else {
3639 $drive = PVE::QemuServer::parse_drive(
3640 $target_disk,
3641 $source_conf->{$disk},
3642 );
3643 }
dbc817ba 3644 die "failed to parse source disk for target disk format - $@\n" if !$drive;
a9453218
AL
3645
3646 my $repl_conf = PVE::ReplicationConfig->new();
dbc817ba
FG
3647 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
3648 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
3649 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
3650 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
a9453218
AL
3651 }
3652
dbc817ba 3653 return ($source_conf, $target_conf, $drive);
a9453218
AL
3654 };
3655
3656 my $logfunc = sub {
3657 my ($msg) = @_;
3658 print STDERR "$msg\n";
3659 };
3660
3661 my $disk_reassignfn = sub {
3662 return PVE::QemuConfig->lock_config($vmid, sub {
3663 return PVE::QemuConfig->lock_config($target_vmid, sub {
dbc817ba 3664 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
a9453218 3665
dbc817ba 3666 my $source_volid = $drive->{file};
a9453218
AL
3667
3668 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
3669 my ($storeid, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
3670
3671 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
3672
3673 my $new_volid = PVE::Storage::rename_volume(
3674 $storecfg,
3675 $source_volid,
3676 $target_vmid,
3677 );
3678
dbc817ba 3679 $drive->{file} = $new_volid;
a9453218
AL
3680
3681 delete $source_conf->{$disk};
3682 print "removing disk '${disk}' from VM '${vmid}' config\n";
3683 PVE::QemuConfig->write_config($vmid, $source_conf);
3684
dbc817ba 3685 my $drive_string = PVE::QemuServer::print_drive($drive);
bf67da2b
AL
3686
3687 if ($target_disk =~ /^unused\d+$/) {
3688 $target_conf->{$target_disk} = $drive_string;
3689 PVE::QemuConfig->write_config($target_vmid, $target_conf);
3690 } else {
3691 &$update_vm_api(
3692 {
3693 node => $node,
3694 vmid => $target_vmid,
3695 digest => $target_digest,
3696 $target_disk => $drive_string,
3697 },
3698 1,
3699 );
3700 }
a9453218
AL
3701
3702 # remove possible replication snapshots
3703 if (PVE::Storage::volume_has_feature(
3704 $storecfg,
3705 'replicate',
3706 $source_volid),
3707 ) {
3708 eval {
3709 PVE::Replication::prepare(
3710 $storecfg,
3711 [$new_volid],
3712 undef,
3713 1,
3714 undef,
3715 $logfunc,
3716 )
3717 };
3718 if (my $err = $@) {
3719 print "Failed to remove replication snapshots on moved disk " .
3720 "'$target_disk'. Manual cleanup could be necessary.\n";
3721 }
3722 }
3723 });
3724 });
3725 };
3726
dbc817ba
FG
3727 if ($target_vmid && $storeid) {
3728 my $msg = "either set 'storage' or 'target-vmid', but not both";
3729 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
3730 } elsif ($target_vmid) {
a9453218
AL
3731 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
3732 if $authuser ne 'root@pam';
3733
dbc817ba 3734 raise_param_exc({ 'target-vmid' => "must be different than source VMID to reassign disk" })
a9453218
AL
3735 if $vmid eq $target_vmid;
3736
44102492 3737 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
a6273aa8
FG
3738 my $storage = PVE::Storage::parse_volume_id($drive->{file});
3739 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
44102492 3740
a9453218
AL
3741 return $rpcenv->fork_worker(
3742 'qmmove',
3743 "${vmid}-${disk}>${target_vmid}-${target_disk}",
3744 $authuser,
3745 $disk_reassignfn
3746 );
3747 } elsif ($storeid) {
44102492
FG
3748 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3749
a9453218
AL
3750 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
3751 if $disk =~ m/^unused\d+$/;
bdf6ba1e
FE
3752
3753 $load_and_check_move->(); # early checks before forking/locking
3754
3755 my $realcmd = sub {
3756 PVE::QemuConfig->lock_config($vmid, $move_updatefn);
3757 };
3758
3759 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
a9453218 3760 } else {
dbc817ba
FG
3761 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
3762 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
a9453218 3763 }
586bfa78
AD
3764 }});
3765
71fc647f
TM
3766my $check_vm_disks_local = sub {
3767 my ($storecfg, $vmconf, $vmid) = @_;
3768
3769 my $local_disks = {};
3770
3771 # add some more information to the disks e.g. cdrom
3772 PVE::QemuServer::foreach_volid($vmconf, sub {
3773 my ($volid, $attr) = @_;
3774
3775 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
3776 if ($storeid) {
3777 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
3778 return if $scfg->{shared};
3779 }
3780 # The shared attr here is just a special case where the vdisk
3781 # is marked as shared manually
3782 return if $attr->{shared};
3783 return if $attr->{cdrom} and $volid eq "none";
3784
3785 if (exists $local_disks->{$volid}) {
3786 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
3787 } else {
3788 $local_disks->{$volid} = $attr;
3789 # ensure volid is present in case it's needed
3790 $local_disks->{$volid}->{volid} = $volid;
3791 }
3792 });
3793
3794 return $local_disks;
3795};
3796
3797__PACKAGE__->register_method({
3798 name => 'migrate_vm_precondition',
3799 path => '{vmid}/migrate',
3800 method => 'GET',
3801 protected => 1,
3802 proxyto => 'node',
3803 description => "Get preconditions for migration.",
3804 permissions => {
3805 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3806 },
3807 parameters => {
3808 additionalProperties => 0,
3809 properties => {
3810 node => get_standard_option('pve-node'),
3811 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3812 target => get_standard_option('pve-node', {
3813 description => "Target node.",
3814 completion => \&PVE::Cluster::complete_migration_target,
3815 optional => 1,
3816 }),
3817 },
3818 },
3819 returns => {
3820 type => "object",
3821 properties => {
3822 running => { type => 'boolean' },
3823 allowed_nodes => {
3824 type => 'array',
3825 optional => 1,
f25852c2
TM
3826 description => "List nodes allowed for offline migration, only passed if VM is offline"
3827 },
3828 not_allowed_nodes => {
3829 type => 'object',
3830 optional => 1,
3831 description => "List not allowed nodes with additional informations, only passed if VM is offline"
71fc647f
TM
3832 },
3833 local_disks => {
3834 type => 'array',
3835 description => "List local disks including CD-Rom, unsused and not referenced disks"
3836 },
3837 local_resources => {
3838 type => 'array',
3839 description => "List local resources e.g. pci, usb"
3840 }
3841 },
3842 },
3843 code => sub {
3844 my ($param) = @_;
3845
3846 my $rpcenv = PVE::RPCEnvironment::get();
3847
3848 my $authuser = $rpcenv->get_user();
3849
3850 PVE::Cluster::check_cfs_quorum();
3851
3852 my $res = {};
3853
3854 my $vmid = extract_param($param, 'vmid');
3855 my $target = extract_param($param, 'target');
3856 my $localnode = PVE::INotify::nodename();
3857
3858
3859 # test if VM exists
3860 my $vmconf = PVE::QemuConfig->load_config($vmid);
3861 my $storecfg = PVE::Storage::config();
3862
3863
3864 # try to detect errors early
3865 PVE::QemuConfig->check_lock($vmconf);
3866
3867 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
3868
3869 # if vm is not running, return target nodes where local storage is available
3870 # for offline migration
3871 if (!$res->{running}) {
f25852c2
TM
3872 $res->{allowed_nodes} = [];
3873 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
32075a2c 3874 delete $checked_nodes->{$localnode};
f25852c2 3875
f25852c2 3876 foreach my $node (keys %$checked_nodes) {
32075a2c 3877 if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
f25852c2 3878 push @{$res->{allowed_nodes}}, $node;
f25852c2 3879 }
71fc647f 3880
f25852c2
TM
3881 }
3882 $res->{not_allowed_nodes} = $checked_nodes;
71fc647f
TM
3883 }
3884
3885
3886 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
3887 $res->{local_disks} = [ values %$local_disks ];;
3888
3889 my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1);
3890
3891 $res->{local_resources} = $local_resources;
3892
3893 return $res;
3894
3895
3896 }});
3897
3ea94c60 3898__PACKAGE__->register_method({
afdb31d5 3899 name => 'migrate_vm',
3ea94c60
DM
3900 path => '{vmid}/migrate',
3901 method => 'POST',
3902 protected => 1,
3903 proxyto => 'node',
3904 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
3905 permissions => {
3906 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3907 },
3ea94c60 3908 parameters => {
3326ae19 3909 additionalProperties => 0,
3ea94c60
DM
3910 properties => {
3911 node => get_standard_option('pve-node'),
335af808 3912 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
c2ed338e 3913 target => get_standard_option('pve-node', {
335af808
DM
3914 description => "Target node.",
3915 completion => \&PVE::Cluster::complete_migration_target,
3916 }),
3ea94c60
DM
3917 online => {
3918 type => 'boolean',
13739386 3919 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
3ea94c60
DM
3920 optional => 1,
3921 },
3922 force => {
3923 type => 'boolean',
3924 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
3925 optional => 1,
3926 },
2de2d6f7
TL
3927 migration_type => {
3928 type => 'string',
3929 enum => ['secure', 'insecure'],
c07a9e3d 3930 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2de2d6f7
TL
3931 optional => 1,
3932 },
3933 migration_network => {
c07a9e3d 3934 type => 'string', format => 'CIDR',
2de2d6f7
TL
3935 description => "CIDR of the (sub) network that is used for migration.",
3936 optional => 1,
3937 },
56af7146
AD
3938 "with-local-disks" => {
3939 type => 'boolean',
3940 description => "Enable live storage migration for local disk",
b74cad8a 3941 optional => 1,
56af7146 3942 },
bf8fc5a3 3943 targetstorage => get_standard_option('pve-targetstorage', {
255e9c54 3944 completion => \&PVE::QemuServer::complete_migration_storage,
56af7146 3945 }),
0aab5a16
SI
3946 bwlimit => {
3947 description => "Override I/O bandwidth limit (in KiB/s).",
3948 optional => 1,
3949 type => 'integer',
3950 minimum => '0',
41756a3b 3951 default => 'migrate limit from datacenter or storage config',
0aab5a16 3952 },
3ea94c60
DM
3953 },
3954 },
afdb31d5 3955 returns => {
3ea94c60
DM
3956 type => 'string',
3957 description => "the task ID.",
3958 },
3959 code => sub {
3960 my ($param) = @_;
3961
3962 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3963 my $authuser = $rpcenv->get_user();
3ea94c60
DM
3964
3965 my $target = extract_param($param, 'target');
3966
3967 my $localnode = PVE::INotify::nodename();
3968 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
3969
3970 PVE::Cluster::check_cfs_quorum();
3971
3972 PVE::Cluster::check_node_exists($target);
3973
3974 my $targetip = PVE::Cluster::remote_node_ip($target);
3975
3976 my $vmid = extract_param($param, 'vmid');
3977
afdb31d5 3978 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 3979 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 3980
2de2d6f7
TL
3981 raise_param_exc({ migration_type => "Only root may use this option." })
3982 if $param->{migration_type} && $authuser ne 'root@pam';
3983
3984 # allow root only until better network permissions are available
3985 raise_param_exc({ migration_network => "Only root may use this option." })
3986 if $param->{migration_network} && $authuser ne 'root@pam';
3987
3ea94c60 3988 # test if VM exists
ffda963f 3989 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
3990
3991 # try to detect errors early
a5ed42d3 3992
ffda963f 3993 PVE::QemuConfig->check_lock($conf);
a5ed42d3 3994
3ea94c60 3995 if (PVE::QemuServer::check_running($vmid)) {
fda72913 3996 die "can't migrate running VM without --online\n" if !$param->{online};
aa491a6e
FE
3997
3998 my $repl_conf = PVE::ReplicationConfig->new();
3999 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4000 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
68980d66
FE
4001 if (!$param->{force} && $is_replicated && !$is_replicated_to_target) {
4002 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4003 "target. Use 'force' to override.\n";
aa491a6e 4004 }
13739386 4005 } else {
c3ddb94d 4006 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
13739386 4007 $param->{online} = 0;
3ea94c60
DM
4008 }
4009
47152e2e 4010 my $storecfg = PVE::Storage::config();
bf8fc5a3
FG
4011 if (my $targetstorage = $param->{targetstorage}) {
4012 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 4013 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
4014 if $@;
4015
aea447bb
FG
4016 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4017 if !defined($storagemap->{identity});
4018
bd61033e 4019 foreach my $target_sid (values %{$storagemap->{entries}}) {
9fb295d0 4020 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
bf8fc5a3
FG
4021 }
4022
9fb295d0 4023 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
bf8fc5a3
FG
4024 if $storagemap->{default};
4025
4026 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target)
4027 if $storagemap->{identity};
4028
4029 $param->{storagemap} = $storagemap;
d80ad67f
AD
4030 } else {
4031 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
4032 }
47152e2e 4033
2003f0f8 4034 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 4035
88fc87b4
DM
4036 my $hacmd = sub {
4037 my $upid = shift;
3ea94c60 4038
02765844 4039 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4 4040
a4262553 4041 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
88fc87b4 4042 PVE::Tools::run_command($cmd);
88fc87b4
DM
4043 return;
4044 };
4045
4046 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4047
4048 } else {
4049
f53c6ad8 4050 my $realcmd = sub {
f53c6ad8
DM
4051 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
4052 };
88fc87b4 4053
f53c6ad8
DM
4054 my $worker = sub {
4055 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
4056 };
4057
f53c6ad8 4058 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 4059 }
3ea94c60 4060
3ea94c60 4061 }});
1e3baf05 4062
91c94f0a 4063__PACKAGE__->register_method({
afdb31d5
DM
4064 name => 'monitor',
4065 path => '{vmid}/monitor',
91c94f0a
DM
4066 method => 'POST',
4067 protected => 1,
4068 proxyto => 'node',
4069 description => "Execute Qemu monitor commands.",
a0d1b1a2 4070 permissions => {
a8f2f427 4071 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 4072 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 4073 },
91c94f0a 4074 parameters => {
3326ae19 4075 additionalProperties => 0,
91c94f0a
DM
4076 properties => {
4077 node => get_standard_option('pve-node'),
4078 vmid => get_standard_option('pve-vmid'),
4079 command => {
4080 type => 'string',
4081 description => "The monitor command.",
4082 }
4083 },
4084 },
4085 returns => { type => 'string'},
4086 code => sub {
4087 my ($param) = @_;
4088
a8f2f427
FG
4089 my $rpcenv = PVE::RPCEnvironment::get();
4090 my $authuser = $rpcenv->get_user();
4091
4092 my $is_ro = sub {
4093 my $command = shift;
4094 return $command =~ m/^\s*info(\s+|$)/
4095 || $command =~ m/^\s*help\s*$/;
4096 };
4097
4098 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4099 if !&$is_ro($param->{command});
4100
91c94f0a
DM
4101 my $vmid = $param->{vmid};
4102
ffda963f 4103 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
4104
4105 my $res = '';
4106 eval {
0a13e08e 4107 $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, $param->{command});
91c94f0a
DM
4108 };
4109 $res = "ERROR: $@" if $@;
4110
4111 return $res;
4112 }});
4113
0d02881c
AD
4114__PACKAGE__->register_method({
4115 name => 'resize_vm',
614e3941 4116 path => '{vmid}/resize',
0d02881c
AD
4117 method => 'PUT',
4118 protected => 1,
4119 proxyto => 'node',
2f48a4f5 4120 description => "Extend volume size.",
0d02881c 4121 permissions => {
3b2773f6 4122 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
4123 },
4124 parameters => {
3326ae19
TL
4125 additionalProperties => 0,
4126 properties => {
2f48a4f5 4127 node => get_standard_option('pve-node'),
335af808 4128 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
4129 skiplock => get_standard_option('skiplock'),
4130 disk => {
4131 type => 'string',
4132 description => "The disk you want to resize.",
e0fd2b2f 4133 enum => [PVE::QemuServer::Drive::valid_drive_names()],
2f48a4f5
DM
4134 },
4135 size => {
4136 type => 'string',
f91b2e45 4137 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 4138 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.",
2f48a4f5
DM
4139 },
4140 digest => {
4141 type => 'string',
4142 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4143 maxLength => 40,
4144 optional => 1,
4145 },
4146 },
0d02881c
AD
4147 },
4148 returns => { type => 'null'},
4149 code => sub {
4150 my ($param) = @_;
4151
4152 my $rpcenv = PVE::RPCEnvironment::get();
4153
4154 my $authuser = $rpcenv->get_user();
4155
4156 my $node = extract_param($param, 'node');
4157
4158 my $vmid = extract_param($param, 'vmid');
4159
4160 my $digest = extract_param($param, 'digest');
4161
2f48a4f5 4162 my $disk = extract_param($param, 'disk');
75466c4f 4163
2f48a4f5 4164 my $sizestr = extract_param($param, 'size');
0d02881c 4165
f91b2e45 4166 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
4167 raise_param_exc({ skiplock => "Only root may use this option." })
4168 if $skiplock && $authuser ne 'root@pam';
4169
0d02881c
AD
4170 my $storecfg = PVE::Storage::config();
4171
0d02881c
AD
4172 my $updatefn = sub {
4173
ffda963f 4174 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
4175
4176 die "checksum missmatch (file change by other user?)\n"
4177 if $digest && $digest ne $conf->{digest};
ffda963f 4178 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 4179
f91b2e45
DM
4180 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4181
4182 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
4183
d662790a
WL
4184 my (undef, undef, undef, undef, undef, undef, $format) =
4185 PVE::Storage::parse_volname($storecfg, $drive->{file});
4186
c2ed338e 4187 die "can't resize volume: $disk if snapshot exists\n"
d662790a
WL
4188 if %{$conf->{snapshots}} && $format eq 'qcow2';
4189
f91b2e45
DM
4190 my $volid = $drive->{file};
4191
4192 die "disk '$disk' has no associated volume\n" if !$volid;
4193
4194 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
4195
4196 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
4197
4198 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4199
b572a606 4200 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
4201 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
4202
ed94b2ad 4203 die "Could not determine current size of volume '$volid'\n" if !defined($size);
f8b829aa 4204
f91b2e45
DM
4205 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4206 my ($ext, $newsize, $unit) = ($1, $2, $4);
4207 if ($unit) {
4208 if ($unit eq 'K') {
4209 $newsize = $newsize * 1024;
4210 } elsif ($unit eq 'M') {
4211 $newsize = $newsize * 1024 * 1024;
4212 } elsif ($unit eq 'G') {
4213 $newsize = $newsize * 1024 * 1024 * 1024;
4214 } elsif ($unit eq 'T') {
4215 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4216 }
4217 }
4218 $newsize += $size if $ext;
4219 $newsize = int($newsize);
4220
9a478b17 4221 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
4222
4223 return if $size == $newsize;
4224
2f48a4f5 4225 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 4226
f91b2e45 4227 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 4228
e29e5be6 4229 $drive->{size} = $newsize;
71c58bb7 4230 $conf->{$disk} = PVE::QemuServer::print_drive($drive);
f91b2e45 4231
ffda963f 4232 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 4233 };
0d02881c 4234
ffda963f 4235 PVE::QemuConfig->lock_config($vmid, $updatefn);
d1c1af4b 4236 return;
0d02881c
AD
4237 }});
4238
9dbd1ee4 4239__PACKAGE__->register_method({
7e7d7b61 4240 name => 'snapshot_list',
9dbd1ee4 4241 path => '{vmid}/snapshot',
7e7d7b61
DM
4242 method => 'GET',
4243 description => "List all snapshots.",
4244 permissions => {
4245 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4246 },
4247 proxyto => 'node',
4248 protected => 1, # qemu pid files are only readable by root
4249 parameters => {
3326ae19 4250 additionalProperties => 0,
7e7d7b61 4251 properties => {
e261de40 4252 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
4253 node => get_standard_option('pve-node'),
4254 },
4255 },
4256 returns => {
4257 type => 'array',
4258 items => {
4259 type => "object",
ce9b0a38
DM
4260 properties => {
4261 name => {
4262 description => "Snapshot identifier. Value 'current' identifies the current VM.",
4263 type => 'string',
4264 },
4265 vmstate => {
4266 description => "Snapshot includes RAM.",
4267 type => 'boolean',
4268 optional => 1,
4269 },
4270 description => {
4271 description => "Snapshot description.",
4272 type => 'string',
4273 },
4274 snaptime => {
4275 description => "Snapshot creation time",
4276 type => 'integer',
4277 renderer => 'timestamp',
4278 optional => 1,
4279 },
4280 parent => {
4281 description => "Parent snapshot identifier.",
4282 type => 'string',
4283 optional => 1,
4284 },
4285 },
7e7d7b61
DM
4286 },
4287 links => [ { rel => 'child', href => "{name}" } ],
4288 },
4289 code => sub {
4290 my ($param) = @_;
4291
6aa4651b
DM
4292 my $vmid = $param->{vmid};
4293
ffda963f 4294 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
4295 my $snaphash = $conf->{snapshots} || {};
4296
4297 my $res = [];
4298
4299 foreach my $name (keys %$snaphash) {
0ea6bc69 4300 my $d = $snaphash->{$name};
75466c4f
DM
4301 my $item = {
4302 name => $name,
4303 snaptime => $d->{snaptime} || 0,
6aa4651b 4304 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
4305 description => $d->{description} || '',
4306 };
0ea6bc69 4307 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 4308 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
4309 push @$res, $item;
4310 }
4311
6aa4651b 4312 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
4313 my $current = {
4314 name => 'current',
4315 digest => $conf->{digest},
4316 running => $running,
4317 description => "You are here!",
4318 };
d1914468
DM
4319 $current->{parent} = $conf->{parent} if $conf->{parent};
4320
4321 push @$res, $current;
7e7d7b61
DM
4322
4323 return $res;
4324 }});
4325
4326__PACKAGE__->register_method({
4327 name => 'snapshot',
4328 path => '{vmid}/snapshot',
4329 method => 'POST',
9dbd1ee4
AD
4330 protected => 1,
4331 proxyto => 'node',
4332 description => "Snapshot a VM.",
4333 permissions => {
f1baf1df 4334 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
4335 },
4336 parameters => {
4337 additionalProperties => 0,
4338 properties => {
4339 node => get_standard_option('pve-node'),
335af808 4340 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4341 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
4342 vmstate => {
4343 optional => 1,
4344 type => 'boolean',
4345 description => "Save the vmstate",
4346 },
782f4f75
DM
4347 description => {
4348 optional => 1,
4349 type => 'string',
4350 description => "A textual description or comment.",
4351 },
9dbd1ee4
AD
4352 },
4353 },
7e7d7b61
DM
4354 returns => {
4355 type => 'string',
4356 description => "the task ID.",
4357 },
9dbd1ee4
AD
4358 code => sub {
4359 my ($param) = @_;
4360
4361 my $rpcenv = PVE::RPCEnvironment::get();
4362
4363 my $authuser = $rpcenv->get_user();
4364
4365 my $node = extract_param($param, 'node');
4366
4367 my $vmid = extract_param($param, 'vmid');
4368
9dbd1ee4
AD
4369 my $snapname = extract_param($param, 'snapname');
4370
d1914468
DM
4371 die "unable to use snapshot name 'current' (reserved name)\n"
4372 if $snapname eq 'current';
4373
a85c6be1
FG
4374 die "unable to use snapshot name 'pending' (reserved name)\n"
4375 if lc($snapname) eq 'pending';
4376
7e7d7b61 4377 my $realcmd = sub {
22c377f0 4378 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
c2ed338e 4379 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 4380 $param->{description});
7e7d7b61
DM
4381 };
4382
4383 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
4384 }});
4385
154ccdcd
DM
4386__PACKAGE__->register_method({
4387 name => 'snapshot_cmd_idx',
4388 path => '{vmid}/snapshot/{snapname}',
4389 description => '',
4390 method => 'GET',
4391 permissions => {
4392 user => 'all',
4393 },
4394 parameters => {
3326ae19 4395 additionalProperties => 0,
154ccdcd
DM
4396 properties => {
4397 vmid => get_standard_option('pve-vmid'),
4398 node => get_standard_option('pve-node'),
8abd398b 4399 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
4400 },
4401 },
4402 returns => {
4403 type => 'array',
4404 items => {
4405 type => "object",
4406 properties => {},
4407 },
4408 links => [ { rel => 'child', href => "{cmd}" } ],
4409 },
4410 code => sub {
4411 my ($param) = @_;
4412
4413 my $res = [];
4414
4415 push @$res, { cmd => 'rollback' };
d788cea6 4416 push @$res, { cmd => 'config' };
154ccdcd
DM
4417
4418 return $res;
4419 }});
4420
d788cea6
DM
4421__PACKAGE__->register_method({
4422 name => 'update_snapshot_config',
4423 path => '{vmid}/snapshot/{snapname}/config',
4424 method => 'PUT',
4425 protected => 1,
4426 proxyto => 'node',
4427 description => "Update snapshot metadata.",
4428 permissions => {
4429 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4430 },
4431 parameters => {
4432 additionalProperties => 0,
4433 properties => {
4434 node => get_standard_option('pve-node'),
4435 vmid => get_standard_option('pve-vmid'),
4436 snapname => get_standard_option('pve-snapshot-name'),
4437 description => {
4438 optional => 1,
4439 type => 'string',
4440 description => "A textual description or comment.",
4441 },
4442 },
4443 },
4444 returns => { type => 'null' },
4445 code => sub {
4446 my ($param) = @_;
4447
4448 my $rpcenv = PVE::RPCEnvironment::get();
4449
4450 my $authuser = $rpcenv->get_user();
4451
4452 my $vmid = extract_param($param, 'vmid');
4453
4454 my $snapname = extract_param($param, 'snapname');
4455
d1c1af4b 4456 return if !defined($param->{description});
d788cea6
DM
4457
4458 my $updatefn = sub {
4459
ffda963f 4460 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 4461
ffda963f 4462 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
4463
4464 my $snap = $conf->{snapshots}->{$snapname};
4465
75466c4f
DM
4466 die "snapshot '$snapname' does not exist\n" if !defined($snap);
4467
d788cea6
DM
4468 $snap->{description} = $param->{description} if defined($param->{description});
4469
ffda963f 4470 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
4471 };
4472
ffda963f 4473 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6 4474
d1c1af4b 4475 return;
d788cea6
DM
4476 }});
4477
4478__PACKAGE__->register_method({
4479 name => 'get_snapshot_config',
4480 path => '{vmid}/snapshot/{snapname}/config',
4481 method => 'GET',
4482 proxyto => 'node',
4483 description => "Get snapshot configuration",
4484 permissions => {
65204e92 4485 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any => 1],
d788cea6
DM
4486 },
4487 parameters => {
4488 additionalProperties => 0,
4489 properties => {
4490 node => get_standard_option('pve-node'),
4491 vmid => get_standard_option('pve-vmid'),
4492 snapname => get_standard_option('pve-snapshot-name'),
4493 },
4494 },
4495 returns => { type => "object" },
4496 code => sub {
4497 my ($param) = @_;
4498
4499 my $rpcenv = PVE::RPCEnvironment::get();
4500
4501 my $authuser = $rpcenv->get_user();
4502
4503 my $vmid = extract_param($param, 'vmid');
4504
4505 my $snapname = extract_param($param, 'snapname');
4506
ffda963f 4507 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
4508
4509 my $snap = $conf->{snapshots}->{$snapname};
4510
75466c4f
DM
4511 die "snapshot '$snapname' does not exist\n" if !defined($snap);
4512
d788cea6
DM
4513 return $snap;
4514 }});
4515
7e7d7b61
DM
4516__PACKAGE__->register_method({
4517 name => 'rollback',
154ccdcd 4518 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
4519 method => 'POST',
4520 protected => 1,
4521 proxyto => 'node',
4522 description => "Rollback VM state to specified snapshot.",
4523 permissions => {
c268337d 4524 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
4525 },
4526 parameters => {
4527 additionalProperties => 0,
4528 properties => {
4529 node => get_standard_option('pve-node'),
335af808 4530 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4531 snapname => get_standard_option('pve-snapshot-name'),
7e7d7b61
DM
4532 },
4533 },
4534 returns => {
4535 type => 'string',
4536 description => "the task ID.",
4537 },
4538 code => sub {
4539 my ($param) = @_;
4540
4541 my $rpcenv = PVE::RPCEnvironment::get();
4542
4543 my $authuser = $rpcenv->get_user();
4544
4545 my $node = extract_param($param, 'node');
4546
4547 my $vmid = extract_param($param, 'vmid');
4548
4549 my $snapname = extract_param($param, 'snapname');
4550
7e7d7b61 4551 my $realcmd = sub {
22c377f0 4552 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 4553 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
7e7d7b61
DM
4554 };
4555
c068c1c3
WL
4556 my $worker = sub {
4557 # hold migration lock, this makes sure that nobody create replication snapshots
4558 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
4559 };
4560
4561 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
4562 }});
4563
4564__PACKAGE__->register_method({
4565 name => 'delsnapshot',
4566 path => '{vmid}/snapshot/{snapname}',
4567 method => 'DELETE',
4568 protected => 1,
4569 proxyto => 'node',
4570 description => "Delete a VM snapshot.",
4571 permissions => {
f1baf1df 4572 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
4573 },
4574 parameters => {
4575 additionalProperties => 0,
4576 properties => {
4577 node => get_standard_option('pve-node'),
335af808 4578 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4579 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
4580 force => {
4581 optional => 1,
4582 type => 'boolean',
4583 description => "For removal from config file, even if removing disk snapshots fails.",
4584 },
7e7d7b61
DM
4585 },
4586 },
4587 returns => {
4588 type => 'string',
4589 description => "the task ID.",
4590 },
4591 code => sub {
4592 my ($param) = @_;
4593
4594 my $rpcenv = PVE::RPCEnvironment::get();
4595
4596 my $authuser = $rpcenv->get_user();
4597
4598 my $node = extract_param($param, 'node');
4599
4600 my $vmid = extract_param($param, 'vmid');
4601
4602 my $snapname = extract_param($param, 'snapname');
4603
1770b70f 4604 my $lock_obtained;
fdbbed2f 4605 my $do_delete = sub {
1770b70f 4606 $lock_obtained = 1;
22c377f0 4607 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 4608 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 4609 };
9dbd1ee4 4610
fdbbed2f
FE
4611 my $realcmd = sub {
4612 if ($param->{force}) {
4613 $do_delete->();
4614 } else {
1770b70f
FG
4615 eval { PVE::GuestHelpers::guest_migration_lock($vmid, 10, $do_delete); };
4616 if (my $err = $@) {
4617 die $err if $lock_obtained;
4618 die "Failed to obtain guest migration lock - replication running?\n";
4619 }
fdbbed2f
FE
4620 }
4621 };
4622
7b2257a8 4623 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
4624 }});
4625
04a69bb4
AD
4626__PACKAGE__->register_method({
4627 name => 'template',
4628 path => '{vmid}/template',
4629 method => 'POST',
4630 protected => 1,
4631 proxyto => 'node',
4632 description => "Create a Template.",
b02691d8 4633 permissions => {
7af0a6c8
DM
4634 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
4635 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 4636 },
04a69bb4
AD
4637 parameters => {
4638 additionalProperties => 0,
4639 properties => {
4640 node => get_standard_option('pve-node'),
335af808 4641 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
4642 disk => {
4643 optional => 1,
4644 type => 'string',
4645 description => "If you want to convert only 1 disk to base image.",
e0fd2b2f 4646 enum => [PVE::QemuServer::Drive::valid_drive_names()],
04a69bb4
AD
4647 },
4648
4649 },
4650 },
b297918c
FG
4651 returns => {
4652 type => 'string',
4653 description => "the task ID.",
4654 },
04a69bb4
AD
4655 code => sub {
4656 my ($param) = @_;
4657
4658 my $rpcenv = PVE::RPCEnvironment::get();
4659
4660 my $authuser = $rpcenv->get_user();
4661
4662 my $node = extract_param($param, 'node');
4663
4664 my $vmid = extract_param($param, 'vmid');
4665
4666 my $disk = extract_param($param, 'disk');
4667
d2ceac56 4668 my $load_and_check = sub {
ffda963f 4669 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 4670
ffda963f 4671 PVE::QemuConfig->check_lock($conf);
04a69bb4 4672
75466c4f 4673 die "unable to create template, because VM contains snapshots\n"
b91c2aae 4674 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 4675
75466c4f 4676 die "you can't convert a template to a template\n"
ffda963f 4677 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 4678
75466c4f 4679 die "you can't convert a VM to template if VM is running\n"
218cab9a 4680 if PVE::QemuServer::check_running($vmid);
35c5fdef 4681
d2ceac56
FG
4682 return $conf;
4683 };
04a69bb4 4684
d2ceac56 4685 $load_and_check->();
75e7e997 4686
d2ceac56
FG
4687 my $realcmd = sub {
4688 PVE::QemuConfig->lock_config($vmid, sub {
4689 my $conf = $load_and_check->();
4690
4691 $conf->{template} = 1;
4692 PVE::QemuConfig->write_config($vmid, $conf);
4693
4694 PVE::QemuServer::template_create($vmid, $conf, $disk);
4695 });
04a69bb4
AD
4696 };
4697
d2ceac56 4698 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
4699 }});
4700
73709749
ML
4701__PACKAGE__->register_method({
4702 name => 'cloudinit_generated_config_dump',
4703 path => '{vmid}/cloudinit/dump',
4704 method => 'GET',
4705 proxyto => 'node',
4706 description => "Get automatically generated cloudinit config.",
4707 permissions => {
4708 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4709 },
4710 parameters => {
4711 additionalProperties => 0,
4712 properties => {
4713 node => get_standard_option('pve-node'),
4714 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4715 type => {
4716 description => 'Config type.',
4717 type => 'string',
4718 enum => ['user', 'network', 'meta'],
4719 },
4720 },
4721 },
4722 returns => {
4723 type => 'string',
4724 },
4725 code => sub {
4726 my ($param) = @_;
4727
4728 my $conf = PVE::QemuConfig->load_config($param->{vmid});
4729
4730 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
4731 }});
4732
1e3baf05 47331;