]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
introduce one global CLOUDINIT_DISK_SIZE constant
[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();
2024
a0d1b1a2 2025 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2026
2027 my $node = extract_param($param, 'node');
2028
1e3baf05
DM
2029 my $vmid = extract_param($param, 'vmid');
2030
952958bc
DM
2031 my $machine = extract_param($param, 'machine');
2032
3ea94c60 2033 my $stateuri = extract_param($param, 'stateuri');
afdb31d5 2034 raise_param_exc({ stateuri => "Only root may use this option." })
a0d1b1a2 2035 if $stateuri && $authuser ne 'root@pam';
3ea94c60 2036
1e3baf05 2037 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2038 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2039 if $skiplock && $authuser ne 'root@pam';
1e3baf05 2040
7e8dcf2c
AD
2041 my $migratedfrom = extract_param($param, 'migratedfrom');
2042 raise_param_exc({ migratedfrom => "Only root may use this option." })
2043 if $migratedfrom && $authuser ne 'root@pam';
2044
2de2d6f7
TL
2045 my $migration_type = extract_param($param, 'migration_type');
2046 raise_param_exc({ migration_type => "Only root may use this option." })
2047 if $migration_type && $authuser ne 'root@pam';
2048
2049 my $migration_network = extract_param($param, 'migration_network');
2050 raise_param_exc({ migration_network => "Only root may use this option." })
2051 if $migration_network && $authuser ne 'root@pam';
2052
2189246c
AD
2053 my $targetstorage = extract_param($param, 'targetstorage');
2054 raise_param_exc({ targetstorage => "Only root may use this option." })
2055 if $targetstorage && $authuser ne 'root@pam';
2056
2057 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
2058 if $targetstorage && !$migratedfrom;
2059
7c14dcae
DM
2060 # read spice ticket from STDIN
2061 my $spice_ticket;
2062 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
e5caa02e 2063 if (defined(my $line = <STDIN>)) {
760fb3c8
DM
2064 chomp $line;
2065 $spice_ticket = $line;
2066 }
7c14dcae
DM
2067 }
2068
98cbd0f4
WB
2069 PVE::Cluster::check_cfs_quorum();
2070
afdb31d5 2071 my $storecfg = PVE::Storage::config();
5fdbe4f0 2072
2003f0f8 2073 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri &&
cce37749 2074 $rpcenv->{type} ne 'ha') {
5fdbe4f0 2075
88fc87b4
DM
2076 my $hacmd = sub {
2077 my $upid = shift;
5fdbe4f0 2078
c44291cd 2079 my $service = "vm:$vmid";
5fdbe4f0 2080
2a7e2b82 2081 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
88fc87b4 2082
02765844 2083 print "Requesting HA start for VM $vmid\n";
88fc87b4
DM
2084
2085 PVE::Tools::run_command($cmd);
2086
2087 return;
2088 };
2089
2090 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2091
2092 } else {
2093
2094 my $realcmd = sub {
2095 my $upid = shift;
2096
2097 syslog('info', "start VM $vmid: $upid\n");
2098
fa8ea931 2099 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
2189246c 2100 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
88fc87b4
DM
2101
2102 return;
2103 };
5fdbe4f0 2104
88fc87b4
DM
2105 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2106 }
5fdbe4f0
DM
2107 }});
2108
2109__PACKAGE__->register_method({
afdb31d5 2110 name => 'vm_stop',
5fdbe4f0
DM
2111 path => '{vmid}/status/stop',
2112 method => 'POST',
2113 protected => 1,
2114 proxyto => 'node',
346130b2 2115 description => "Stop virtual machine. The qemu process will exit immediately. This" .
d6c747ff 2116 "is akin to pulling the power plug of a running computer and may damage the VM data",
a0d1b1a2
DM
2117 permissions => {
2118 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2119 },
5fdbe4f0
DM
2120 parameters => {
2121 additionalProperties => 0,
2122 properties => {
2123 node => get_standard_option('pve-node'),
ab5904f7
TL
2124 vmid => get_standard_option('pve-vmid',
2125 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2126 skiplock => get_standard_option('skiplock'),
debe8882 2127 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
c6bb9502
DM
2128 timeout => {
2129 description => "Wait maximal timeout seconds.",
2130 type => 'integer',
2131 minimum => 0,
2132 optional => 1,
254575e9
DM
2133 },
2134 keepActive => {
94a17e1d 2135 description => "Do not deactivate storage volumes.",
254575e9
DM
2136 type => 'boolean',
2137 optional => 1,
2138 default => 0,
c6bb9502 2139 }
5fdbe4f0
DM
2140 },
2141 },
afdb31d5 2142 returns => {
5fdbe4f0
DM
2143 type => 'string',
2144 },
2145 code => sub {
2146 my ($param) = @_;
2147
2148 my $rpcenv = PVE::RPCEnvironment::get();
2149
a0d1b1a2 2150 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2151
2152 my $node = extract_param($param, 'node');
2153
2154 my $vmid = extract_param($param, 'vmid');
2155
2156 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2157 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2158 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2159
254575e9 2160 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2161 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2162 if $keepActive && $authuser ne 'root@pam';
254575e9 2163
af30308f
DM
2164 my $migratedfrom = extract_param($param, 'migratedfrom');
2165 raise_param_exc({ migratedfrom => "Only root may use this option." })
2166 if $migratedfrom && $authuser ne 'root@pam';
2167
2168
ff1a2432
DM
2169 my $storecfg = PVE::Storage::config();
2170
2003f0f8 2171 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
5fdbe4f0 2172
88fc87b4
DM
2173 my $hacmd = sub {
2174 my $upid = shift;
5fdbe4f0 2175
c44291cd 2176 my $service = "vm:$vmid";
c6bb9502 2177
e0feef86 2178 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
88fc87b4 2179
02765844 2180 print "Requesting HA stop for VM $vmid\n";
88fc87b4
DM
2181
2182 PVE::Tools::run_command($cmd);
5fdbe4f0 2183
88fc87b4
DM
2184 return;
2185 };
2186
2187 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2188
2189 } else {
2190 my $realcmd = sub {
2191 my $upid = shift;
2192
2193 syslog('info', "stop VM $vmid: $upid\n");
2194
2195 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
af30308f 2196 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
88fc87b4
DM
2197
2198 return;
2199 };
2200
2201 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2202 }
5fdbe4f0
DM
2203 }});
2204
2205__PACKAGE__->register_method({
afdb31d5 2206 name => 'vm_reset',
5fdbe4f0
DM
2207 path => '{vmid}/status/reset',
2208 method => 'POST',
2209 protected => 1,
2210 proxyto => 'node',
2211 description => "Reset virtual machine.",
a0d1b1a2
DM
2212 permissions => {
2213 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2214 },
5fdbe4f0
DM
2215 parameters => {
2216 additionalProperties => 0,
2217 properties => {
2218 node => get_standard_option('pve-node'),
ab5904f7
TL
2219 vmid => get_standard_option('pve-vmid',
2220 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
2221 skiplock => get_standard_option('skiplock'),
2222 },
2223 },
afdb31d5 2224 returns => {
5fdbe4f0
DM
2225 type => 'string',
2226 },
2227 code => sub {
2228 my ($param) = @_;
2229
2230 my $rpcenv = PVE::RPCEnvironment::get();
2231
a0d1b1a2 2232 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2233
2234 my $node = extract_param($param, 'node');
2235
2236 my $vmid = extract_param($param, 'vmid');
2237
2238 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2239 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2240 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2241
ff1a2432
DM
2242 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2243
5fdbe4f0
DM
2244 my $realcmd = sub {
2245 my $upid = shift;
2246
1e3baf05 2247 PVE::QemuServer::vm_reset($vmid, $skiplock);
5fdbe4f0
DM
2248
2249 return;
2250 };
2251
a0d1b1a2 2252 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2253 }});
2254
2255__PACKAGE__->register_method({
afdb31d5 2256 name => 'vm_shutdown',
5fdbe4f0
DM
2257 path => '{vmid}/status/shutdown',
2258 method => 'POST',
2259 protected => 1,
2260 proxyto => 'node',
d6c747ff
EK
2261 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2262 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
a0d1b1a2
DM
2263 permissions => {
2264 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2265 },
5fdbe4f0
DM
2266 parameters => {
2267 additionalProperties => 0,
2268 properties => {
2269 node => get_standard_option('pve-node'),
ab5904f7
TL
2270 vmid => get_standard_option('pve-vmid',
2271 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2272 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
2273 timeout => {
2274 description => "Wait maximal timeout seconds.",
2275 type => 'integer',
2276 minimum => 0,
2277 optional => 1,
9269013a
DM
2278 },
2279 forceStop => {
2280 description => "Make sure the VM stops.",
2281 type => 'boolean',
2282 optional => 1,
2283 default => 0,
254575e9
DM
2284 },
2285 keepActive => {
94a17e1d 2286 description => "Do not deactivate storage volumes.",
254575e9
DM
2287 type => 'boolean',
2288 optional => 1,
2289 default => 0,
c6bb9502 2290 }
5fdbe4f0
DM
2291 },
2292 },
afdb31d5 2293 returns => {
5fdbe4f0
DM
2294 type => 'string',
2295 },
2296 code => sub {
2297 my ($param) = @_;
2298
2299 my $rpcenv = PVE::RPCEnvironment::get();
2300
a0d1b1a2 2301 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2302
2303 my $node = extract_param($param, 'node');
2304
2305 my $vmid = extract_param($param, 'vmid');
2306
2307 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2308 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2309 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2310
254575e9 2311 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2312 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2313 if $keepActive && $authuser ne 'root@pam';
254575e9 2314
02d07cf5
DM
2315 my $storecfg = PVE::Storage::config();
2316
89897367
DC
2317 my $shutdown = 1;
2318
2319 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2320 # otherwise, we will infer a shutdown command, but run into the timeout,
2321 # then when the vm is resumed, it will instantly shutdown
2322 #
2323 # checking the qmp status here to get feedback to the gui/cli/api
2324 # and the status query should not take too long
2325 my $qmpstatus;
2326 eval {
2327 $qmpstatus = PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
2328 };
2329 my $err = $@ if $@;
2330
2331 if (!$err && $qmpstatus->{status} eq "paused") {
2332 if ($param->{forceStop}) {
2333 warn "VM is paused - stop instead of shutdown\n";
2334 $shutdown = 0;
2335 } else {
2336 die "VM is paused - cannot shutdown\n";
2337 }
2338 }
2339
ae849692
DM
2340 if (PVE::HA::Config::vm_is_ha_managed($vmid) &&
2341 ($rpcenv->{type} ne 'ha')) {
5fdbe4f0 2342
ae849692
DM
2343 my $hacmd = sub {
2344 my $upid = shift;
5fdbe4f0 2345
ae849692 2346 my $service = "vm:$vmid";
c6bb9502 2347
ae849692
DM
2348 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2349
02765844 2350 print "Requesting HA stop for VM $vmid\n";
ae849692
DM
2351
2352 PVE::Tools::run_command($cmd);
2353
2354 return;
2355 };
2356
2357 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2358
2359 } else {
2360
2361 my $realcmd = sub {
2362 my $upid = shift;
2363
2364 syslog('info', "shutdown VM $vmid: $upid\n");
5fdbe4f0 2365
ae849692
DM
2366 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2367 $shutdown, $param->{forceStop}, $keepActive);
2368
2369 return;
2370 };
2371
2372 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2373 }
5fdbe4f0
DM
2374 }});
2375
2376__PACKAGE__->register_method({
afdb31d5 2377 name => 'vm_suspend',
5fdbe4f0
DM
2378 path => '{vmid}/status/suspend',
2379 method => 'POST',
2380 protected => 1,
2381 proxyto => 'node',
2382 description => "Suspend virtual machine.",
a0d1b1a2
DM
2383 permissions => {
2384 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2385 },
5fdbe4f0
DM
2386 parameters => {
2387 additionalProperties => 0,
2388 properties => {
2389 node => get_standard_option('pve-node'),
ab5904f7
TL
2390 vmid => get_standard_option('pve-vmid',
2391 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2392 skiplock => get_standard_option('skiplock'),
22371fe0
DC
2393 todisk => {
2394 type => 'boolean',
2395 default => 0,
2396 optional => 1,
2397 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
2398 },
48b4cdc2
DC
2399 statestorage => get_standard_option('pve-storage-id', {
2400 description => "The storage for the VM state",
2401 requires => 'todisk',
2402 optional => 1,
2403 completion => \&PVE::Storage::complete_storage_enabled,
2404 }),
5fdbe4f0
DM
2405 },
2406 },
afdb31d5 2407 returns => {
5fdbe4f0
DM
2408 type => 'string',
2409 },
2410 code => sub {
2411 my ($param) = @_;
2412
2413 my $rpcenv = PVE::RPCEnvironment::get();
2414
a0d1b1a2 2415 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2416
2417 my $node = extract_param($param, 'node');
2418
2419 my $vmid = extract_param($param, 'vmid');
2420
22371fe0
DC
2421 my $todisk = extract_param($param, 'todisk') // 0;
2422
48b4cdc2
DC
2423 my $statestorage = extract_param($param, 'statestorage');
2424
5fdbe4f0 2425 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2426 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2427 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2428
ff1a2432
DM
2429 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2430
22371fe0
DC
2431 die "Cannot suspend HA managed VM to disk\n"
2432 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
2433
f17fb184
DC
2434 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
2435
5fdbe4f0
DM
2436 my $realcmd = sub {
2437 my $upid = shift;
2438
2439 syslog('info', "suspend VM $vmid: $upid\n");
2440
48b4cdc2 2441 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
5fdbe4f0
DM
2442
2443 return;
2444 };
2445
f17fb184 2446 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2447 }});
2448
2449__PACKAGE__->register_method({
afdb31d5 2450 name => 'vm_resume',
5fdbe4f0
DM
2451 path => '{vmid}/status/resume',
2452 method => 'POST',
2453 protected => 1,
2454 proxyto => 'node',
2455 description => "Resume virtual machine.",
a0d1b1a2
DM
2456 permissions => {
2457 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2458 },
5fdbe4f0
DM
2459 parameters => {
2460 additionalProperties => 0,
2461 properties => {
2462 node => get_standard_option('pve-node'),
ab5904f7
TL
2463 vmid => get_standard_option('pve-vmid',
2464 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2465 skiplock => get_standard_option('skiplock'),
289e0b85
AD
2466 nocheck => { type => 'boolean', optional => 1 },
2467
5fdbe4f0
DM
2468 },
2469 },
afdb31d5 2470 returns => {
5fdbe4f0
DM
2471 type => 'string',
2472 },
2473 code => sub {
2474 my ($param) = @_;
2475
2476 my $rpcenv = PVE::RPCEnvironment::get();
2477
a0d1b1a2 2478 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2479
2480 my $node = extract_param($param, 'node');
2481
2482 my $vmid = extract_param($param, 'vmid');
2483
2484 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2485 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2486 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2487
289e0b85
AD
2488 my $nocheck = extract_param($param, 'nocheck');
2489
cd9a035b
TL
2490 my $to_disk_suspended;
2491 eval {
2492 PVE::QemuConfig->lock_config($vmid, sub {
2493 my $conf = PVE::QemuConfig->load_config($vmid);
2494 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
2495 });
2496 };
2497
2498 die "VM $vmid not running\n"
2499 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
ff1a2432 2500
5fdbe4f0
DM
2501 my $realcmd = sub {
2502 my $upid = shift;
2503
2504 syslog('info', "resume VM $vmid: $upid\n");
2505
cd9a035b
TL
2506 if (!$to_disk_suspended) {
2507 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2508 } else {
2509 my $storecfg = PVE::Storage::config();
2510 PVE::QemuServer::vm_start($storecfg, $vmid, undef, $skiplock);
2511 }
1e3baf05 2512
5fdbe4f0
DM
2513 return;
2514 };
2515
a0d1b1a2 2516 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2517 }});
2518
2519__PACKAGE__->register_method({
afdb31d5 2520 name => 'vm_sendkey',
5fdbe4f0
DM
2521 path => '{vmid}/sendkey',
2522 method => 'PUT',
2523 protected => 1,
2524 proxyto => 'node',
2525 description => "Send key event to virtual machine.",
a0d1b1a2
DM
2526 permissions => {
2527 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2528 },
5fdbe4f0
DM
2529 parameters => {
2530 additionalProperties => 0,
2531 properties => {
2532 node => get_standard_option('pve-node'),
ab5904f7
TL
2533 vmid => get_standard_option('pve-vmid',
2534 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
2535 skiplock => get_standard_option('skiplock'),
2536 key => {
2537 description => "The key (qemu monitor encoding).",
2538 type => 'string'
2539 }
2540 },
2541 },
2542 returns => { type => 'null'},
2543 code => sub {
2544 my ($param) = @_;
2545
2546 my $rpcenv = PVE::RPCEnvironment::get();
2547
a0d1b1a2 2548 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2549
2550 my $node = extract_param($param, 'node');
2551
2552 my $vmid = extract_param($param, 'vmid');
2553
2554 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2555 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2556 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0
DM
2557
2558 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2559
2560 return;
1e3baf05
DM
2561 }});
2562
1ac0d2ee
AD
2563__PACKAGE__->register_method({
2564 name => 'vm_feature',
2565 path => '{vmid}/feature',
2566 method => 'GET',
2567 proxyto => 'node',
75466c4f 2568 protected => 1,
1ac0d2ee
AD
2569 description => "Check if feature for virtual machine is available.",
2570 permissions => {
2571 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2572 },
2573 parameters => {
2574 additionalProperties => 0,
2575 properties => {
2576 node => get_standard_option('pve-node'),
2577 vmid => get_standard_option('pve-vmid'),
2578 feature => {
2579 description => "Feature to check.",
2580 type => 'string',
7758ce86 2581 enum => [ 'snapshot', 'clone', 'copy' ],
1ac0d2ee
AD
2582 },
2583 snapname => get_standard_option('pve-snapshot-name', {
2584 optional => 1,
2585 }),
2586 },
1ac0d2ee
AD
2587 },
2588 returns => {
719893a9
DM
2589 type => "object",
2590 properties => {
2591 hasFeature => { type => 'boolean' },
7043d946 2592 nodes => {
719893a9
DM
2593 type => 'array',
2594 items => { type => 'string' },
2595 }
2596 },
1ac0d2ee
AD
2597 },
2598 code => sub {
2599 my ($param) = @_;
2600
2601 my $node = extract_param($param, 'node');
2602
2603 my $vmid = extract_param($param, 'vmid');
2604
2605 my $snapname = extract_param($param, 'snapname');
2606
2607 my $feature = extract_param($param, 'feature');
2608
2609 my $running = PVE::QemuServer::check_running($vmid);
2610
ffda963f 2611 my $conf = PVE::QemuConfig->load_config($vmid);
1ac0d2ee
AD
2612
2613 if($snapname){
2614 my $snap = $conf->{snapshots}->{$snapname};
2615 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2616 $conf = $snap;
2617 }
2618 my $storecfg = PVE::Storage::config();
2619
719893a9 2620 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
b2c9558d 2621 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
7043d946 2622
719893a9
DM
2623 return {
2624 hasFeature => $hasFeature,
2625 nodes => [ keys %$nodelist ],
7043d946 2626 };
1ac0d2ee
AD
2627 }});
2628
6116f729 2629__PACKAGE__->register_method({
9418baad
DM
2630 name => 'clone_vm',
2631 path => '{vmid}/clone',
6116f729
DM
2632 method => 'POST',
2633 protected => 1,
2634 proxyto => 'node',
37329185 2635 description => "Create a copy of virtual machine/template.",
6116f729 2636 permissions => {
9418baad 2637 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
6116f729
DM
2638 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2639 "'Datastore.AllocateSpace' on any used storage.",
75466c4f
DM
2640 check =>
2641 [ 'and',
9418baad 2642 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
75466c4f 2643 [ 'or',
6116f729
DM
2644 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2645 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2646 ],
2647 ]
2648 },
2649 parameters => {
2650 additionalProperties => 0,
2651 properties => {
6116f729 2652 node => get_standard_option('pve-node'),
335af808 2653 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1ae43f8c
DM
2654 newid => get_standard_option('pve-vmid', {
2655 completion => \&PVE::Cluster::complete_next_vmid,
2656 description => 'VMID for the clone.' }),
a60ab1a6
DM
2657 name => {
2658 optional => 1,
2659 type => 'string', format => 'dns-name',
2660 description => "Set a name for the new VM.",
2661 },
2662 description => {
2663 optional => 1,
2664 type => 'string',
2665 description => "Description for the new VM.",
2666 },
75466c4f 2667 pool => {
6116f729
DM
2668 optional => 1,
2669 type => 'string', format => 'pve-poolid',
2670 description => "Add the new VM to the specified pool.",
2671 },
9076d880 2672 snapname => get_standard_option('pve-snapshot-name', {
9076d880
DM
2673 optional => 1,
2674 }),
81f043eb 2675 storage => get_standard_option('pve-storage-id', {
9418baad 2676 description => "Target storage for full clone.",
81f043eb
AD
2677 optional => 1,
2678 }),
55173c6b 2679 'format' => {
fd13b1d0 2680 description => "Target format for file storage. Only valid for full clone.",
42a19c87
AD
2681 type => 'string',
2682 optional => 1,
55173c6b 2683 enum => [ 'raw', 'qcow2', 'vmdk'],
42a19c87 2684 },
6116f729
DM
2685 full => {
2686 optional => 1,
55173c6b 2687 type => 'boolean',
fd13b1d0 2688 description => "Create a full copy of all disks. This is always done when " .
9418baad 2689 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
6116f729 2690 },
75466c4f 2691 target => get_standard_option('pve-node', {
55173c6b
DM
2692 description => "Target node. Only allowed if the original VM is on shared storage.",
2693 optional => 1,
2694 }),
0aab5a16
SI
2695 bwlimit => {
2696 description => "Override I/O bandwidth limit (in KiB/s).",
2697 optional => 1,
2698 type => 'integer',
2699 minimum => '0',
41756a3b 2700 default => 'clone limit from datacenter or storage config',
0aab5a16 2701 },
55173c6b 2702 },
6116f729
DM
2703 },
2704 returns => {
2705 type => 'string',
2706 },
2707 code => sub {
2708 my ($param) = @_;
2709
2710 my $rpcenv = PVE::RPCEnvironment::get();
2711
55173c6b 2712 my $authuser = $rpcenv->get_user();
6116f729
DM
2713
2714 my $node = extract_param($param, 'node');
2715
2716 my $vmid = extract_param($param, 'vmid');
2717
2718 my $newid = extract_param($param, 'newid');
2719
6116f729
DM
2720 my $pool = extract_param($param, 'pool');
2721
2722 if (defined($pool)) {
2723 $rpcenv->check_pool_exist($pool);
2724 }
2725
55173c6b 2726 my $snapname = extract_param($param, 'snapname');
9076d880 2727
81f043eb
AD
2728 my $storage = extract_param($param, 'storage');
2729
42a19c87
AD
2730 my $format = extract_param($param, 'format');
2731
55173c6b
DM
2732 my $target = extract_param($param, 'target');
2733
2734 my $localnode = PVE::INotify::nodename();
2735
751cc556 2736 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
55173c6b
DM
2737
2738 PVE::Cluster::check_node_exists($target) if $target;
2739
6116f729
DM
2740 my $storecfg = PVE::Storage::config();
2741
4a5a2590
DM
2742 if ($storage) {
2743 # check if storage is enabled on local node
2744 PVE::Storage::storage_check_enabled($storecfg, $storage);
2745 if ($target) {
2746 # check if storage is available on target node
2747 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2748 # clone only works if target storage is shared
2749 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2750 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2751 }
2752 }
2753
55173c6b 2754 PVE::Cluster::check_cfs_quorum();
6116f729 2755
4e4f83fe
DM
2756 my $running = PVE::QemuServer::check_running($vmid) || 0;
2757
4e4f83fe
DM
2758 # exclusive lock if VM is running - else shared lock is enough;
2759 my $shared_lock = $running ? 0 : 1;
2760
9418baad 2761 my $clonefn = sub {
6116f729 2762
829967a9
DM
2763 # do all tests after lock
2764 # we also try to do all tests before we fork the worker
2765
ffda963f 2766 my $conf = PVE::QemuConfig->load_config($vmid);
6116f729 2767
ffda963f 2768 PVE::QemuConfig->check_lock($conf);
6116f729 2769
4e4f83fe 2770 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
6116f729 2771
4e4f83fe 2772 die "unexpected state change\n" if $verify_running != $running;
6116f729 2773
75466c4f
DM
2774 die "snapshot '$snapname' does not exist\n"
2775 if $snapname && !defined( $conf->{snapshots}->{$snapname});
6116f729 2776
fd13b1d0
DM
2777 my $full = extract_param($param, 'full');
2778 if (!defined($full)) {
2779 $full = !PVE::QemuConfig->is_template($conf);
2780 }
2781
2782 die "parameter 'storage' not allowed for linked clones\n"
2783 if defined($storage) && !$full;
2784
2785 die "parameter 'format' not allowed for linked clones\n"
2786 if defined($format) && !$full;
2787
75466c4f 2788 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
9076d880 2789
9418baad 2790 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
6116f729 2791
9418baad 2792 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
75466c4f 2793
ffda963f 2794 my $conffile = PVE::QemuConfig->config_file($newid);
6116f729
DM
2795
2796 die "unable to create VM $newid: config file already exists\n"
2797 if -f $conffile;
2798
9418baad 2799 my $newconf = { lock => 'clone' };
829967a9 2800 my $drives = {};
34456bf0 2801 my $fullclone = {};
829967a9
DM
2802 my $vollist = [];
2803
2804 foreach my $opt (keys %$oldconf) {
2805 my $value = $oldconf->{$opt};
2806
2807 # do not copy snapshot related info
2808 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2809 $opt eq 'vmstate' || $opt eq 'snapstate';
2810
a78ea5df
WL
2811 # no need to copy unused images, because VMID(owner) changes anyways
2812 next if $opt =~ m/^unused\d+$/;
2813
829967a9
DM
2814 # always change MAC! address
2815 if ($opt =~ m/^net(\d+)$/) {
2816 my $net = PVE::QemuServer::parse_net($value);
b5b99790
WB
2817 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2818 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
829967a9 2819 $newconf->{$opt} = PVE::QemuServer::print_net($net);
74479ee9 2820 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1f1412d1
DM
2821 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2822 die "unable to parse drive options for '$opt'\n" if !$drive;
931432bd 2823 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
829967a9
DM
2824 $newconf->{$opt} = $value; # simply copy configuration
2825 } else {
fd13b1d0 2826 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
6318daca 2827 die "Full clone feature is not supported for drive '$opt'\n"
dba198b0 2828 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
34456bf0 2829 $fullclone->{$opt} = 1;
64ff6fe4
SP
2830 } else {
2831 # not full means clone instead of copy
6318daca 2832 die "Linked clone feature is not supported for drive '$opt'\n"
64ff6fe4 2833 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
dba198b0 2834 }
829967a9
DM
2835 $drives->{$opt} = $drive;
2836 push @$vollist, $drive->{file};
2837 }
2838 } else {
2839 # copy everything else
2840 $newconf->{$opt} = $value;
2841 }
2842 }
2843
cd11416f 2844 # auto generate a new uuid
cd11416f 2845 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
6ee499ff 2846 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
cd11416f
DM
2847 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2848
6ee499ff
DC
2849 # auto generate a new vmgenid if the option was set
2850 if ($newconf->{vmgenid}) {
2851 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
2852 }
2853
829967a9
DM
2854 delete $newconf->{template};
2855
2856 if ($param->{name}) {
2857 $newconf->{name} = $param->{name};
2858 } else {
c55fee03
DM
2859 if ($oldconf->{name}) {
2860 $newconf->{name} = "Copy-of-$oldconf->{name}";
2861 } else {
2862 $newconf->{name} = "Copy-of-VM-$vmid";
2863 }
829967a9 2864 }
2dd53043 2865
829967a9
DM
2866 if ($param->{description}) {
2867 $newconf->{description} = $param->{description};
2868 }
2869
6116f729 2870 # create empty/temp config - this fails if VM already exists on other node
9418baad 2871 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
6116f729
DM
2872
2873 my $realcmd = sub {
2874 my $upid = shift;
2875
b83e0181 2876 my $newvollist = [];
c6fdd002 2877 my $jobs = {};
6116f729 2878
b83e0181 2879 eval {
eaae66be
TL
2880 local $SIG{INT} =
2881 local $SIG{TERM} =
2882 local $SIG{QUIT} =
2883 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
75466c4f 2884
eb15b9f0 2885 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
6116f729 2886
0aab5a16
SI
2887 my $bwlimit = extract_param($param, 'bwlimit');
2888
c6fdd002
AD
2889 my $total_jobs = scalar(keys %{$drives});
2890 my $i = 1;
c6fdd002 2891
829967a9
DM
2892 foreach my $opt (keys %$drives) {
2893 my $drive = $drives->{$opt};
3b4cf0f0 2894 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2dd53043 2895
0aab5a16 2896 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
f0dbdb68
TL
2897 my $storage_list = [ $src_sid ];
2898 push @$storage_list, $storage if defined($storage);
ee43cd48 2899 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
0aab5a16 2900
152fe752 2901 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
3b4cf0f0 2902 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
0aab5a16 2903 $jobs, $skipcomplete, $oldconf->{agent}, $clonelimit);
00b095ca 2904
152fe752 2905 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2dd53043 2906
ffda963f 2907 PVE::QemuConfig->write_config($newid, $newconf);
c6fdd002 2908 $i++;
829967a9 2909 }
b83e0181
DM
2910
2911 delete $newconf->{lock};
68e46b84
DC
2912
2913 # do not write pending changes
c725dd5f
DC
2914 if (my @changes = keys %{$newconf->{pending}}) {
2915 my $pending = join(',', @changes);
2916 warn "found pending changes for '$pending', discarding for clone\n";
68e46b84
DC
2917 delete $newconf->{pending};
2918 }
2919
ffda963f 2920 PVE::QemuConfig->write_config($newid, $newconf);
55173c6b
DM
2921
2922 if ($target) {
baca276d 2923 # always deactivate volumes - avoid lvm LVs to be active on several nodes
51eefb7e 2924 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
32acc380 2925 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
baca276d 2926
ffda963f 2927 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
55173c6b
DM
2928 die "Failed to move config to node '$target' - rename failed: $!\n"
2929 if !rename($conffile, $newconffile);
2930 }
d703d4c0 2931
be517049 2932 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
6116f729 2933 };
75466c4f 2934 if (my $err = $@) {
6116f729
DM
2935 unlink $conffile;
2936
c6fdd002
AD
2937 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2938
b83e0181
DM
2939 sleep 1; # some storage like rbd need to wait before release volume - really?
2940
2941 foreach my $volid (@$newvollist) {
2942 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2943 warn $@ if $@;
2944 }
9418baad 2945 die "clone failed: $err";
6116f729
DM
2946 }
2947
2948 return;
2949 };
2950
457010cc
AG
2951 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2952
9418baad 2953 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
6116f729
DM
2954 };
2955
ffda963f 2956 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
6116f729 2957 # Aquire exclusive lock lock for $newid
ffda963f 2958 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
6116f729
DM
2959 });
2960
2961 }});
2962
586bfa78 2963__PACKAGE__->register_method({
43bc02a9
DM
2964 name => 'move_vm_disk',
2965 path => '{vmid}/move_disk',
e2cd75fa 2966 method => 'POST',
586bfa78
AD
2967 protected => 1,
2968 proxyto => 'node',
2969 description => "Move volume to different storage.",
2970 permissions => {
c07a9e3d
DM
2971 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2972 check => [ 'and',
2973 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2974 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2975 ],
586bfa78
AD
2976 },
2977 parameters => {
2978 additionalProperties => 0,
c07a9e3d 2979 properties => {
586bfa78 2980 node => get_standard_option('pve-node'),
335af808 2981 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
586bfa78
AD
2982 disk => {
2983 type => 'string',
2984 description => "The disk you want to move.",
74479ee9 2985 enum => [ PVE::QemuServer::valid_drive_names() ],
586bfa78 2986 },
335af808
DM
2987 storage => get_standard_option('pve-storage-id', {
2988 description => "Target storage.",
2989 completion => \&PVE::QemuServer::complete_storage,
2990 }),
635c3c44 2991 'format' => {
586bfa78
AD
2992 type => 'string',
2993 description => "Target Format.",
2994 enum => [ 'raw', 'qcow2', 'vmdk' ],
2995 optional => 1,
2996 },
70d45e33
DM
2997 delete => {
2998 type => 'boolean',
2999 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
3000 optional => 1,
3001 default => 0,
3002 },
586bfa78
AD
3003 digest => {
3004 type => 'string',
3005 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3006 maxLength => 40,
3007 optional => 1,
3008 },
0aab5a16
SI
3009 bwlimit => {
3010 description => "Override I/O bandwidth limit (in KiB/s).",
3011 optional => 1,
3012 type => 'integer',
3013 minimum => '0',
41756a3b 3014 default => 'move limit from datacenter or storage config',
0aab5a16 3015 },
586bfa78
AD
3016 },
3017 },
e2cd75fa
DM
3018 returns => {
3019 type => 'string',
3020 description => "the task ID.",
3021 },
586bfa78
AD
3022 code => sub {
3023 my ($param) = @_;
3024
3025 my $rpcenv = PVE::RPCEnvironment::get();
3026
3027 my $authuser = $rpcenv->get_user();
3028
3029 my $node = extract_param($param, 'node');
3030
3031 my $vmid = extract_param($param, 'vmid');
3032
3033 my $digest = extract_param($param, 'digest');
3034
3035 my $disk = extract_param($param, 'disk');
3036
3037 my $storeid = extract_param($param, 'storage');
3038
3039 my $format = extract_param($param, 'format');
3040
586bfa78
AD
3041 my $storecfg = PVE::Storage::config();
3042
3043 my $updatefn = sub {
3044
ffda963f 3045 my $conf = PVE::QemuConfig->load_config($vmid);
586bfa78 3046
dcce9b46
FG
3047 PVE::QemuConfig->check_lock($conf);
3048
586bfa78
AD
3049 die "checksum missmatch (file change by other user?)\n"
3050 if $digest && $digest ne $conf->{digest};
586bfa78
AD
3051
3052 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3053
3054 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3055
70d45e33 3056 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
586bfa78 3057
931432bd 3058 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 3059
e2cd75fa 3060 my $oldfmt;
70d45e33 3061 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
3062 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3063 $oldfmt = $1;
3064 }
3065
7043d946 3066 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
e2cd75fa 3067 (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 3068
9dbf9b54
FG
3069 # this only checks snapshots because $disk is passed!
3070 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
3071 die "you can't move a disk with snapshots and delete the source\n"
3072 if $snapshotted && $param->{delete};
3073
586bfa78
AD
3074 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
3075
3076 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
3077
3078 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
3079
586bfa78
AD
3080 my $realcmd = sub {
3081
3082 my $newvollist = [];
3083
3084 eval {
6cb0144a
EK
3085 local $SIG{INT} =
3086 local $SIG{TERM} =
3087 local $SIG{QUIT} =
3088 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
586bfa78 3089
9dbf9b54
FG
3090 warn "moving disk with snapshots, snapshots will not be moved!\n"
3091 if $snapshotted;
3092
0aab5a16
SI
3093 my $bwlimit = extract_param($param, 'bwlimit');
3094 my $movelimit = PVE::Storage::get_bandwidth_limit('move', [$oldstoreid, $storeid], $bwlimit);
3095
e2cd75fa 3096 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
0aab5a16 3097 $vmid, $storeid, $format, 1, $newvollist, undef, undef, undef, $movelimit);
e2cd75fa
DM
3098
3099 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
3100
8793d495 3101 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
7043d946 3102
fbd7dcce
FG
3103 # convert moved disk to base if part of template
3104 PVE::QemuServer::template_create($vmid, $conf, $disk)
3105 if PVE::QemuConfig->is_template($conf);
3106
ffda963f 3107 PVE::QemuConfig->write_config($vmid, $conf);
73272365 3108
ca662131
SI
3109 if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) {
3110 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); };
3111 }
3112
f34ebd52 3113 eval {
73272365 3114 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
f34ebd52 3115 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
73272365
DM
3116 if !$running;
3117 };
3118 warn $@ if $@;
586bfa78
AD
3119 };
3120 if (my $err = $@) {
3121
3122 foreach my $volid (@$newvollist) {
3123 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3124 warn $@ if $@;
3125 }
3126 die "storage migration failed: $err";
3127 }
70d45e33
DM
3128
3129 if ($param->{delete}) {
a3d0bafb
FG
3130 eval {
3131 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
3132 PVE::Storage::vdisk_free($storecfg, $old_volid);
3133 };
3134 warn $@ if $@;
70d45e33 3135 }
586bfa78
AD
3136 };
3137
3138 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
3139 };
e2cd75fa 3140
ffda963f 3141 return PVE::QemuConfig->lock_config($vmid, $updatefn);
586bfa78
AD
3142 }});
3143
3ea94c60 3144__PACKAGE__->register_method({
afdb31d5 3145 name => 'migrate_vm',
3ea94c60
DM
3146 path => '{vmid}/migrate',
3147 method => 'POST',
3148 protected => 1,
3149 proxyto => 'node',
3150 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
3151 permissions => {
3152 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3153 },
3ea94c60
DM
3154 parameters => {
3155 additionalProperties => 0,
3156 properties => {
3157 node => get_standard_option('pve-node'),
335af808
DM
3158 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3159 target => get_standard_option('pve-node', {
3160 description => "Target node.",
3161 completion => \&PVE::Cluster::complete_migration_target,
3162 }),
3ea94c60
DM
3163 online => {
3164 type => 'boolean',
3165 description => "Use online/live migration.",
3166 optional => 1,
3167 },
3168 force => {
3169 type => 'boolean',
3170 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
3171 optional => 1,
3172 },
2de2d6f7
TL
3173 migration_type => {
3174 type => 'string',
3175 enum => ['secure', 'insecure'],
c07a9e3d 3176 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
3177 optional => 1,
3178 },
3179 migration_network => {
c07a9e3d 3180 type => 'string', format => 'CIDR',
2de2d6f7
TL
3181 description => "CIDR of the (sub) network that is used for migration.",
3182 optional => 1,
3183 },
56af7146
AD
3184 "with-local-disks" => {
3185 type => 'boolean',
3186 description => "Enable live storage migration for local disk",
b74cad8a 3187 optional => 1,
56af7146
AD
3188 },
3189 targetstorage => get_standard_option('pve-storage-id', {
3190 description => "Default target storage.",
3191 optional => 1,
3192 completion => \&PVE::QemuServer::complete_storage,
3193 }),
0aab5a16
SI
3194 bwlimit => {
3195 description => "Override I/O bandwidth limit (in KiB/s).",
3196 optional => 1,
3197 type => 'integer',
3198 minimum => '0',
41756a3b 3199 default => 'migrate limit from datacenter or storage config',
0aab5a16 3200 },
3ea94c60
DM
3201 },
3202 },
afdb31d5 3203 returns => {
3ea94c60
DM
3204 type => 'string',
3205 description => "the task ID.",
3206 },
3207 code => sub {
3208 my ($param) = @_;
3209
3210 my $rpcenv = PVE::RPCEnvironment::get();
3211
a0d1b1a2 3212 my $authuser = $rpcenv->get_user();
3ea94c60
DM
3213
3214 my $target = extract_param($param, 'target');
3215
3216 my $localnode = PVE::INotify::nodename();
3217 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
3218
3219 PVE::Cluster::check_cfs_quorum();
3220
3221 PVE::Cluster::check_node_exists($target);
3222
3223 my $targetip = PVE::Cluster::remote_node_ip($target);
3224
3225 my $vmid = extract_param($param, 'vmid');
3226
bd2d5fe6 3227 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
b74cad8a
AD
3228 if !$param->{online} && $param->{targetstorage};
3229
afdb31d5 3230 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 3231 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 3232
2de2d6f7
TL
3233 raise_param_exc({ migration_type => "Only root may use this option." })
3234 if $param->{migration_type} && $authuser ne 'root@pam';
3235
3236 # allow root only until better network permissions are available
3237 raise_param_exc({ migration_network => "Only root may use this option." })
3238 if $param->{migration_network} && $authuser ne 'root@pam';
3239
3ea94c60 3240 # test if VM exists
ffda963f 3241 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
3242
3243 # try to detect errors early
a5ed42d3 3244
ffda963f 3245 PVE::QemuConfig->check_lock($conf);
a5ed42d3 3246
3ea94c60 3247 if (PVE::QemuServer::check_running($vmid)) {
afdb31d5 3248 die "cant migrate running VM without --online\n"
3ea94c60
DM
3249 if !$param->{online};
3250 }
3251
47152e2e 3252 my $storecfg = PVE::Storage::config();
d80ad67f
AD
3253
3254 if( $param->{targetstorage}) {
3255 PVE::Storage::storage_check_node($storecfg, $param->{targetstorage}, $target);
3256 } else {
3257 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
3258 }
47152e2e 3259
2003f0f8 3260 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 3261
88fc87b4
DM
3262 my $hacmd = sub {
3263 my $upid = shift;
3ea94c60 3264
c44291cd 3265 my $service = "vm:$vmid";
88fc87b4 3266
2003f0f8 3267 my $cmd = ['ha-manager', 'migrate', $service, $target];
88fc87b4 3268
02765844 3269 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4
DM
3270
3271 PVE::Tools::run_command($cmd);
3272
3273 return;
3274 };
3275
3276 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
3277
3278 } else {
3279
f53c6ad8 3280 my $realcmd = sub {
f53c6ad8
DM
3281 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
3282 };
88fc87b4 3283
f53c6ad8
DM
3284 my $worker = sub {
3285 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
3286 };
3287
f53c6ad8 3288 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 3289 }
3ea94c60 3290
3ea94c60 3291 }});
1e3baf05 3292
91c94f0a 3293__PACKAGE__->register_method({
afdb31d5
DM
3294 name => 'monitor',
3295 path => '{vmid}/monitor',
91c94f0a
DM
3296 method => 'POST',
3297 protected => 1,
3298 proxyto => 'node',
3299 description => "Execute Qemu monitor commands.",
a0d1b1a2 3300 permissions => {
a8f2f427 3301 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 3302 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 3303 },
91c94f0a
DM
3304 parameters => {
3305 additionalProperties => 0,
3306 properties => {
3307 node => get_standard_option('pve-node'),
3308 vmid => get_standard_option('pve-vmid'),
3309 command => {
3310 type => 'string',
3311 description => "The monitor command.",
3312 }
3313 },
3314 },
3315 returns => { type => 'string'},
3316 code => sub {
3317 my ($param) = @_;
3318
a8f2f427
FG
3319 my $rpcenv = PVE::RPCEnvironment::get();
3320 my $authuser = $rpcenv->get_user();
3321
3322 my $is_ro = sub {
3323 my $command = shift;
3324 return $command =~ m/^\s*info(\s+|$)/
3325 || $command =~ m/^\s*help\s*$/;
3326 };
3327
3328 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
3329 if !&$is_ro($param->{command});
3330
91c94f0a
DM
3331 my $vmid = $param->{vmid};
3332
ffda963f 3333 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
3334
3335 my $res = '';
3336 eval {
7b7c6d1b 3337 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
91c94f0a
DM
3338 };
3339 $res = "ERROR: $@" if $@;
3340
3341 return $res;
3342 }});
3343
0d02881c
AD
3344__PACKAGE__->register_method({
3345 name => 'resize_vm',
614e3941 3346 path => '{vmid}/resize',
0d02881c
AD
3347 method => 'PUT',
3348 protected => 1,
3349 proxyto => 'node',
2f48a4f5 3350 description => "Extend volume size.",
0d02881c 3351 permissions => {
3b2773f6 3352 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
3353 },
3354 parameters => {
3355 additionalProperties => 0,
2f48a4f5
DM
3356 properties => {
3357 node => get_standard_option('pve-node'),
335af808 3358 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
3359 skiplock => get_standard_option('skiplock'),
3360 disk => {
3361 type => 'string',
3362 description => "The disk you want to resize.",
74479ee9 3363 enum => [PVE::QemuServer::valid_drive_names()],
2f48a4f5
DM
3364 },
3365 size => {
3366 type => 'string',
f91b2e45 3367 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 3368 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
3369 },
3370 digest => {
3371 type => 'string',
3372 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3373 maxLength => 40,
3374 optional => 1,
3375 },
3376 },
0d02881c
AD
3377 },
3378 returns => { type => 'null'},
3379 code => sub {
3380 my ($param) = @_;
3381
3382 my $rpcenv = PVE::RPCEnvironment::get();
3383
3384 my $authuser = $rpcenv->get_user();
3385
3386 my $node = extract_param($param, 'node');
3387
3388 my $vmid = extract_param($param, 'vmid');
3389
3390 my $digest = extract_param($param, 'digest');
3391
2f48a4f5 3392 my $disk = extract_param($param, 'disk');
75466c4f 3393
2f48a4f5 3394 my $sizestr = extract_param($param, 'size');
0d02881c 3395
f91b2e45 3396 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
3397 raise_param_exc({ skiplock => "Only root may use this option." })
3398 if $skiplock && $authuser ne 'root@pam';
3399
0d02881c
AD
3400 my $storecfg = PVE::Storage::config();
3401
0d02881c
AD
3402 my $updatefn = sub {
3403
ffda963f 3404 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
3405
3406 die "checksum missmatch (file change by other user?)\n"
3407 if $digest && $digest ne $conf->{digest};
ffda963f 3408 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 3409
f91b2e45
DM
3410 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3411
3412 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3413
d662790a
WL
3414 my (undef, undef, undef, undef, undef, undef, $format) =
3415 PVE::Storage::parse_volname($storecfg, $drive->{file});
3416
3417 die "can't resize volume: $disk if snapshot exists\n"
3418 if %{$conf->{snapshots}} && $format eq 'qcow2';
3419
f91b2e45
DM
3420 my $volid = $drive->{file};
3421
3422 die "disk '$disk' has no associated volume\n" if !$volid;
3423
3424 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3425
3426 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3427
3428 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3429
b572a606 3430 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
3431 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3432
3433 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3434 my ($ext, $newsize, $unit) = ($1, $2, $4);
3435 if ($unit) {
3436 if ($unit eq 'K') {
3437 $newsize = $newsize * 1024;
3438 } elsif ($unit eq 'M') {
3439 $newsize = $newsize * 1024 * 1024;
3440 } elsif ($unit eq 'G') {
3441 $newsize = $newsize * 1024 * 1024 * 1024;
3442 } elsif ($unit eq 'T') {
3443 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3444 }
3445 }
3446 $newsize += $size if $ext;
3447 $newsize = int($newsize);
3448
9a478b17 3449 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
3450
3451 return if $size == $newsize;
3452
2f48a4f5 3453 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 3454
f91b2e45 3455 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 3456
f91b2e45
DM
3457 $drive->{size} = $newsize;
3458 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3459
ffda963f 3460 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 3461 };
0d02881c 3462
ffda963f 3463 PVE::QemuConfig->lock_config($vmid, $updatefn);
0d02881c
AD
3464 return undef;
3465 }});
3466
9dbd1ee4 3467__PACKAGE__->register_method({
7e7d7b61 3468 name => 'snapshot_list',
9dbd1ee4 3469 path => '{vmid}/snapshot',
7e7d7b61
DM
3470 method => 'GET',
3471 description => "List all snapshots.",
3472 permissions => {
3473 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3474 },
3475 proxyto => 'node',
3476 protected => 1, # qemu pid files are only readable by root
3477 parameters => {
3478 additionalProperties => 0,
3479 properties => {
e261de40 3480 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
3481 node => get_standard_option('pve-node'),
3482 },
3483 },
3484 returns => {
3485 type => 'array',
3486 items => {
3487 type => "object",
ce9b0a38
DM
3488 properties => {
3489 name => {
3490 description => "Snapshot identifier. Value 'current' identifies the current VM.",
3491 type => 'string',
3492 },
3493 vmstate => {
3494 description => "Snapshot includes RAM.",
3495 type => 'boolean',
3496 optional => 1,
3497 },
3498 description => {
3499 description => "Snapshot description.",
3500 type => 'string',
3501 },
3502 snaptime => {
3503 description => "Snapshot creation time",
3504 type => 'integer',
3505 renderer => 'timestamp',
3506 optional => 1,
3507 },
3508 parent => {
3509 description => "Parent snapshot identifier.",
3510 type => 'string',
3511 optional => 1,
3512 },
3513 },
7e7d7b61
DM
3514 },
3515 links => [ { rel => 'child', href => "{name}" } ],
3516 },
3517 code => sub {
3518 my ($param) = @_;
3519
6aa4651b
DM
3520 my $vmid = $param->{vmid};
3521
ffda963f 3522 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
3523 my $snaphash = $conf->{snapshots} || {};
3524
3525 my $res = [];
3526
3527 foreach my $name (keys %$snaphash) {
0ea6bc69 3528 my $d = $snaphash->{$name};
75466c4f
DM
3529 my $item = {
3530 name => $name,
3531 snaptime => $d->{snaptime} || 0,
6aa4651b 3532 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
3533 description => $d->{description} || '',
3534 };
0ea6bc69 3535 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 3536 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
3537 push @$res, $item;
3538 }
3539
6aa4651b 3540 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
3541 my $current = {
3542 name => 'current',
3543 digest => $conf->{digest},
3544 running => $running,
3545 description => "You are here!",
3546 };
d1914468
DM
3547 $current->{parent} = $conf->{parent} if $conf->{parent};
3548
3549 push @$res, $current;
7e7d7b61
DM
3550
3551 return $res;
3552 }});
3553
3554__PACKAGE__->register_method({
3555 name => 'snapshot',
3556 path => '{vmid}/snapshot',
3557 method => 'POST',
9dbd1ee4
AD
3558 protected => 1,
3559 proxyto => 'node',
3560 description => "Snapshot a VM.",
3561 permissions => {
f1baf1df 3562 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
3563 },
3564 parameters => {
3565 additionalProperties => 0,
3566 properties => {
3567 node => get_standard_option('pve-node'),
335af808 3568 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3569 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
3570 vmstate => {
3571 optional => 1,
3572 type => 'boolean',
3573 description => "Save the vmstate",
3574 },
782f4f75
DM
3575 description => {
3576 optional => 1,
3577 type => 'string',
3578 description => "A textual description or comment.",
3579 },
9dbd1ee4
AD
3580 },
3581 },
7e7d7b61
DM
3582 returns => {
3583 type => 'string',
3584 description => "the task ID.",
3585 },
9dbd1ee4
AD
3586 code => sub {
3587 my ($param) = @_;
3588
3589 my $rpcenv = PVE::RPCEnvironment::get();
3590
3591 my $authuser = $rpcenv->get_user();
3592
3593 my $node = extract_param($param, 'node');
3594
3595 my $vmid = extract_param($param, 'vmid');
3596
9dbd1ee4
AD
3597 my $snapname = extract_param($param, 'snapname');
3598
d1914468
DM
3599 die "unable to use snapshot name 'current' (reserved name)\n"
3600 if $snapname eq 'current';
3601
7e7d7b61 3602 my $realcmd = sub {
22c377f0 3603 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
b2c9558d 3604 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 3605 $param->{description});
7e7d7b61
DM
3606 };
3607
3608 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3609 }});
3610
154ccdcd
DM
3611__PACKAGE__->register_method({
3612 name => 'snapshot_cmd_idx',
3613 path => '{vmid}/snapshot/{snapname}',
3614 description => '',
3615 method => 'GET',
3616 permissions => {
3617 user => 'all',
3618 },
3619 parameters => {
3620 additionalProperties => 0,
3621 properties => {
3622 vmid => get_standard_option('pve-vmid'),
3623 node => get_standard_option('pve-node'),
8abd398b 3624 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
3625 },
3626 },
3627 returns => {
3628 type => 'array',
3629 items => {
3630 type => "object",
3631 properties => {},
3632 },
3633 links => [ { rel => 'child', href => "{cmd}" } ],
3634 },
3635 code => sub {
3636 my ($param) = @_;
3637
3638 my $res = [];
3639
3640 push @$res, { cmd => 'rollback' };
d788cea6 3641 push @$res, { cmd => 'config' };
154ccdcd
DM
3642
3643 return $res;
3644 }});
3645
d788cea6
DM
3646__PACKAGE__->register_method({
3647 name => 'update_snapshot_config',
3648 path => '{vmid}/snapshot/{snapname}/config',
3649 method => 'PUT',
3650 protected => 1,
3651 proxyto => 'node',
3652 description => "Update snapshot metadata.",
3653 permissions => {
3654 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3655 },
3656 parameters => {
3657 additionalProperties => 0,
3658 properties => {
3659 node => get_standard_option('pve-node'),
3660 vmid => get_standard_option('pve-vmid'),
3661 snapname => get_standard_option('pve-snapshot-name'),
3662 description => {
3663 optional => 1,
3664 type => 'string',
3665 description => "A textual description or comment.",
3666 },
3667 },
3668 },
3669 returns => { type => 'null' },
3670 code => sub {
3671 my ($param) = @_;
3672
3673 my $rpcenv = PVE::RPCEnvironment::get();
3674
3675 my $authuser = $rpcenv->get_user();
3676
3677 my $vmid = extract_param($param, 'vmid');
3678
3679 my $snapname = extract_param($param, 'snapname');
3680
3681 return undef if !defined($param->{description});
3682
3683 my $updatefn = sub {
3684
ffda963f 3685 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 3686
ffda963f 3687 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
3688
3689 my $snap = $conf->{snapshots}->{$snapname};
3690
75466c4f
DM
3691 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3692
d788cea6
DM
3693 $snap->{description} = $param->{description} if defined($param->{description});
3694
ffda963f 3695 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
3696 };
3697
ffda963f 3698 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6
DM
3699
3700 return undef;
3701 }});
3702
3703__PACKAGE__->register_method({
3704 name => 'get_snapshot_config',
3705 path => '{vmid}/snapshot/{snapname}/config',
3706 method => 'GET',
3707 proxyto => 'node',
3708 description => "Get snapshot configuration",
3709 permissions => {
c268337d 3710 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
d788cea6
DM
3711 },
3712 parameters => {
3713 additionalProperties => 0,
3714 properties => {
3715 node => get_standard_option('pve-node'),
3716 vmid => get_standard_option('pve-vmid'),
3717 snapname => get_standard_option('pve-snapshot-name'),
3718 },
3719 },
3720 returns => { type => "object" },
3721 code => sub {
3722 my ($param) = @_;
3723
3724 my $rpcenv = PVE::RPCEnvironment::get();
3725
3726 my $authuser = $rpcenv->get_user();
3727
3728 my $vmid = extract_param($param, 'vmid');
3729
3730 my $snapname = extract_param($param, 'snapname');
3731
ffda963f 3732 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
3733
3734 my $snap = $conf->{snapshots}->{$snapname};
3735
75466c4f
DM
3736 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3737
d788cea6
DM
3738 return $snap;
3739 }});
3740
7e7d7b61
DM
3741__PACKAGE__->register_method({
3742 name => 'rollback',
154ccdcd 3743 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
3744 method => 'POST',
3745 protected => 1,
3746 proxyto => 'node',
3747 description => "Rollback VM state to specified snapshot.",
3748 permissions => {
c268337d 3749 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
3750 },
3751 parameters => {
3752 additionalProperties => 0,
3753 properties => {
3754 node => get_standard_option('pve-node'),
335af808 3755 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3756 snapname => get_standard_option('pve-snapshot-name'),
7e7d7b61
DM
3757 },
3758 },
3759 returns => {
3760 type => 'string',
3761 description => "the task ID.",
3762 },
3763 code => sub {
3764 my ($param) = @_;
3765
3766 my $rpcenv = PVE::RPCEnvironment::get();
3767
3768 my $authuser = $rpcenv->get_user();
3769
3770 my $node = extract_param($param, 'node');
3771
3772 my $vmid = extract_param($param, 'vmid');
3773
3774 my $snapname = extract_param($param, 'snapname');
3775
7e7d7b61 3776 my $realcmd = sub {
22c377f0 3777 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 3778 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
7e7d7b61
DM
3779 };
3780
c068c1c3
WL
3781 my $worker = sub {
3782 # hold migration lock, this makes sure that nobody create replication snapshots
3783 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3784 };
3785
3786 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
3787 }});
3788
3789__PACKAGE__->register_method({
3790 name => 'delsnapshot',
3791 path => '{vmid}/snapshot/{snapname}',
3792 method => 'DELETE',
3793 protected => 1,
3794 proxyto => 'node',
3795 description => "Delete a VM snapshot.",
3796 permissions => {
f1baf1df 3797 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
3798 },
3799 parameters => {
3800 additionalProperties => 0,
3801 properties => {
3802 node => get_standard_option('pve-node'),
335af808 3803 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 3804 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
3805 force => {
3806 optional => 1,
3807 type => 'boolean',
3808 description => "For removal from config file, even if removing disk snapshots fails.",
3809 },
7e7d7b61
DM
3810 },
3811 },
3812 returns => {
3813 type => 'string',
3814 description => "the task ID.",
3815 },
3816 code => sub {
3817 my ($param) = @_;
3818
3819 my $rpcenv = PVE::RPCEnvironment::get();
3820
3821 my $authuser = $rpcenv->get_user();
3822
3823 my $node = extract_param($param, 'node');
3824
3825 my $vmid = extract_param($param, 'vmid');
3826
3827 my $snapname = extract_param($param, 'snapname');
3828
7e7d7b61 3829 my $realcmd = sub {
22c377f0 3830 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 3831 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 3832 };
9dbd1ee4 3833
7b2257a8 3834 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
3835 }});
3836
04a69bb4
AD
3837__PACKAGE__->register_method({
3838 name => 'template',
3839 path => '{vmid}/template',
3840 method => 'POST',
3841 protected => 1,
3842 proxyto => 'node',
3843 description => "Create a Template.",
b02691d8 3844 permissions => {
7af0a6c8
DM
3845 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3846 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 3847 },
04a69bb4
AD
3848 parameters => {
3849 additionalProperties => 0,
3850 properties => {
3851 node => get_standard_option('pve-node'),
335af808 3852 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
3853 disk => {
3854 optional => 1,
3855 type => 'string',
3856 description => "If you want to convert only 1 disk to base image.",
74479ee9 3857 enum => [PVE::QemuServer::valid_drive_names()],
04a69bb4
AD
3858 },
3859
3860 },
3861 },
3862 returns => { type => 'null'},
3863 code => sub {
3864 my ($param) = @_;
3865
3866 my $rpcenv = PVE::RPCEnvironment::get();
3867
3868 my $authuser = $rpcenv->get_user();
3869
3870 my $node = extract_param($param, 'node');
3871
3872 my $vmid = extract_param($param, 'vmid');
3873
3874 my $disk = extract_param($param, 'disk');
3875
3876 my $updatefn = sub {
3877
ffda963f 3878 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 3879
ffda963f 3880 PVE::QemuConfig->check_lock($conf);
04a69bb4 3881
75466c4f 3882 die "unable to create template, because VM contains snapshots\n"
b91c2aae 3883 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 3884
75466c4f 3885 die "you can't convert a template to a template\n"
ffda963f 3886 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 3887
75466c4f 3888 die "you can't convert a VM to template if VM is running\n"
218cab9a 3889 if PVE::QemuServer::check_running($vmid);
35c5fdef 3890
04a69bb4
AD
3891 my $realcmd = sub {
3892 PVE::QemuServer::template_create($vmid, $conf, $disk);
3893 };
04a69bb4 3894
75e7e997 3895 $conf->{template} = 1;
ffda963f 3896 PVE::QemuConfig->write_config($vmid, $conf);
75e7e997
DM
3897
3898 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
3899 };
3900
ffda963f 3901 PVE::QemuConfig->lock_config($vmid, $updatefn);
04a69bb4
AD
3902 return undef;
3903 }});
3904
1e3baf05 39051;