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