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