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