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