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