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