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