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