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