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