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