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