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