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