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