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