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