]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
fix version check in qemu_machine_feature_enabled
[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
c725dd5f
DC
2788 if (my @changes = keys %{$newconf->{pending}}) {
2789 my $pending = join(',', @changes);
2790 warn "found pending changes for '$pending', discarding for clone\n";
68e46b84
DC
2791 delete $newconf->{pending};
2792 }
2793
ffda963f 2794 PVE::QemuConfig->write_config($newid, $newconf);
55173c6b
DM
2795
2796 if ($target) {
baca276d 2797 # always deactivate volumes - avoid lvm LVs to be active on several nodes
51eefb7e 2798 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
32acc380 2799 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
baca276d 2800
ffda963f 2801 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
55173c6b
DM
2802 die "Failed to move config to node '$target' - rename failed: $!\n"
2803 if !rename($conffile, $newconffile);
2804 }
d703d4c0 2805
be517049 2806 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
6116f729 2807 };
75466c4f 2808 if (my $err = $@) {
6116f729
DM
2809 unlink $conffile;
2810
c6fdd002
AD
2811 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2812
b83e0181
DM
2813 sleep 1; # some storage like rbd need to wait before release volume - really?
2814
2815 foreach my $volid (@$newvollist) {
2816 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2817 warn $@ if $@;
2818 }
9418baad 2819 die "clone failed: $err";
6116f729
DM
2820 }
2821
2822 return;
2823 };
2824
457010cc
AG
2825 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2826
9418baad 2827 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
6116f729
DM
2828 };
2829
ffda963f 2830 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
6116f729 2831 # Aquire exclusive lock lock for $newid
ffda963f 2832 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
6116f729
DM
2833 });
2834
2835 }});
2836
586bfa78 2837__PACKAGE__->register_method({
43bc02a9
DM
2838 name => 'move_vm_disk',
2839 path => '{vmid}/move_disk',
e2cd75fa 2840 method => 'POST',
586bfa78
AD
2841 protected => 1,
2842 proxyto => 'node',
2843 description => "Move volume to different storage.",
2844 permissions => {
c07a9e3d
DM
2845 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2846 check => [ 'and',
2847 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2848 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2849 ],
586bfa78
AD
2850 },
2851 parameters => {
2852 additionalProperties => 0,
c07a9e3d 2853 properties => {
586bfa78 2854 node => get_standard_option('pve-node'),
335af808 2855 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
586bfa78
AD
2856 disk => {
2857 type => 'string',
2858 description => "The disk you want to move.",
74479ee9 2859 enum => [ PVE::QemuServer::valid_drive_names() ],
586bfa78 2860 },
335af808
DM
2861 storage => get_standard_option('pve-storage-id', {
2862 description => "Target storage.",
2863 completion => \&PVE::QemuServer::complete_storage,
2864 }),
635c3c44 2865 'format' => {
586bfa78
AD
2866 type => 'string',
2867 description => "Target Format.",
2868 enum => [ 'raw', 'qcow2', 'vmdk' ],
2869 optional => 1,
2870 },
70d45e33
DM
2871 delete => {
2872 type => 'boolean',
2873 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2874 optional => 1,
2875 default => 0,
2876 },
586bfa78
AD
2877 digest => {
2878 type => 'string',
2879 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2880 maxLength => 40,
2881 optional => 1,
2882 },
2883 },
2884 },
e2cd75fa
DM
2885 returns => {
2886 type => 'string',
2887 description => "the task ID.",
2888 },
586bfa78
AD
2889 code => sub {
2890 my ($param) = @_;
2891
2892 my $rpcenv = PVE::RPCEnvironment::get();
2893
2894 my $authuser = $rpcenv->get_user();
2895
2896 my $node = extract_param($param, 'node');
2897
2898 my $vmid = extract_param($param, 'vmid');
2899
2900 my $digest = extract_param($param, 'digest');
2901
2902 my $disk = extract_param($param, 'disk');
2903
2904 my $storeid = extract_param($param, 'storage');
2905
2906 my $format = extract_param($param, 'format');
2907
586bfa78
AD
2908 my $storecfg = PVE::Storage::config();
2909
2910 my $updatefn = sub {
2911
ffda963f 2912 my $conf = PVE::QemuConfig->load_config($vmid);
586bfa78 2913
dcce9b46
FG
2914 PVE::QemuConfig->check_lock($conf);
2915
586bfa78
AD
2916 die "checksum missmatch (file change by other user?)\n"
2917 if $digest && $digest ne $conf->{digest};
586bfa78
AD
2918
2919 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2920
2921 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2922
70d45e33 2923 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
586bfa78 2924
931432bd 2925 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 2926
e2cd75fa 2927 my $oldfmt;
70d45e33 2928 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
2929 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2930 $oldfmt = $1;
2931 }
2932
7043d946 2933 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
e2cd75fa 2934 (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 2935
9dbf9b54
FG
2936 # this only checks snapshots because $disk is passed!
2937 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
2938 die "you can't move a disk with snapshots and delete the source\n"
2939 if $snapshotted && $param->{delete};
2940
586bfa78
AD
2941 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2942
2943 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
2944
2945 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
2946
586bfa78
AD
2947 my $realcmd = sub {
2948
2949 my $newvollist = [];
2950
2951 eval {
6cb0144a
EK
2952 local $SIG{INT} =
2953 local $SIG{TERM} =
2954 local $SIG{QUIT} =
2955 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
586bfa78 2956
9dbf9b54
FG
2957 warn "moving disk with snapshots, snapshots will not be moved!\n"
2958 if $snapshotted;
2959
e2cd75fa
DM
2960 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
2961 $vmid, $storeid, $format, 1, $newvollist);
2962
2963 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
2964
8793d495 2965 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
7043d946 2966
fbd7dcce
FG
2967 # convert moved disk to base if part of template
2968 PVE::QemuServer::template_create($vmid, $conf, $disk)
2969 if PVE::QemuConfig->is_template($conf);
2970
ffda963f 2971 PVE::QemuConfig->write_config($vmid, $conf);
73272365 2972
ca662131
SI
2973 if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) {
2974 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); };
2975 }
2976
f34ebd52 2977 eval {
73272365 2978 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
f34ebd52 2979 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
73272365
DM
2980 if !$running;
2981 };
2982 warn $@ if $@;
586bfa78
AD
2983 };
2984 if (my $err = $@) {
2985
2986 foreach my $volid (@$newvollist) {
2987 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2988 warn $@ if $@;
2989 }
2990 die "storage migration failed: $err";
2991 }
70d45e33
DM
2992
2993 if ($param->{delete}) {
a3d0bafb
FG
2994 eval {
2995 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
2996 PVE::Storage::vdisk_free($storecfg, $old_volid);
2997 };
2998 warn $@ if $@;
70d45e33 2999 }
586bfa78
AD
3000 };
3001
3002 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
3003 };
e2cd75fa 3004
ffda963f 3005 return PVE::QemuConfig->lock_config($vmid, $updatefn);
586bfa78
AD
3006 }});
3007
3ea94c60 3008__PACKAGE__->register_method({
afdb31d5 3009 name => 'migrate_vm',
3ea94c60
DM
3010 path => '{vmid}/migrate',
3011 method => 'POST',
3012 protected => 1,
3013 proxyto => 'node',
3014 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
3015 permissions => {
3016 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3017 },
3ea94c60
DM
3018 parameters => {
3019 additionalProperties => 0,
3020 properties => {
3021 node => get_standard_option('pve-node'),
335af808
DM
3022 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3023 target => get_standard_option('pve-node', {
3024 description => "Target node.",
3025 completion => \&PVE::Cluster::complete_migration_target,
3026 }),
3ea94c60
DM
3027 online => {
3028 type => 'boolean',
3029 description => "Use online/live migration.",
3030 optional => 1,
3031 },
3032 force => {
3033 type => 'boolean',
3034 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
3035 optional => 1,
3036 },
2de2d6f7
TL
3037 migration_type => {
3038 type => 'string',
3039 enum => ['secure', 'insecure'],
c07a9e3d 3040 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
3041 optional => 1,
3042 },
3043 migration_network => {
c07a9e3d 3044 type => 'string', format => 'CIDR',
2de2d6f7
TL
3045 description => "CIDR of the (sub) network that is used for migration.",
3046 optional => 1,
3047 },
56af7146
AD
3048 "with-local-disks" => {
3049 type => 'boolean',
3050 description => "Enable live storage migration for local disk",
b74cad8a 3051 optional => 1,
56af7146
AD
3052 },
3053 targetstorage => get_standard_option('pve-storage-id', {
3054 description => "Default target storage.",
3055 optional => 1,
3056 completion => \&PVE::QemuServer::complete_storage,
3057 }),
3ea94c60
DM
3058 },
3059 },
afdb31d5 3060 returns => {
3ea94c60
DM
3061 type => 'string',
3062 description => "the task ID.",
3063 },
3064 code => sub {
3065 my ($param) = @_;
3066
3067 my $rpcenv = PVE::RPCEnvironment::get();
3068
a0d1b1a2 3069 my $authuser = $rpcenv->get_user();
3ea94c60
DM
3070
3071 my $target = extract_param($param, 'target');
3072
3073 my $localnode = PVE::INotify::nodename();
3074 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
3075
3076 PVE::Cluster::check_cfs_quorum();
3077
3078 PVE::Cluster::check_node_exists($target);
3079
3080 my $targetip = PVE::Cluster::remote_node_ip($target);
3081
3082 my $vmid = extract_param($param, 'vmid');
3083
bd2d5fe6 3084 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
b74cad8a
AD
3085 if !$param->{online} && $param->{targetstorage};
3086
afdb31d5 3087 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 3088 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 3089
2de2d6f7
TL
3090 raise_param_exc({ migration_type => "Only root may use this option." })
3091 if $param->{migration_type} && $authuser ne 'root@pam';
3092
3093 # allow root only until better network permissions are available
3094 raise_param_exc({ migration_network => "Only root may use this option." })
3095 if $param->{migration_network} && $authuser ne 'root@pam';
3096
3ea94c60 3097 # test if VM exists
ffda963f 3098 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
3099
3100 # try to detect errors early
a5ed42d3 3101
ffda963f 3102 PVE::QemuConfig->check_lock($conf);
a5ed42d3 3103
3ea94c60 3104 if (PVE::QemuServer::check_running($vmid)) {
afdb31d5 3105 die "cant migrate running VM without --online\n"
3ea94c60
DM
3106 if !$param->{online};
3107 }
3108
47152e2e 3109 my $storecfg = PVE::Storage::config();
d80ad67f
AD
3110
3111 if( $param->{targetstorage}) {
3112 PVE::Storage::storage_check_node($storecfg, $param->{targetstorage}, $target);
3113 } else {
3114 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
3115 }
47152e2e 3116
2003f0f8 3117 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 3118
88fc87b4
DM
3119 my $hacmd = sub {
3120 my $upid = shift;
3ea94c60 3121
c44291cd 3122 my $service = "vm:$vmid";
88fc87b4 3123
2003f0f8 3124 my $cmd = ['ha-manager', 'migrate', $service, $target];
88fc87b4 3125
02765844 3126 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4
DM
3127
3128 PVE::Tools::run_command($cmd);
3129
3130 return;
3131 };
3132
3133 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
3134
3135 } else {
3136
f53c6ad8 3137 my $realcmd = sub {
f53c6ad8
DM
3138 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
3139 };
88fc87b4 3140
f53c6ad8
DM
3141 my $worker = sub {
3142 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
3143 };
3144
f53c6ad8 3145 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 3146 }
3ea94c60 3147
3ea94c60 3148 }});
1e3baf05 3149
91c94f0a 3150__PACKAGE__->register_method({
afdb31d5
DM
3151 name => 'monitor',
3152 path => '{vmid}/monitor',
91c94f0a
DM
3153 method => 'POST',
3154 protected => 1,
3155 proxyto => 'node',
3156 description => "Execute Qemu monitor commands.",
a0d1b1a2 3157 permissions => {
a8f2f427 3158 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 3159 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 3160 },
91c94f0a
DM
3161 parameters => {
3162 additionalProperties => 0,
3163 properties => {
3164 node => get_standard_option('pve-node'),
3165 vmid => get_standard_option('pve-vmid'),
3166 command => {
3167 type => 'string',
3168 description => "The monitor command.",
3169 }
3170 },
3171 },
3172 returns => { type => 'string'},
3173 code => sub {
3174 my ($param) = @_;
3175
a8f2f427
FG
3176 my $rpcenv = PVE::RPCEnvironment::get();
3177 my $authuser = $rpcenv->get_user();
3178
3179 my $is_ro = sub {
3180 my $command = shift;
3181 return $command =~ m/^\s*info(\s+|$)/
3182 || $command =~ m/^\s*help\s*$/;
3183 };
3184
3185 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
3186 if !&$is_ro($param->{command});
3187
91c94f0a
DM
3188 my $vmid = $param->{vmid};
3189
ffda963f 3190 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
3191
3192 my $res = '';
3193 eval {
7b7c6d1b 3194 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
91c94f0a
DM
3195 };
3196 $res = "ERROR: $@" if $@;
3197
3198 return $res;
3199 }});
3200
0d02881c
AD
3201__PACKAGE__->register_method({
3202 name => 'resize_vm',
614e3941 3203 path => '{vmid}/resize',
0d02881c
AD
3204 method => 'PUT',
3205 protected => 1,
3206 proxyto => 'node',
2f48a4f5 3207 description => "Extend volume size.",
0d02881c 3208 permissions => {
3b2773f6 3209 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
3210 },
3211 parameters => {
3212 additionalProperties => 0,
2f48a4f5
DM
3213 properties => {
3214 node => get_standard_option('pve-node'),
335af808 3215 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
3216 skiplock => get_standard_option('skiplock'),
3217 disk => {
3218 type => 'string',
3219 description => "The disk you want to resize.",
74479ee9 3220 enum => [PVE::QemuServer::valid_drive_names()],
2f48a4f5
DM
3221 },
3222 size => {
3223 type => 'string',
f91b2e45 3224 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 3225 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
3226 },
3227 digest => {
3228 type => 'string',
3229 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3230 maxLength => 40,
3231 optional => 1,
3232 },
3233 },
0d02881c
AD
3234 },
3235 returns => { type => 'null'},
3236 code => sub {
3237 my ($param) = @_;
3238
3239 my $rpcenv = PVE::RPCEnvironment::get();
3240
3241 my $authuser = $rpcenv->get_user();
3242
3243 my $node = extract_param($param, 'node');
3244
3245 my $vmid = extract_param($param, 'vmid');
3246
3247 my $digest = extract_param($param, 'digest');
3248
2f48a4f5 3249 my $disk = extract_param($param, 'disk');
75466c4f 3250
2f48a4f5 3251 my $sizestr = extract_param($param, 'size');
0d02881c 3252
f91b2e45 3253 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
3254 raise_param_exc({ skiplock => "Only root may use this option." })
3255 if $skiplock && $authuser ne 'root@pam';
3256
0d02881c
AD
3257 my $storecfg = PVE::Storage::config();
3258
0d02881c
AD
3259 my $updatefn = sub {
3260
ffda963f 3261 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
3262
3263 die "checksum missmatch (file change by other user?)\n"
3264 if $digest && $digest ne $conf->{digest};
ffda963f 3265 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 3266
f91b2e45
DM
3267 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3268
3269 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3270
d662790a
WL
3271 my (undef, undef, undef, undef, undef, undef, $format) =
3272 PVE::Storage::parse_volname($storecfg, $drive->{file});
3273
3274 die "can't resize volume: $disk if snapshot exists\n"
3275 if %{$conf->{snapshots}} && $format eq 'qcow2';
3276
f91b2e45
DM
3277 my $volid = $drive->{file};
3278
3279 die "disk '$disk' has no associated volume\n" if !$volid;
3280
3281 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3282
3283 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3284
3285 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3286
b572a606 3287 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
3288 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3289
3290 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3291 my ($ext, $newsize, $unit) = ($1, $2, $4);
3292 if ($unit) {
3293 if ($unit eq 'K') {
3294 $newsize = $newsize * 1024;
3295 } elsif ($unit eq 'M') {
3296 $newsize = $newsize * 1024 * 1024;
3297 } elsif ($unit eq 'G') {
3298 $newsize = $newsize * 1024 * 1024 * 1024;
3299 } elsif ($unit eq 'T') {
3300 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3301 }
3302 }
3303 $newsize += $size if $ext;
3304 $newsize = int($newsize);
3305
9a478b17 3306 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
3307
3308 return if $size == $newsize;
3309
2f48a4f5 3310 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 3311
f91b2e45 3312 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 3313
f91b2e45
DM
3314 $drive->{size} = $newsize;
3315 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3316
ffda963f 3317 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 3318 };
0d02881c 3319
ffda963f 3320 PVE::QemuConfig->lock_config($vmid, $updatefn);
0d02881c
AD
3321 return undef;
3322 }});
3323
9dbd1ee4 3324__PACKAGE__->register_method({
7e7d7b61 3325 name => 'snapshot_list',
9dbd1ee4 3326 path => '{vmid}/snapshot',
7e7d7b61
DM
3327 method => 'GET',
3328 description => "List all snapshots.",
3329 permissions => {
3330 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3331 },
3332 proxyto => 'node',
3333 protected => 1, # qemu pid files are only readable by root
3334 parameters => {
3335 additionalProperties => 0,
3336 properties => {
e261de40 3337 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
3338 node => get_standard_option('pve-node'),
3339 },
3340 },
3341 returns => {
3342 type => 'array',
3343 items => {
3344 type => "object",
ce9b0a38
DM
3345 properties => {
3346 name => {
3347 description => "Snapshot identifier. Value 'current' identifies the current VM.",
3348 type => 'string',
3349 },
3350 vmstate => {
3351 description => "Snapshot includes RAM.",
3352 type => 'boolean',
3353 optional => 1,
3354 },
3355 description => {
3356 description => "Snapshot description.",
3357 type => 'string',
3358 },
3359 snaptime => {
3360 description => "Snapshot creation time",
3361 type => 'integer',
3362 renderer => 'timestamp',
3363 optional => 1,
3364 },
3365 parent => {
3366 description => "Parent snapshot identifier.",
3367 type => 'string',
3368 optional => 1,
3369 },
3370 },
7e7d7b61
DM
3371 },
3372 links => [ { rel => 'child', href => "{name}" } ],
3373 },
3374 code => sub {
3375 my ($param) = @_;
3376
6aa4651b
DM
3377 my $vmid = $param->{vmid};
3378
ffda963f 3379 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
3380 my $snaphash = $conf->{snapshots} || {};
3381
3382 my $res = [];
3383
3384 foreach my $name (keys %$snaphash) {
0ea6bc69 3385 my $d = $snaphash->{$name};
75466c4f
DM
3386 my $item = {
3387 name => $name,
3388 snaptime => $d->{snaptime} || 0,
6aa4651b 3389 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
3390 description => $d->{description} || '',
3391 };
0ea6bc69 3392 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 3393 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
3394 push @$res, $item;
3395 }
3396
6aa4651b 3397 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
3398 my $current = {
3399 name => 'current',
3400 digest => $conf->{digest},
3401 running => $running,
3402 description => "You are here!",
3403 };
d1914468
DM
3404 $current->{parent} = $conf->{parent} if $conf->{parent};
3405
3406 push @$res, $current;
7e7d7b61
DM
3407
3408 return $res;
3409 }});
3410
3411__PACKAGE__->register_method({
3412 name => 'snapshot',
3413 path => '{vmid}/snapshot',
3414 method => 'POST',
9dbd1ee4
AD
3415 protected => 1,
3416 proxyto => 'node',
3417 description => "Snapshot a VM.",
3418 permissions => {
f1baf1df 3419 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
3420 },
3421 parameters => {
3422 additionalProperties => 0,
3423 properties => {
3424 node => get_standard_option('pve-node'),
335af808 3425 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3426 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
3427 vmstate => {
3428 optional => 1,
3429 type => 'boolean',
3430 description => "Save the vmstate",
3431 },
782f4f75
DM
3432 description => {
3433 optional => 1,
3434 type => 'string',
3435 description => "A textual description or comment.",
3436 },
9dbd1ee4
AD
3437 },
3438 },
7e7d7b61
DM
3439 returns => {
3440 type => 'string',
3441 description => "the task ID.",
3442 },
9dbd1ee4
AD
3443 code => sub {
3444 my ($param) = @_;
3445
3446 my $rpcenv = PVE::RPCEnvironment::get();
3447
3448 my $authuser = $rpcenv->get_user();
3449
3450 my $node = extract_param($param, 'node');
3451
3452 my $vmid = extract_param($param, 'vmid');
3453
9dbd1ee4
AD
3454 my $snapname = extract_param($param, 'snapname');
3455
d1914468
DM
3456 die "unable to use snapshot name 'current' (reserved name)\n"
3457 if $snapname eq 'current';
3458
7e7d7b61 3459 my $realcmd = sub {
22c377f0 3460 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
b2c9558d 3461 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 3462 $param->{description});
7e7d7b61
DM
3463 };
3464
3465 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3466 }});
3467
154ccdcd
DM
3468__PACKAGE__->register_method({
3469 name => 'snapshot_cmd_idx',
3470 path => '{vmid}/snapshot/{snapname}',
3471 description => '',
3472 method => 'GET',
3473 permissions => {
3474 user => 'all',
3475 },
3476 parameters => {
3477 additionalProperties => 0,
3478 properties => {
3479 vmid => get_standard_option('pve-vmid'),
3480 node => get_standard_option('pve-node'),
8abd398b 3481 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
3482 },
3483 },
3484 returns => {
3485 type => 'array',
3486 items => {
3487 type => "object",
3488 properties => {},
3489 },
3490 links => [ { rel => 'child', href => "{cmd}" } ],
3491 },
3492 code => sub {
3493 my ($param) = @_;
3494
3495 my $res = [];
3496
3497 push @$res, { cmd => 'rollback' };
d788cea6 3498 push @$res, { cmd => 'config' };
154ccdcd
DM
3499
3500 return $res;
3501 }});
3502
d788cea6
DM
3503__PACKAGE__->register_method({
3504 name => 'update_snapshot_config',
3505 path => '{vmid}/snapshot/{snapname}/config',
3506 method => 'PUT',
3507 protected => 1,
3508 proxyto => 'node',
3509 description => "Update snapshot metadata.",
3510 permissions => {
3511 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3512 },
3513 parameters => {
3514 additionalProperties => 0,
3515 properties => {
3516 node => get_standard_option('pve-node'),
3517 vmid => get_standard_option('pve-vmid'),
3518 snapname => get_standard_option('pve-snapshot-name'),
3519 description => {
3520 optional => 1,
3521 type => 'string',
3522 description => "A textual description or comment.",
3523 },
3524 },
3525 },
3526 returns => { type => 'null' },
3527 code => sub {
3528 my ($param) = @_;
3529
3530 my $rpcenv = PVE::RPCEnvironment::get();
3531
3532 my $authuser = $rpcenv->get_user();
3533
3534 my $vmid = extract_param($param, 'vmid');
3535
3536 my $snapname = extract_param($param, 'snapname');
3537
3538 return undef if !defined($param->{description});
3539
3540 my $updatefn = sub {
3541
ffda963f 3542 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 3543
ffda963f 3544 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
3545
3546 my $snap = $conf->{snapshots}->{$snapname};
3547
75466c4f
DM
3548 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3549
d788cea6
DM
3550 $snap->{description} = $param->{description} if defined($param->{description});
3551
ffda963f 3552 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
3553 };
3554
ffda963f 3555 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6
DM
3556
3557 return undef;
3558 }});
3559
3560__PACKAGE__->register_method({
3561 name => 'get_snapshot_config',
3562 path => '{vmid}/snapshot/{snapname}/config',
3563 method => 'GET',
3564 proxyto => 'node',
3565 description => "Get snapshot configuration",
3566 permissions => {
c268337d 3567 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
d788cea6
DM
3568 },
3569 parameters => {
3570 additionalProperties => 0,
3571 properties => {
3572 node => get_standard_option('pve-node'),
3573 vmid => get_standard_option('pve-vmid'),
3574 snapname => get_standard_option('pve-snapshot-name'),
3575 },
3576 },
3577 returns => { type => "object" },
3578 code => sub {
3579 my ($param) = @_;
3580
3581 my $rpcenv = PVE::RPCEnvironment::get();
3582
3583 my $authuser = $rpcenv->get_user();
3584
3585 my $vmid = extract_param($param, 'vmid');
3586
3587 my $snapname = extract_param($param, 'snapname');
3588
ffda963f 3589 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
3590
3591 my $snap = $conf->{snapshots}->{$snapname};
3592
75466c4f
DM
3593 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3594
d788cea6
DM
3595 return $snap;
3596 }});
3597
7e7d7b61
DM
3598__PACKAGE__->register_method({
3599 name => 'rollback',
154ccdcd 3600 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
3601 method => 'POST',
3602 protected => 1,
3603 proxyto => 'node',
3604 description => "Rollback VM state to specified snapshot.",
3605 permissions => {
c268337d 3606 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
3607 },
3608 parameters => {
3609 additionalProperties => 0,
3610 properties => {
3611 node => get_standard_option('pve-node'),
335af808 3612 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3613 snapname => get_standard_option('pve-snapshot-name'),
7e7d7b61
DM
3614 },
3615 },
3616 returns => {
3617 type => 'string',
3618 description => "the task ID.",
3619 },
3620 code => sub {
3621 my ($param) = @_;
3622
3623 my $rpcenv = PVE::RPCEnvironment::get();
3624
3625 my $authuser = $rpcenv->get_user();
3626
3627 my $node = extract_param($param, 'node');
3628
3629 my $vmid = extract_param($param, 'vmid');
3630
3631 my $snapname = extract_param($param, 'snapname');
3632
7e7d7b61 3633 my $realcmd = sub {
22c377f0 3634 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 3635 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
7e7d7b61
DM
3636 };
3637
c068c1c3
WL
3638 my $worker = sub {
3639 # hold migration lock, this makes sure that nobody create replication snapshots
3640 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3641 };
3642
3643 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
3644 }});
3645
3646__PACKAGE__->register_method({
3647 name => 'delsnapshot',
3648 path => '{vmid}/snapshot/{snapname}',
3649 method => 'DELETE',
3650 protected => 1,
3651 proxyto => 'node',
3652 description => "Delete a VM snapshot.",
3653 permissions => {
f1baf1df 3654 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
3655 },
3656 parameters => {
3657 additionalProperties => 0,
3658 properties => {
3659 node => get_standard_option('pve-node'),
335af808 3660 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3661 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
3662 force => {
3663 optional => 1,
3664 type => 'boolean',
3665 description => "For removal from config file, even if removing disk snapshots fails.",
3666 },
7e7d7b61
DM
3667 },
3668 },
3669 returns => {
3670 type => 'string',
3671 description => "the task ID.",
3672 },
3673 code => sub {
3674 my ($param) = @_;
3675
3676 my $rpcenv = PVE::RPCEnvironment::get();
3677
3678 my $authuser = $rpcenv->get_user();
3679
3680 my $node = extract_param($param, 'node');
3681
3682 my $vmid = extract_param($param, 'vmid');
3683
3684 my $snapname = extract_param($param, 'snapname');
3685
7e7d7b61 3686 my $realcmd = sub {
22c377f0 3687 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 3688 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 3689 };
9dbd1ee4 3690
7b2257a8 3691 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
3692 }});
3693
04a69bb4
AD
3694__PACKAGE__->register_method({
3695 name => 'template',
3696 path => '{vmid}/template',
3697 method => 'POST',
3698 protected => 1,
3699 proxyto => 'node',
3700 description => "Create a Template.",
b02691d8 3701 permissions => {
7af0a6c8
DM
3702 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3703 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 3704 },
04a69bb4
AD
3705 parameters => {
3706 additionalProperties => 0,
3707 properties => {
3708 node => get_standard_option('pve-node'),
335af808 3709 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
3710 disk => {
3711 optional => 1,
3712 type => 'string',
3713 description => "If you want to convert only 1 disk to base image.",
74479ee9 3714 enum => [PVE::QemuServer::valid_drive_names()],
04a69bb4
AD
3715 },
3716
3717 },
3718 },
3719 returns => { type => 'null'},
3720 code => sub {
3721 my ($param) = @_;
3722
3723 my $rpcenv = PVE::RPCEnvironment::get();
3724
3725 my $authuser = $rpcenv->get_user();
3726
3727 my $node = extract_param($param, 'node');
3728
3729 my $vmid = extract_param($param, 'vmid');
3730
3731 my $disk = extract_param($param, 'disk');
3732
3733 my $updatefn = sub {
3734
ffda963f 3735 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 3736
ffda963f 3737 PVE::QemuConfig->check_lock($conf);
04a69bb4 3738
75466c4f 3739 die "unable to create template, because VM contains snapshots\n"
b91c2aae 3740 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 3741
75466c4f 3742 die "you can't convert a template to a template\n"
ffda963f 3743 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 3744
75466c4f 3745 die "you can't convert a VM to template if VM is running\n"
218cab9a 3746 if PVE::QemuServer::check_running($vmid);
35c5fdef 3747
04a69bb4
AD
3748 my $realcmd = sub {
3749 PVE::QemuServer::template_create($vmid, $conf, $disk);
3750 };
04a69bb4 3751
75e7e997 3752 $conf->{template} = 1;
ffda963f 3753 PVE::QemuConfig->write_config($vmid, $conf);
75e7e997
DM
3754
3755 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
3756 };
3757
ffda963f 3758 PVE::QemuConfig->lock_config($vmid, $updatefn);
04a69bb4
AD
3759 return undef;
3760 }});
3761
1e3baf05 37621;