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