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