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