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