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