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