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