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