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