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