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