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