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