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