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