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