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