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