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