1 package PVE
::API2
::Qemu
;
12 use Crypt
::OpenSSL
::Random
;
13 use Socket
qw(SOCK_STREAM);
15 use PVE
::APIClient
::LWP
;
17 use PVE
::Cluster qw
(cfs_read_file cfs_write_file
);;
20 use PVE
::Tools
qw(extract_param);
21 use PVE
::Exception
qw(raise raise_param_exc raise_perm_exc);
23 use PVE
::JSONSchema
qw(get_standard_option);
25 use PVE
::ReplicationConfig
;
26 use PVE
::GuestHelpers
qw(assert_tag_permissions);
29 use PVE
::QemuServer
::Cloudinit
;
30 use PVE
::QemuServer
::CPUConfig
;
31 use PVE
::QemuServer
::Drive
;
32 use PVE
::QemuServer
::ImportDisk
;
33 use PVE
::QemuServer
::Monitor
qw(mon_cmd);
34 use PVE
::QemuServer
::Machine
;
35 use PVE
::QemuServer
::Memory
qw(get_current_memory);
36 use PVE
::QemuServer
::PCI
;
37 use PVE
::QemuServer
::USB
;
39 use PVE
::RPCEnvironment
;
40 use PVE
::AccessControl
;
44 use PVE
::API2
::Firewall
::VM
;
45 use PVE
::API2
::Qemu
::Agent
;
46 use PVE
::VZDump
::Plugin
;
47 use PVE
::DataCenterConfig
;
50 use PVE
::StorageTunnel
;
53 if (!$ENV{PVE_GENERATING_DOCS
}) {
54 require PVE
::HA
::Env
::PVE2
;
55 import PVE
::HA
::Env
::PVE2
;
56 require PVE
::HA
::Config
;
57 import PVE
::HA
::Config
;
61 use base
qw(PVE::RESTHandler);
63 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.";
65 my $resolve_cdrom_alias = sub {
68 if (my $value = $param->{cdrom
}) {
69 $value .= ",media=cdrom" if $value !~ m/media=/;
70 $param->{ide2
} = $value;
71 delete $param->{cdrom
};
75 # Used in import-enabled API endpoints. Parses drives using the extended '_with_alloc' schema.
76 my $foreach_volume_with_alloc = sub {
77 my ($param, $func) = @_;
79 for my $opt (sort keys $param->%*) {
80 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
82 my $drive = PVE
::QemuServer
::Drive
::parse_drive
($opt, $param->{$opt}, 1);
85 $func->($opt, $drive);
89 my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
91 my $check_drive_param = sub {
92 my ($param, $storecfg, $extra_checks) = @_;
94 for my $opt (sort keys $param->%*) {
95 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
97 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
98 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
100 if ($drive->{'import-from'}) {
101 if ($drive->{file
} !~ $NEW_DISK_RE || $3 != 0) {
103 $opt => "'import-from' requires special syntax - ".
104 "use <storage ID>:0,import-from=<source>",
108 if ($opt eq 'efidisk0') {
109 for my $required (qw(efitype pre-enrolled-keys)) {
110 if (!defined($drive->{$required})) {
112 $opt => "need to specify '$required' when using 'import-from'",
116 } elsif ($opt eq 'tpmstate0') {
117 raise_param_exc
({ $opt => "need to specify 'version' when using 'import-from'" })
118 if !defined($drive->{version
});
122 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
124 $extra_checks->($drive) if $extra_checks;
126 $param->{$opt} = PVE
::QemuServer
::print_drive
($drive, 1);
130 my $check_storage_access = sub {
131 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
133 $foreach_volume_with_alloc->($settings, sub {
134 my ($ds, $drive) = @_;
136 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
138 my $volid = $drive->{file
};
139 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
141 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit' || (defined($volname) && $volname eq 'cloudinit'))) {
143 } elsif ($isCDROM && ($volid eq 'cdrom')) {
144 $rpcenv->check($authuser, "/", ['Sys.Console']);
145 } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
146 my ($storeid, $size) = ($2 || $default_storage, $3);
147 die "no storage ID specified (and no default storage)\n" if !$storeid;
148 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
149 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
150 raise_param_exc
({ storage
=> "storage '$storeid' does not support vm images"})
151 if !$scfg->{content
}->{images
};
153 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
155 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
156 raise_param_exc
({ $ds => "content type needs to be 'images' or 'iso'" })
157 if $vtype ne 'images' && $vtype ne 'iso';
161 if (my $src_image = $drive->{'import-from'}) {
163 if (PVE
::Storage
::parse_volume_id
($src_image, 1)) { # PVE-managed volume
164 (my $vtype, undef, $src_vmid) = PVE
::Storage
::parse_volname
($storecfg, $src_image);
165 raise_param_exc
({ $ds => "$src_image has wrong type '$vtype' - not an image" })
166 if $vtype ne 'images';
169 if ($src_vmid) { # might be actively used by VM and will be copied via clone_disk()
170 $rpcenv->check($authuser, "/vms/${src_vmid}", ['VM.Clone']);
172 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $src_image);
177 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
178 if defined($settings->{vmstatestorage
});
181 my $check_storage_access_clone = sub {
182 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
186 PVE
::QemuConfig-
>foreach_volume($conf, sub {
187 my ($ds, $drive) = @_;
189 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
191 my $volid = $drive->{file
};
193 return if !$volid || $volid eq 'none';
196 if ($volid eq 'cdrom') {
197 $rpcenv->check($authuser, "/", ['Sys.Console']);
199 # we simply allow access
200 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
201 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
202 $sharedvm = 0 if !$scfg->{shared
};
206 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
207 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
208 $sharedvm = 0 if !$scfg->{shared
};
210 $sid = $storage if $storage;
211 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
215 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
216 if defined($conf->{vmstatestorage
});
221 my $check_storage_access_migrate = sub {
222 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
224 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $node);
226 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
228 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
229 die "storage '$storage' does not support vm images\n"
230 if !$scfg->{content
}->{images
};
233 my $import_from_volid = sub {
234 my ($storecfg, $src_volid, $dest_info, $vollist) = @_;
236 die "could not get size of $src_volid\n"
237 if !PVE
::Storage
::volume_size_info
($storecfg, $src_volid, 10);
239 die "cannot import from cloudinit disk\n"
240 if PVE
::QemuServer
::Drive
::drive_is_cloudinit
({ file
=> $src_volid });
242 my $src_vmid = (PVE
::Storage
::parse_volname
($storecfg, $src_volid))[2];
244 my $src_vm_state = sub {
245 my $exists = $src_vmid && PVE
::Cluster
::get_vmlist
()->{ids
}->{$src_vmid} ?
1 : 0;
249 eval { PVE
::QemuConfig
::assert_config_exists_on_node
($src_vmid); };
250 die "owner VM $src_vmid not on local node\n" if $@;
251 $runs = PVE
::QemuServer
::Helpers
::vm_running_locally
($src_vmid) || 0;
254 return ($exists, $runs);
257 my ($src_vm_exists, $running) = $src_vm_state->();
259 die "cannot import from '$src_volid' - full clone feature is not supported\n"
260 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $src_volid, undef, $running);
263 my ($src_vm_exists_now, $running_now) = $src_vm_state->();
265 die "owner VM $src_vmid changed state unexpectedly\n"
266 if $src_vm_exists_now != $src_vm_exists || $running_now != $running;
268 my $src_conf = $src_vm_exists_now ? PVE
::QemuConfig-
>load_config($src_vmid) : {};
270 my $src_drive = { file
=> $src_volid };
272 PVE
::QemuConfig-
>foreach_volume($src_conf, sub {
273 my ($ds, $drive) = @_;
275 return if $src_drivename;
277 if ($drive->{file
} eq $src_volid) {
279 $src_drivename = $ds;
285 running
=> $running_now,
286 drivename
=> $src_drivename,
291 my ($src_storeid) = PVE
::Storage
::parse_volume_id
($src_volid);
293 return PVE
::QemuServer
::clone_disk
(
302 PVE
::Storage
::get_bandwidth_limit
('clone', [$src_storeid, $dest_info->{storage
}]),
308 $cloned = PVE
::QemuConfig-
>lock_config_full($src_vmid, 30, $clonefn);
309 } elsif ($src_vmid) {
310 $cloned = PVE
::QemuConfig-
>lock_config_shared($src_vmid, 30, $clonefn);
312 $cloned = $clonefn->();
315 return $cloned->@{qw(file size)};
318 # Note: $pool is only needed when creating a VM, because pool permissions
319 # are automatically inherited if VM already exists inside a pool.
320 my $create_disks = sub {
321 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
328 my ($ds, $disk) = @_;
330 my $volid = $disk->{file
};
331 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
333 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
334 delete $disk->{size
};
335 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
336 } elsif (defined($volname) && $volname eq 'cloudinit') {
337 $storeid = $storeid // $default_storage;
338 die "no storage ID specified (and no default storage)\n" if !$storeid;
341 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
342 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
343 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
345 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
348 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
349 my $name = "vm-$vmid-cloudinit";
353 $fmt = $disk->{format
} // "qcow2";
356 $fmt = $disk->{format
} // "raw";
359 # Initial disk created with 4 MB and aligned to 4MB on regeneration
360 my $ci_size = PVE
::QemuServer
::Cloudinit
::CLOUDINIT_DISK_SIZE
;
361 my $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
362 $disk->{file
} = $volid;
363 $disk->{media
} = 'cdrom';
364 push @$vollist, $volid;
365 delete $disk->{format
}; # no longer needed
366 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
367 print "$ds: successfully created disk '$res->{$ds}'\n";
368 } elsif ($volid =~ $NEW_DISK_RE) {
369 my ($storeid, $size) = ($2 || $default_storage, $3);
370 die "no storage ID specified (and no default storage)\n" if !$storeid;
372 if (my $source = delete $disk->{'import-from'}) {
375 if (PVE
::Storage
::parse_volume_id
($source, 1)) { # PVE-managed volume
380 format
=> $disk->{format
},
383 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf, $disk)
384 if $ds eq 'efidisk0';
386 ($dst_volid, $size) = eval {
387 $import_from_volid->($storecfg, $source, $dest_info, $vollist);
389 die "cannot import from '$source' - $@" if $@;
391 $source = PVE
::Storage
::abs_filesystem_path
($storecfg, $source, 1);
392 $size = PVE
::Storage
::file_size_info
($source);
393 die "could not get file size of $source\n" if !$size;
395 (undef, $dst_volid) = PVE
::QemuServer
::ImportDisk
::do_import
(
401 format
=> $disk->{format
},
402 'skip-config-update' => 1,
405 push @$vollist, $dst_volid;
408 $disk->{file
} = $dst_volid;
409 $disk->{size
} = $size;
410 delete $disk->{format
}; # no longer needed
411 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
413 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
414 my $fmt = $disk->{format
} || $defformat;
416 $size = PVE
::Tools
::convert_size
($size, 'gb' => 'kb'); # vdisk_alloc uses kb
419 if ($ds eq 'efidisk0') {
420 my $smm = PVE
::QemuServer
::Machine
::machine_type_is_q35
($conf);
421 ($volid, $size) = PVE
::QemuServer
::create_efidisk
(
422 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
423 } elsif ($ds eq 'tpmstate0') {
424 # swtpm can only use raw volumes, and uses a fixed size
425 $size = PVE
::Tools
::convert_size
(PVE
::QemuServer
::Drive
::TPMSTATE_DISK_SIZE
, 'b' => 'kb');
426 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, "raw", undef, $size);
428 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, undef, $size);
430 push @$vollist, $volid;
431 $disk->{file
} = $volid;
432 $disk->{size
} = PVE
::Tools
::convert_size
($size, 'kb' => 'b');
433 delete $disk->{format
}; # no longer needed
434 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
437 print "$ds: successfully created disk '$res->{$ds}'\n";
439 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
441 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
442 die "cannot use volume $volid - content type needs to be 'images' or 'iso'"
443 if $vtype ne 'images' && $vtype ne 'iso';
445 if (PVE
::QemuServer
::Drive
::drive_is_cloudinit
($disk)) {
447 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
448 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
449 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
451 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
456 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
458 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
459 die "volume $volid does not exist\n" if !$size;
460 $disk->{size
} = $size;
462 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
466 eval { $foreach_volume_with_alloc->($settings, $code); };
468 # free allocated images on error
470 syslog
('err', "VM $vmid creating disks failed");
471 foreach my $volid (@$vollist) {
472 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
478 return ($vollist, $res);
481 my $check_cpu_model_access = sub {
482 my ($rpcenv, $authuser, $new, $existing) = @_;
484 return if !defined($new->{cpu
});
486 my $cpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $new->{cpu
});
487 return if !$cpu || !$cpu->{cputype
}; # always allow default
488 my $cputype = $cpu->{cputype
};
490 if ($existing && $existing->{cpu
}) {
491 # changing only other settings doesn't require permissions for CPU model
492 my $existingCpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $existing->{cpu
});
493 return if $existingCpu->{cputype
} eq $cputype;
496 if (PVE
::QemuServer
::CPUConfig
::is_custom_model
($cputype)) {
497 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
512 my $memoryoptions = {
518 my $hwtypeoptions = {
531 my $generaloptions = {
538 'migrate_downtime' => 1,
539 'migrate_speed' => 1,
551 my $vmpoweroptions = {
558 'vmstatestorage' => 1,
561 my $cloudinitoptions = {
572 my $check_vm_create_serial_perm = sub {
573 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
575 return 1 if $authuser eq 'root@pam';
577 foreach my $opt (keys %{$param}) {
578 next if $opt !~ m/^serial\d+$/;
580 if ($param->{$opt} eq 'socket') {
581 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
583 die "only root can set '$opt' config for real devices\n";
590 my sub check_usb_perm
{
591 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
593 return 1 if $authuser eq 'root@pam';
595 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
597 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-usb', $value);
598 if ($device->{host
} && $device->{host
} !~ m/^spice$/i) {
599 die "only root can set '$opt' config for real devices\n";
600 } elsif ($device->{mapping
}) {
601 $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
603 die "either 'host' or 'mapping' must be set.\n";
609 my sub check_vm_create_usb_perm
{
610 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
612 return 1 if $authuser eq 'root@pam';
614 foreach my $opt (keys %{$param}) {
615 next if $opt !~ m/^usb\d+$/;
616 check_usb_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
622 my sub check_hostpci_perm
{
623 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
625 return 1 if $authuser eq 'root@pam';
627 my $device = PVE
::JSONSchema
::parse_property_string
('pve-qm-hostpci', $value);
628 if ($device->{host
}) {
629 die "only root can set '$opt' config for non-mapped devices\n";
630 } elsif ($device->{mapping
}) {
631 $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
632 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
634 die "either 'host' or 'mapping' must be set.\n";
640 my sub check_vm_create_hostpci_perm
{
641 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
643 return 1 if $authuser eq 'root@pam';
645 foreach my $opt (keys %{$param}) {
646 next if $opt !~ m/^hostpci\d+$/;
647 check_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
653 my $check_vm_modify_config_perm = sub {
654 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
656 return 1 if $authuser eq 'root@pam';
658 foreach my $opt (@$key_list) {
659 # some checks (e.g., disk, serial port, usb) need to be done somewhere
660 # else, as there the permission can be value dependend
661 next if PVE
::QemuServer
::is_valid_drivename
($opt);
662 next if $opt eq 'cdrom';
663 next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
664 next if $opt eq 'tags';
667 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
668 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
669 } elsif ($memoryoptions->{$opt}) {
670 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
671 } elsif ($hwtypeoptions->{$opt}) {
672 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
673 } elsif ($generaloptions->{$opt}) {
674 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
675 # special case for startup since it changes host behaviour
676 if ($opt eq 'startup') {
677 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
679 } elsif ($vmpoweroptions->{$opt}) {
680 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
681 } elsif ($diskoptions->{$opt}) {
682 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
683 } elsif ($opt =~ m/^net\d+$/) {
684 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
685 } elsif ($cloudinitoptions->{$opt} || $opt =~ m/^ipconfig\d+$/) {
686 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
687 } elsif ($opt eq 'vmstate') {
688 # the user needs Disk and PowerMgmt privileges to change the vmstate
689 # also needs privileges on the storage, that will be checked later
690 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
692 # catches args, lock, etc.
693 # new options will be checked here
694 die "only root can set '$opt' config\n";
701 __PACKAGE__-
>register_method({
705 description
=> "Virtual machine index (per node).",
707 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
711 protected
=> 1, # qemu pid files are only readable by root
713 additionalProperties
=> 0,
715 node
=> get_standard_option
('pve-node'),
719 description
=> "Determine the full status of active VMs.",
727 properties
=> $PVE::QemuServer
::vmstatus_return_properties
,
729 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
734 my $rpcenv = PVE
::RPCEnvironment
::get
();
735 my $authuser = $rpcenv->get_user();
737 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
740 foreach my $vmid (keys %$vmstatus) {
741 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
743 my $data = $vmstatus->{$vmid};
750 my $parse_restore_archive = sub {
751 my ($storecfg, $archive) = @_;
753 my ($archive_storeid, $archive_volname) = PVE
::Storage
::parse_volume_id
($archive, 1);
757 if (defined($archive_storeid)) {
758 my $scfg = PVE
::Storage
::storage_config
($storecfg, $archive_storeid);
759 $res->{volid
} = $archive;
760 if ($scfg->{type
} eq 'pbs') {
761 $res->{type
} = 'pbs';
765 my $path = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
766 $res->{type
} = 'file';
767 $res->{path
} = $path;
772 __PACKAGE__-
>register_method({
776 description
=> "Create or restore a virtual machine.",
778 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
779 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
780 "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
781 "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
782 user
=> 'all', # check inside
787 additionalProperties
=> 0,
788 properties
=> PVE
::QemuServer
::json_config_properties
(
790 node
=> get_standard_option
('pve-node'),
791 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
793 description
=> "The backup archive. Either the file system path to a .tar or .vma file (use '-' to pipe data from stdin) or a proxmox storage backup volume identifier.",
797 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
799 storage
=> get_standard_option
('pve-storage-id', {
800 description
=> "Default storage.",
802 completion
=> \
&PVE
::QemuServer
::complete_storage
,
807 description
=> "Allow to overwrite existing VM.",
808 requires
=> 'archive',
813 description
=> "Assign a unique random ethernet address.",
814 requires
=> 'archive',
819 description
=> "Start the VM immediately from the backup and restore in background. PBS only.",
820 requires
=> 'archive',
824 type
=> 'string', format
=> 'pve-poolid',
825 description
=> "Add the VM to the specified pool.",
828 description
=> "Override I/O bandwidth limit (in KiB/s).",
832 default => 'restore limit from datacenter or storage config',
838 description
=> "Start VM after it was created successfully.",
850 my $rpcenv = PVE
::RPCEnvironment
::get
();
851 my $authuser = $rpcenv->get_user();
853 my $node = extract_param
($param, 'node');
854 my $vmid = extract_param
($param, 'vmid');
856 my $archive = extract_param
($param, 'archive');
857 my $is_restore = !!$archive;
859 my $bwlimit = extract_param
($param, 'bwlimit');
860 my $force = extract_param
($param, 'force');
861 my $pool = extract_param
($param, 'pool');
862 my $start_after_create = extract_param
($param, 'start');
863 my $storage = extract_param
($param, 'storage');
864 my $unique = extract_param
($param, 'unique');
865 my $live_restore = extract_param
($param, 'live-restore');
867 if (defined(my $ssh_keys = $param->{sshkeys
})) {
868 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
869 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
872 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
873 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
875 PVE
::Cluster
::check_cfs_quorum
();
877 my $filename = PVE
::QemuConfig-
>config_file($vmid);
878 my $storecfg = PVE
::Storage
::config
();
880 if (defined($pool)) {
881 $rpcenv->check_pool_exist($pool);
884 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
885 if defined($storage);
887 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
889 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
891 } elsif ($archive && $force && (-f
$filename) &&
892 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
893 # OK: user has VM.Backup permissions and wants to restore an existing VM
899 for my $opt (sort keys $param->%*) {
900 if (PVE
::QemuServer
::Drive
::is_valid_drivename
($opt)) {
901 raise_param_exc
({ $opt => "option conflicts with option 'archive'" });
905 if ($archive eq '-') {
906 die "pipe requires cli environment\n" if $rpcenv->{type
} ne 'cli';
907 $archive = { type
=> 'pipe' };
909 PVE
::Storage
::check_volume_access
(
918 $archive = $parse_restore_archive->($storecfg, $archive);
922 if (scalar(keys $param->%*) > 0) {
923 &$resolve_cdrom_alias($param);
925 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
927 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
929 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
930 check_vm_create_usb_perm
($rpcenv, $authuser, $vmid, $pool, $param);
931 check_vm_create_hostpci_perm
($rpcenv, $authuser, $vmid, $pool, $param);
933 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
934 &$check_cpu_model_access($rpcenv, $authuser, $param);
936 $check_drive_param->($param, $storecfg);
938 PVE
::QemuServer
::add_random_macs
($param);
941 my $emsg = $is_restore ?
"unable to restore VM $vmid -" : "unable to create VM $vmid -";
943 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, $force) };
944 die "$emsg $@" if $@;
946 my $restored_data = 0;
947 my $restorefn = sub {
948 my $conf = PVE
::QemuConfig-
>load_config($vmid);
950 PVE
::QemuConfig-
>check_protection($conf, $emsg);
952 die "$emsg vm is running\n" if PVE
::QemuServer
::check_running
($vmid);
955 my $restore_options = {
960 live
=> $live_restore,
961 override_conf
=> $param,
963 if (my $volid = $archive->{volid
}) {
964 # best effort, real check is after restoring!
966 my $old_conf = PVE
::Storage
::extract_vzdump_config
($storecfg, $volid);
967 PVE
::QemuServer
::restore_merge_config
("backup/qemu-server/$vmid.conf", $old_conf, $param);
970 warn "Could not extract backed up config: $@\n";
971 warn "Skipping early checks!\n";
973 PVE
::QemuServer
::check_restore_permissions
($rpcenv, $authuser, $merged);
976 if ($archive->{type
} eq 'file' || $archive->{type
} eq 'pipe') {
977 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
979 PVE
::QemuServer
::restore_file_archive
($archive->{path
} // '-', $vmid, $authuser, $restore_options);
980 } elsif ($archive->{type
} eq 'pbs') {
981 PVE
::QemuServer
::restore_proxmox_backup_archive
($archive->{volid
}, $vmid, $authuser, $restore_options);
983 die "unknown backup archive type\n";
987 my $restored_conf = PVE
::QemuConfig-
>load_config($vmid);
988 # Convert restored VM to template if backup was VM template
989 if (PVE
::QemuConfig-
>is_template($restored_conf)) {
990 warn "Convert to template.\n";
991 eval { PVE
::QemuServer
::template_create
($vmid, $restored_conf) };
996 # ensure no old replication state are exists
997 PVE
::ReplicationState
::delete_guest_states
($vmid);
999 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1001 if ($start_after_create && !$live_restore) {
1002 print "Execute autostart\n";
1003 eval { PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node }) };
1008 my $createfn = sub {
1009 # ensure no old replication state are exists
1010 PVE
::ReplicationState
::delete_guest_states
($vmid);
1014 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1016 $conf->{meta
} = PVE
::QemuServer
::new_meta_info_string
();
1020 ($vollist, my $created_opts) = $create_disks->(
1031 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1033 if (!$conf->{boot
}) {
1034 my $devs = PVE
::QemuServer
::get_default_bootdevices
($conf);
1035 $conf->{boot
} = PVE
::QemuServer
::print_bootorder
($devs);
1038 # auto generate uuid if user did not specify smbios1 option
1039 if (!$conf->{smbios1
}) {
1040 $conf->{smbios1
} = PVE
::QemuServer
::generate_smbios1_uuid
();
1043 if ((!defined($conf->{vmgenid
}) || $conf->{vmgenid
} eq '1') && $arch ne 'aarch64') {
1044 $conf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
1047 my $machine = $conf->{machine
};
1048 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
1049 # always pin Windows' machine version on create, they get to easily confused
1050 if (PVE
::QemuServer
::Helpers
::windows_version
($conf->{ostype
})) {
1051 $conf->{machine
} = PVE
::QemuServer
::windows_get_pinned_machine_version
($machine);
1055 PVE
::QemuConfig-
>write_config($vmid, $conf);
1061 foreach my $volid (@$vollist) {
1062 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
1068 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
1071 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1073 if ($start_after_create) {
1074 print "Execute autostart\n";
1075 eval { PVE
::API2
::Qemu-
>vm_start({vmid
=> $vmid, node
=> $node}) };
1080 my ($code, $worker_name);
1082 $worker_name = 'qmrestore';
1084 eval { $restorefn->() };
1086 eval { PVE
::QemuConfig-
>remove_lock($vmid, 'create') };
1088 if ($restored_data) {
1089 warn "error after data was restored, VM disks should be OK but config may "
1090 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1092 warn "error before or during data restore, some or all disks were not "
1093 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1099 $worker_name = 'qmcreate';
1101 eval { $createfn->() };
1104 my $conffile = PVE
::QemuConfig-
>config_file($vmid);
1105 unlink($conffile) or die "failed to remove config file: $!\n";
1113 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1116 __PACKAGE__-
>register_method({
1121 description
=> "Directory index",
1126 additionalProperties
=> 0,
1128 node
=> get_standard_option
('pve-node'),
1129 vmid
=> get_standard_option
('pve-vmid'),
1137 subdir
=> { type
=> 'string' },
1140 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1146 { subdir
=> 'config' },
1147 { subdir
=> 'cloudinit' },
1148 { subdir
=> 'pending' },
1149 { subdir
=> 'status' },
1150 { subdir
=> 'unlink' },
1151 { subdir
=> 'vncproxy' },
1152 { subdir
=> 'termproxy' },
1153 { subdir
=> 'migrate' },
1154 { subdir
=> 'resize' },
1155 { subdir
=> 'move' },
1156 { subdir
=> 'rrd' },
1157 { subdir
=> 'rrddata' },
1158 { subdir
=> 'monitor' },
1159 { subdir
=> 'agent' },
1160 { subdir
=> 'snapshot' },
1161 { subdir
=> 'spiceproxy' },
1162 { subdir
=> 'sendkey' },
1163 { subdir
=> 'firewall' },
1164 { subdir
=> 'mtunnel' },
1165 { subdir
=> 'remote_migrate' },
1171 __PACKAGE__-
>register_method ({
1172 subclass
=> "PVE::API2::Firewall::VM",
1173 path
=> '{vmid}/firewall',
1176 __PACKAGE__-
>register_method ({
1177 subclass
=> "PVE::API2::Qemu::Agent",
1178 path
=> '{vmid}/agent',
1181 __PACKAGE__-
>register_method({
1183 path
=> '{vmid}/rrd',
1185 protected
=> 1, # fixme: can we avoid that?
1187 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1189 description
=> "Read VM RRD statistics (returns PNG)",
1191 additionalProperties
=> 0,
1193 node
=> get_standard_option
('pve-node'),
1194 vmid
=> get_standard_option
('pve-vmid'),
1196 description
=> "Specify the time frame you are interested in.",
1198 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1201 description
=> "The list of datasources you want to display.",
1202 type
=> 'string', format
=> 'pve-configid-list',
1205 description
=> "The RRD consolidation function",
1207 enum
=> [ 'AVERAGE', 'MAX' ],
1215 filename
=> { type
=> 'string' },
1221 return PVE
::RRD
::create_rrd_graph
(
1222 "pve2-vm/$param->{vmid}", $param->{timeframe
},
1223 $param->{ds
}, $param->{cf
});
1227 __PACKAGE__-
>register_method({
1229 path
=> '{vmid}/rrddata',
1231 protected
=> 1, # fixme: can we avoid that?
1233 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1235 description
=> "Read VM RRD statistics",
1237 additionalProperties
=> 0,
1239 node
=> get_standard_option
('pve-node'),
1240 vmid
=> get_standard_option
('pve-vmid'),
1242 description
=> "Specify the time frame you are interested in.",
1244 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1247 description
=> "The RRD consolidation function",
1249 enum
=> [ 'AVERAGE', 'MAX' ],
1264 return PVE
::RRD
::create_rrd_data
(
1265 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
1269 __PACKAGE__-
>register_method({
1270 name
=> 'vm_config',
1271 path
=> '{vmid}/config',
1274 description
=> "Get the virtual machine configuration with pending configuration " .
1275 "changes applied. Set the 'current' parameter to get the current configuration instead.",
1277 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1280 additionalProperties
=> 0,
1282 node
=> get_standard_option
('pve-node'),
1283 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1285 description
=> "Get current values (instead of pending values).",
1290 snapshot
=> get_standard_option
('pve-snapshot-name', {
1291 description
=> "Fetch config values from given snapshot.",
1294 my ($cmd, $pname, $cur, $args) = @_;
1295 PVE
::QemuConfig-
>snapshot_list($args->[0]);
1301 description
=> "The VM configuration.",
1303 properties
=> PVE
::QemuServer
::json_config_properties
({
1306 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1313 raise_param_exc
({ snapshot
=> "cannot use 'snapshot' parameter with 'current'",
1314 current
=> "cannot use 'snapshot' parameter with 'current'"})
1315 if ($param->{snapshot
} && $param->{current
});
1318 if ($param->{snapshot
}) {
1319 $conf = PVE
::QemuConfig-
>load_snapshot_config($param->{vmid
}, $param->{snapshot
});
1321 $conf = PVE
::QemuConfig-
>load_current_config($param->{vmid
}, $param->{current
});
1323 $conf->{cipassword
} = '**********' if $conf->{cipassword
};
1328 __PACKAGE__-
>register_method({
1329 name
=> 'vm_pending',
1330 path
=> '{vmid}/pending',
1333 description
=> "Get the virtual machine configuration with both current and pending values.",
1335 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1338 additionalProperties
=> 0,
1340 node
=> get_standard_option
('pve-node'),
1341 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1350 description
=> "Configuration option name.",
1354 description
=> "Current value.",
1359 description
=> "Pending value.",
1364 description
=> "Indicates a pending delete request if present and not 0. " .
1365 "The value 2 indicates a force-delete request.",
1377 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1379 my $pending_delete_hash = PVE
::QemuConfig-
>parse_pending_delete($conf->{pending
}->{delete});
1381 $conf->{cipassword
} = '**********' if defined($conf->{cipassword
});
1382 $conf->{pending
}->{cipassword
} = '********** ' if defined($conf->{pending
}->{cipassword
});
1384 return PVE
::GuestHelpers
::config_with_pending_array
($conf, $pending_delete_hash);
1387 __PACKAGE__-
>register_method({
1388 name
=> 'cloudinit_pending',
1389 path
=> '{vmid}/cloudinit',
1392 description
=> "Get the cloudinit configuration with both current and pending values.",
1394 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1397 additionalProperties
=> 0,
1399 node
=> get_standard_option
('pve-node'),
1400 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1409 description
=> "Configuration option name.",
1413 description
=> "Value as it was used to generate the current cloudinit image.",
1418 description
=> "The new pending value.",
1423 description
=> "Indicates a pending delete request if present and not 0. ",
1435 my $vmid = $param->{vmid
};
1436 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1438 my $ci = $conf->{cloudinit
};
1440 $conf->{cipassword
} = '**********' if exists $conf->{cipassword
};
1441 $ci->{cipassword
} = '**********' if exists $ci->{cipassword
};
1445 # All the values that got added
1446 my $added = delete($ci->{added
}) // '';
1447 for my $key (PVE
::Tools
::split_list
($added)) {
1448 push @$res, { key
=> $key, pending
=> $conf->{$key} };
1451 # All already existing values (+ their new value, if it exists)
1452 for my $opt (keys %$cloudinitoptions) {
1453 next if !$conf->{$opt};
1454 next if $added =~ m/$opt/;
1459 if (my $pending = $ci->{$opt}) {
1460 $item->{value
} = $pending;
1461 $item->{pending
} = $conf->{$opt};
1463 $item->{value
} = $conf->{$opt},
1469 # Now, we'll find the deleted ones
1470 for my $opt (keys %$ci) {
1471 next if $conf->{$opt};
1472 push @$res, { key
=> $opt, delete => 1 };
1478 __PACKAGE__-
>register_method({
1479 name
=> 'cloudinit_update',
1480 path
=> '{vmid}/cloudinit',
1484 description
=> "Regenerate and change cloudinit config drive.",
1486 check
=> ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
1489 additionalProperties
=> 0,
1491 node
=> get_standard_option
('pve-node'),
1492 vmid
=> get_standard_option
('pve-vmid'),
1495 returns
=> { type
=> 'null' },
1499 my $rpcenv = PVE
::RPCEnvironment
::get
();
1500 my $authuser = $rpcenv->get_user();
1502 my $vmid = extract_param
($param, 'vmid');
1504 PVE
::QemuConfig-
>lock_config($vmid, sub {
1505 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1506 PVE
::QemuConfig-
>check_lock($conf);
1508 my $storecfg = PVE
::Storage
::config
();
1509 PVE
::QemuServer
::vmconfig_update_cloudinit_drive
($storecfg, $conf, $vmid);
1514 # POST/PUT {vmid}/config implementation
1516 # The original API used PUT (idempotent) an we assumed that all operations
1517 # are fast. But it turned out that almost any configuration change can
1518 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1519 # time to complete and have side effects (not idempotent).
1521 # The new implementation uses POST and forks a worker process. We added
1522 # a new option 'background_delay'. If specified we wait up to
1523 # 'background_delay' second for the worker task to complete. It returns null
1524 # if the task is finished within that time, else we return the UPID.
1526 my $update_vm_api = sub {
1527 my ($param, $sync) = @_;
1529 my $rpcenv = PVE
::RPCEnvironment
::get
();
1531 my $authuser = $rpcenv->get_user();
1533 my $node = extract_param
($param, 'node');
1535 my $vmid = extract_param
($param, 'vmid');
1537 my $digest = extract_param
($param, 'digest');
1539 my $background_delay = extract_param
($param, 'background_delay');
1541 my $skip_cloud_init = extract_param
($param, 'skip_cloud_init');
1543 if (defined(my $cipassword = $param->{cipassword
})) {
1544 # Same logic as in cloud-init (but with the regex fixed...)
1545 $param->{cipassword
} = PVE
::Tools
::encrypt_pw
($cipassword)
1546 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1549 my @paramarr = (); # used for log message
1550 foreach my $key (sort keys %$param) {
1551 my $value = $key eq 'cipassword' ?
'<hidden>' : $param->{$key};
1552 push @paramarr, "-$key", $value;
1555 my $skiplock = extract_param
($param, 'skiplock');
1556 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1557 if $skiplock && $authuser ne 'root@pam';
1559 my $delete_str = extract_param
($param, 'delete');
1561 my $revert_str = extract_param
($param, 'revert');
1563 my $force = extract_param
($param, 'force');
1565 if (defined(my $ssh_keys = $param->{sshkeys
})) {
1566 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
1567 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
1570 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
1571 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
1573 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1575 my $storecfg = PVE
::Storage
::config
();
1577 &$resolve_cdrom_alias($param);
1579 # now try to verify all parameters
1582 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
1583 if (!PVE
::QemuServer
::option_exists
($opt)) {
1584 raise_param_exc
({ revert
=> "unknown option '$opt'" });
1587 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1588 "-revert $opt' at the same time" })
1589 if defined($param->{$opt});
1591 $revert->{$opt} = 1;
1595 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
1596 $opt = 'ide2' if $opt eq 'cdrom';
1598 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1599 "-delete $opt' at the same time" })
1600 if defined($param->{$opt});
1602 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
1603 "-revert $opt' at the same time" })
1606 if (!PVE
::QemuServer
::option_exists
($opt)) {
1607 raise_param_exc
({ delete => "unknown option '$opt'" });
1613 my $repl_conf = PVE
::ReplicationConfig-
>new();
1614 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1615 my $check_replication = sub {
1617 return if !$is_replicated;
1618 my $volid = $drive->{file
};
1619 return if !$volid || !($drive->{replicate
}//1);
1620 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
1622 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
1623 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1624 if !defined($storeid);
1626 return if defined($volname) && $volname eq 'cloudinit';
1629 if ($volid =~ $NEW_DISK_RE) {
1631 $format = $drive->{format
} || PVE
::Storage
::storage_default_format
($storecfg, $storeid);
1633 $format = (PVE
::Storage
::parse_volname
($storecfg, $volid))[6];
1635 return if PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
1636 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
1637 return if $scfg->{shared
};
1638 die "cannot add non-replicatable volume to a replicated VM\n";
1641 $check_drive_param->($param, $storecfg, $check_replication);
1643 foreach my $opt (keys %$param) {
1644 if ($opt =~ m/^net(\d+)$/) {
1646 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
1647 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
1648 } elsif ($opt eq 'vmgenid') {
1649 if ($param->{$opt} eq '1') {
1650 $param->{$opt} = PVE
::QemuServer
::generate_uuid
();
1652 } elsif ($opt eq 'hookscript') {
1653 eval { PVE
::GuestHelpers
::check_hookscript
($param->{$opt}, $storecfg); };
1654 raise_param_exc
({ $opt => $@ }) if $@;
1658 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1660 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1662 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1664 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $param);
1666 my $updatefn = sub {
1668 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1670 die "checksum missmatch (file change by other user?)\n"
1671 if $digest && $digest ne $conf->{digest
};
1673 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1675 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1676 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1677 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1678 delete $conf->{lock}; # for check lock check, not written out
1679 push @delete, 'lock'; # this is the real deal to write it out
1681 push @delete, 'runningmachine' if $conf->{runningmachine
};
1682 push @delete, 'runningcpu' if $conf->{runningcpu
};
1685 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
1687 foreach my $opt (keys %$revert) {
1688 if (defined($conf->{$opt})) {
1689 $param->{$opt} = $conf->{$opt};
1690 } elsif (defined($conf->{pending
}->{$opt})) {
1695 if ($param->{memory
} || defined($param->{balloon
})) {
1697 my $memory = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
};
1698 my $maxmem = get_current_memory
($memory);
1699 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
1701 die "balloon value too large (must be smaller than assigned memory)\n"
1702 if $balloon && $balloon > $maxmem;
1705 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1709 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1711 # write updates to pending section
1713 my $modified = {}; # record what $option we modify
1716 if (my $boot = $conf->{boot
}) {
1717 my $bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $boot);
1718 @bootorder = PVE
::Tools
::split_list
($bootcfg->{order
}) if $bootcfg && $bootcfg->{order
};
1720 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1722 my $check_drive_perms = sub {
1723 my ($opt, $val) = @_;
1724 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val, 1);
1725 if (PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
1726 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1727 } elsif (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) { # CDROM
1728 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1730 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1735 foreach my $opt (@delete) {
1736 $modified->{$opt} = 1;
1737 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1739 # value of what we want to delete, independent if pending or not
1740 my $val = $conf->{$opt} // $conf->{pending
}->{$opt};
1741 if (!defined($val)) {
1742 warn "cannot delete '$opt' - not set in current configuration!\n";
1743 $modified->{$opt} = 0;
1746 my $is_pending_val = defined($conf->{pending
}->{$opt});
1747 delete $conf->{pending
}->{$opt};
1749 # remove from bootorder if necessary
1750 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1751 @bootorder = grep {$_ ne $opt} @bootorder;
1752 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1753 $modified->{boot
} = 1;
1756 if ($opt =~ m/^unused/) {
1757 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val);
1758 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1759 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1760 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1761 delete $conf->{$opt};
1762 PVE
::QemuConfig-
>write_config($vmid, $conf);
1764 } elsif ($opt eq 'vmstate') {
1765 PVE
::QemuConfig-
>check_protection($conf, "can't remove vmstate '$val'");
1766 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, { file
=> $val }, $rpcenv, $authuser, 1)) {
1767 delete $conf->{$opt};
1768 PVE
::QemuConfig-
>write_config($vmid, $conf);
1770 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1771 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1772 $check_drive_perms->($opt, $val);
1773 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $val))
1775 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1776 PVE
::QemuConfig-
>write_config($vmid, $conf);
1777 } elsif ($opt =~ m/^serial\d+$/) {
1778 if ($val eq 'socket') {
1779 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1780 } elsif ($authuser ne 'root@pam') {
1781 die "only root can delete '$opt' config for real devices\n";
1783 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1784 PVE
::QemuConfig-
>write_config($vmid, $conf);
1785 } elsif ($opt =~ m/^usb\d+$/) {
1786 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1787 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1788 PVE
::QemuConfig-
>write_config($vmid, $conf);
1789 } elsif ($opt =~ m/^hostpci\d+$/) {
1790 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $val);
1791 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1792 PVE
::QemuConfig-
>write_config($vmid, $conf);
1793 } elsif ($opt eq 'tags') {
1794 assert_tag_permissions
($vmid, $val, '', $rpcenv, $authuser);
1795 delete $conf->{$opt};
1796 PVE
::QemuConfig-
>write_config($vmid, $conf);
1797 } elsif ($opt =~ m/^net\d+$/) {
1798 if ($conf->{$opt}) {
1799 PVE
::QemuServer
::check_bridge_access
(
1802 { $opt => $conf->{$opt} },
1805 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1806 PVE
::QemuConfig-
>write_config($vmid, $conf);
1808 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1809 PVE
::QemuConfig-
>write_config($vmid, $conf);
1813 foreach my $opt (keys %$param) { # add/change
1814 $modified->{$opt} = 1;
1815 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1816 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1818 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1820 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1822 if ($conf->{$opt}) {
1823 $check_drive_perms->($opt, $conf->{$opt});
1827 $check_drive_perms->($opt, $param->{$opt});
1828 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1829 if defined($conf->{pending
}->{$opt});
1831 my (undef, $created_opts) = $create_disks->(
1839 {$opt => $param->{$opt}},
1841 $conf->{pending
}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1843 # default legacy boot order implies all cdroms anyway
1845 # append new CD drives to bootorder to mark them bootable
1846 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
1847 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1848 push @bootorder, $opt;
1849 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1850 $modified->{boot
} = 1;
1853 } elsif ($opt =~ m/^serial\d+/) {
1854 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1855 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1856 } elsif ($authuser ne 'root@pam') {
1857 die "only root can modify '$opt' config for real devices\n";
1859 $conf->{pending
}->{$opt} = $param->{$opt};
1860 } elsif ($opt =~ m/^usb\d+/) {
1861 if (my $olddevice = $conf->{$opt}) {
1862 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
1864 check_usb_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1865 $conf->{pending
}->{$opt} = $param->{$opt};
1866 } elsif ($opt =~ m/^hostpci\d+$/) {
1867 if (my $oldvalue = $conf->{$opt}) {
1868 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
1870 check_hostpci_perm
($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1871 $conf->{pending
}->{$opt} = $param->{$opt};
1872 } elsif ($opt eq 'tags') {
1873 assert_tag_permissions
($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
1874 $conf->{pending
}->{$opt} = PVE
::GuestHelpers
::get_unique_tags
($param->{$opt});
1875 } elsif ($opt =~ m/^net\d+$/) {
1876 if ($conf->{$opt}) {
1877 PVE
::QemuServer
::check_bridge_access
(
1880 { $opt => $conf->{$opt} },
1883 $conf->{pending
}->{$opt} = $param->{$opt};
1885 $conf->{pending
}->{$opt} = $param->{$opt};
1887 if ($opt eq 'boot') {
1888 my $new_bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $param->{$opt});
1889 if ($new_bootcfg->{order
}) {
1890 my @devs = PVE
::Tools
::split_list
($new_bootcfg->{order
});
1891 for my $dev (@devs) {
1892 my $exists = $conf->{$dev} || $conf->{pending
}->{$dev} || $param->{$dev};
1893 my $deleted = grep {$_ eq $dev} @delete;
1894 die "invalid bootorder: device '$dev' does not exist'\n"
1895 if !$exists || $deleted;
1898 # remove legacy boot order settings if new one set
1899 $conf->{pending
}->{$opt} = PVE
::QemuServer
::print_bootorder
(\
@devs);
1900 PVE
::QemuConfig-
>add_to_pending_delete($conf, "bootdisk")
1901 if $conf->{bootdisk
};
1905 PVE
::QemuConfig-
>remove_from_pending_delete($conf, $opt);
1906 PVE
::QemuConfig-
>write_config($vmid, $conf);
1909 # remove pending changes when nothing changed
1910 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1911 my $changes = PVE
::QemuConfig-
>cleanup_pending($conf);
1912 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
1914 return if !scalar(keys %{$conf->{pending
}});
1916 my $running = PVE
::QemuServer
::check_running
($vmid);
1918 # apply pending changes
1920 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1924 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
1926 # cloud_init must be skipped if we are in an incoming, remote live migration
1927 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
1929 raise_param_exc
($errors) if scalar(keys %$errors);
1938 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1940 if ($background_delay) {
1942 # Note: It would be better to do that in the Event based HTTPServer
1943 # to avoid blocking call to sleep.
1945 my $end_time = time() + $background_delay;
1947 my $task = PVE
::Tools
::upid_decode
($upid);
1950 while (time() < $end_time) {
1951 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
1953 sleep(1); # this gets interrupted when child process ends
1957 my $status = PVE
::Tools
::upid_read_status
($upid);
1958 return if !PVE
::Tools
::upid_status_is_error
($status);
1959 die "failed to update VM $vmid: $status\n";
1967 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
1970 my $vm_config_perm_list = [
1975 'VM.Config.Network',
1977 'VM.Config.Options',
1978 'VM.Config.Cloudinit',
1981 __PACKAGE__-
>register_method({
1982 name
=> 'update_vm_async',
1983 path
=> '{vmid}/config',
1987 description
=> "Set virtual machine options (asynchrounous API).",
1989 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1992 additionalProperties
=> 0,
1993 properties
=> PVE
::QemuServer
::json_config_properties
(
1995 node
=> get_standard_option
('pve-node'),
1996 vmid
=> get_standard_option
('pve-vmid'),
1997 skiplock
=> get_standard_option
('skiplock'),
1999 type
=> 'string', format
=> 'pve-configid-list',
2000 description
=> "A list of settings you want to delete.",
2004 type
=> 'string', format
=> 'pve-configid-list',
2005 description
=> "Revert a pending change.",
2010 description
=> $opt_force_description,
2012 requires
=> 'delete',
2016 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2020 background_delay
=> {
2022 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
2028 1, # with_disk_alloc
2035 code
=> $update_vm_api,
2038 __PACKAGE__-
>register_method({
2039 name
=> 'update_vm',
2040 path
=> '{vmid}/config',
2044 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
2046 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
2049 additionalProperties
=> 0,
2050 properties
=> PVE
::QemuServer
::json_config_properties
(
2052 node
=> get_standard_option
('pve-node'),
2053 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2054 skiplock
=> get_standard_option
('skiplock'),
2056 type
=> 'string', format
=> 'pve-configid-list',
2057 description
=> "A list of settings you want to delete.",
2061 type
=> 'string', format
=> 'pve-configid-list',
2062 description
=> "Revert a pending change.",
2067 description
=> $opt_force_description,
2069 requires
=> 'delete',
2073 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2078 1, # with_disk_alloc
2081 returns
=> { type
=> 'null' },
2084 &$update_vm_api($param, 1);
2089 __PACKAGE__-
>register_method({
2090 name
=> 'destroy_vm',
2095 description
=> "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
2096 ." and firewall rules",
2098 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
2101 additionalProperties
=> 0,
2103 node
=> get_standard_option
('pve-node'),
2104 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2105 skiplock
=> get_standard_option
('skiplock'),
2108 description
=> "Remove VMID from configurations, like backup & replication jobs and HA.",
2111 'destroy-unreferenced-disks' => {
2113 description
=> "If set, destroy additionally all disks not referenced in the config"
2114 ." but with a matching VMID from all enabled storages.",
2126 my $rpcenv = PVE
::RPCEnvironment
::get
();
2127 my $authuser = $rpcenv->get_user();
2128 my $vmid = $param->{vmid
};
2130 my $skiplock = $param->{skiplock
};
2131 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2132 if $skiplock && $authuser ne 'root@pam';
2134 my $early_checks = sub {
2136 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2137 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
2139 my $ha_managed = PVE
::HA
::Config
::service_is_configured
("vm:$vmid");
2141 if (!$param->{purge
}) {
2142 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2144 # don't allow destroy if with replication jobs but no purge param
2145 my $repl_conf = PVE
::ReplicationConfig-
>new();
2146 $repl_conf->check_for_existing_jobs($vmid);
2149 die "VM $vmid is running - destroy failed\n"
2150 if PVE
::QemuServer
::check_running
($vmid);
2160 my $storecfg = PVE
::Storage
::config
();
2162 syslog
('info', "destroy VM $vmid: $upid\n");
2163 PVE
::QemuConfig-
>lock_config($vmid, sub {
2164 # repeat, config might have changed
2165 my $ha_managed = $early_checks->();
2167 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
2169 PVE
::QemuServer
::destroy_vm
(
2172 $skiplock, { lock => 'destroyed' },
2173 $purge_unreferenced,
2176 PVE
::AccessControl
::remove_vm_access
($vmid);
2177 PVE
::Firewall
::remove_vmfw_conf
($vmid);
2178 if ($param->{purge
}) {
2179 print "purging VM $vmid from related configurations..\n";
2180 PVE
::ReplicationConfig
::remove_vmid_jobs
($vmid);
2181 PVE
::VZDump
::Plugin
::remove_vmid_from_backup_jobs
($vmid);
2184 PVE
::HA
::Config
::delete_service_from_config
("vm:$vmid");
2185 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2189 # only now remove the zombie config, else we can have reuse race
2190 PVE
::QemuConfig-
>destroy_config($vmid);
2194 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
2197 __PACKAGE__-
>register_method({
2199 path
=> '{vmid}/unlink',
2203 description
=> "Unlink/delete disk images.",
2205 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2208 additionalProperties
=> 0,
2210 node
=> get_standard_option
('pve-node'),
2211 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2213 type
=> 'string', format
=> 'pve-configid-list',
2214 description
=> "A list of disk IDs you want to delete.",
2218 description
=> $opt_force_description,
2223 returns
=> { type
=> 'null'},
2227 $param->{delete} = extract_param
($param, 'idlist');
2229 __PACKAGE__-
>update_vm($param);
2234 # uses good entropy, each char is limited to 6 bit to get printable chars simply
2235 my $gen_rand_chars = sub {
2238 die "invalid length $length" if $length < 1;
2240 my $min = ord('!'); # first printable ascii
2242 my $rand_bytes = Crypt
::OpenSSL
::Random
::random_bytes
($length);
2243 die "failed to generate random bytes!\n"
2246 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
2253 __PACKAGE__-
>register_method({
2255 path
=> '{vmid}/vncproxy',
2259 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2261 description
=> "Creates a TCP VNC proxy connections.",
2263 additionalProperties
=> 0,
2265 node
=> get_standard_option
('pve-node'),
2266 vmid
=> get_standard_option
('pve-vmid'),
2270 description
=> "starts websockify instead of vncproxy",
2272 'generate-password' => {
2276 description
=> "Generates a random password to be used as ticket instead of the API ticket.",
2281 additionalProperties
=> 0,
2283 user
=> { type
=> 'string' },
2284 ticket
=> { type
=> 'string' },
2287 description
=> "Returned if requested with 'generate-password' param."
2288 ." Consists of printable ASCII characters ('!' .. '~').",
2291 cert
=> { type
=> 'string' },
2292 port
=> { type
=> 'integer' },
2293 upid
=> { type
=> 'string' },
2299 my $rpcenv = PVE
::RPCEnvironment
::get
();
2301 my $authuser = $rpcenv->get_user();
2303 my $vmid = $param->{vmid
};
2304 my $node = $param->{node
};
2305 my $websocket = $param->{websocket
};
2307 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2311 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2312 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2315 my $authpath = "/vms/$vmid";
2317 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2318 my $password = $ticket;
2319 if ($param->{'generate-password'}) {
2320 $password = $gen_rand_chars->(8);
2323 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
2329 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2330 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2331 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2332 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
2333 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, defined($serial) ?
'-t' : '-T');
2335 $family = PVE
::Tools
::get_host_address_family
($node);
2338 my $port = PVE
::Tools
::next_vnc_port
($family);
2345 syslog
('info', "starting vnc proxy $upid\n");
2349 if (defined($serial)) {
2351 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
2353 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
2354 '-timeout', $timeout, '-authpath', $authpath,
2355 '-perm', 'Sys.Console'];
2357 if ($param->{websocket
}) {
2358 $ENV{PVE_VNC_TICKET
} = $password; # pass ticket to vncterm
2359 push @$cmd, '-notls', '-listen', 'localhost';
2362 push @$cmd, '-c', @$remcmd, @$termcmd;
2364 PVE
::Tools
::run_command
($cmd);
2368 $ENV{LC_PVE_TICKET
} = $password if $websocket; # set ticket with "qm vncproxy"
2370 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2372 my $sock = IO
::Socket
::IP-
>new(
2377 GetAddrInfoFlags
=> 0,
2378 ) or die "failed to create socket: $!\n";
2379 # Inside the worker we shouldn't have any previous alarms
2380 # running anyway...:
2382 local $SIG{ALRM
} = sub { die "connection timed out\n" };
2384 accept(my $cli, $sock) or die "connection failed: $!\n";
2387 if (PVE
::Tools
::run_command
($cmd,
2388 output
=> '>&'.fileno($cli),
2389 input
=> '<&'.fileno($cli),
2392 die "Failed to run vncproxy.\n";
2399 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2401 PVE
::Tools
::wait_for_vnc_port
($port);
2410 $res->{password
} = $password if $param->{'generate-password'};
2415 __PACKAGE__-
>register_method({
2416 name
=> 'termproxy',
2417 path
=> '{vmid}/termproxy',
2421 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2423 description
=> "Creates a TCP proxy connections.",
2425 additionalProperties
=> 0,
2427 node
=> get_standard_option
('pve-node'),
2428 vmid
=> get_standard_option
('pve-vmid'),
2432 enum
=> [qw(serial0 serial1 serial2 serial3)],
2433 description
=> "opens a serial terminal (defaults to display)",
2438 additionalProperties
=> 0,
2440 user
=> { type
=> 'string' },
2441 ticket
=> { type
=> 'string' },
2442 port
=> { type
=> 'integer' },
2443 upid
=> { type
=> 'string' },
2449 my $rpcenv = PVE
::RPCEnvironment
::get
();
2451 my $authuser = $rpcenv->get_user();
2453 my $vmid = $param->{vmid
};
2454 my $node = $param->{node
};
2455 my $serial = $param->{serial
};
2457 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2459 if (!defined($serial)) {
2461 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2462 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2466 my $authpath = "/vms/$vmid";
2468 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2473 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2474 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2475 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2476 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, '-t');
2477 push @$remcmd, '--';
2479 $family = PVE
::Tools
::get_host_address_family
($node);
2482 my $port = PVE
::Tools
::next_vnc_port
($family);
2484 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
2485 push @$termcmd, '-iface', $serial if $serial;
2490 syslog
('info', "starting qemu termproxy $upid\n");
2492 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2493 '--perm', 'VM.Console', '--'];
2494 push @$cmd, @$remcmd, @$termcmd;
2496 PVE
::Tools
::run_command
($cmd);
2499 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2501 PVE
::Tools
::wait_for_vnc_port
($port);
2511 __PACKAGE__-
>register_method({
2512 name
=> 'vncwebsocket',
2513 path
=> '{vmid}/vncwebsocket',
2516 description
=> "You also need to pass a valid ticket (vncticket).",
2517 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2519 description
=> "Opens a weksocket for VNC traffic.",
2521 additionalProperties
=> 0,
2523 node
=> get_standard_option
('pve-node'),
2524 vmid
=> get_standard_option
('pve-vmid'),
2526 description
=> "Ticket from previous call to vncproxy.",
2531 description
=> "Port number returned by previous vncproxy call.",
2541 port
=> { type
=> 'string' },
2547 my $rpcenv = PVE
::RPCEnvironment
::get
();
2549 my $authuser = $rpcenv->get_user();
2551 my $vmid = $param->{vmid
};
2552 my $node = $param->{node
};
2554 my $authpath = "/vms/$vmid";
2556 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
2558 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
2560 # Note: VNC ports are acessible from outside, so we do not gain any
2561 # security if we verify that $param->{port} belongs to VM $vmid. This
2562 # check is done by verifying the VNC ticket (inside VNC protocol).
2564 my $port = $param->{port
};
2566 return { port
=> $port };
2569 __PACKAGE__-
>register_method({
2570 name
=> 'spiceproxy',
2571 path
=> '{vmid}/spiceproxy',
2576 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2578 description
=> "Returns a SPICE configuration to connect to the VM.",
2580 additionalProperties
=> 0,
2582 node
=> get_standard_option
('pve-node'),
2583 vmid
=> get_standard_option
('pve-vmid'),
2584 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
2587 returns
=> get_standard_option
('remote-viewer-config'),
2591 my $rpcenv = PVE
::RPCEnvironment
::get
();
2593 my $authuser = $rpcenv->get_user();
2595 my $vmid = $param->{vmid
};
2596 my $node = $param->{node
};
2597 my $proxy = $param->{proxy
};
2599 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
2600 my $title = "VM $vmid";
2601 $title .= " - ". $conf->{name
} if $conf->{name
};
2603 my $port = PVE
::QemuServer
::spice_port
($vmid);
2605 my ($ticket, undef, $remote_viewer_config) =
2606 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
2608 mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
2609 mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
2611 return $remote_viewer_config;
2614 __PACKAGE__-
>register_method({
2616 path
=> '{vmid}/status',
2619 description
=> "Directory index",
2624 additionalProperties
=> 0,
2626 node
=> get_standard_option
('pve-node'),
2627 vmid
=> get_standard_option
('pve-vmid'),
2635 subdir
=> { type
=> 'string' },
2638 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
2644 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2647 { subdir
=> 'current' },
2648 { subdir
=> 'start' },
2649 { subdir
=> 'stop' },
2650 { subdir
=> 'reset' },
2651 { subdir
=> 'shutdown' },
2652 { subdir
=> 'suspend' },
2653 { subdir
=> 'reboot' },
2659 __PACKAGE__-
>register_method({
2660 name
=> 'vm_status',
2661 path
=> '{vmid}/status/current',
2664 protected
=> 1, # qemu pid files are only readable by root
2665 description
=> "Get virtual machine status.",
2667 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2670 additionalProperties
=> 0,
2672 node
=> get_standard_option
('pve-node'),
2673 vmid
=> get_standard_option
('pve-vmid'),
2679 %$PVE::QemuServer
::vmstatus_return_properties
,
2681 description
=> "HA manager service status.",
2685 description
=> "QEMU VGA configuration supports spice.",
2690 description
=> "QEMU Guest Agent is enabled in config.",
2700 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2702 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
2703 my $status = $vmstatus->{$param->{vmid
}};
2705 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
2708 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2709 my $spice = defined($vga->{type
}) && $vga->{type
} =~ /^virtio/;
2710 $spice ||= PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
2711 $status->{spice
} = 1 if $spice;
2713 $status->{agent
} = 1 if PVE
::QemuServer
::get_qga_key
($conf, 'enabled');
2718 __PACKAGE__-
>register_method({
2720 path
=> '{vmid}/status/start',
2724 description
=> "Start virtual machine.",
2726 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2729 additionalProperties
=> 0,
2731 node
=> get_standard_option
('pve-node'),
2732 vmid
=> get_standard_option
('pve-vmid',
2733 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2734 skiplock
=> get_standard_option
('skiplock'),
2735 stateuri
=> get_standard_option
('pve-qm-stateuri'),
2736 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
2739 enum
=> ['secure', 'insecure'],
2740 description
=> "Migration traffic is encrypted using an SSH " .
2741 "tunnel by default. On secure, completely private networks " .
2742 "this can be disabled to increase performance.",
2745 migration_network
=> {
2746 type
=> 'string', format
=> 'CIDR',
2747 description
=> "CIDR of the (sub) network that is used for migration.",
2750 machine
=> get_standard_option
('pve-qemu-machine'),
2752 description
=> "Override QEMU's -cpu argument with the given string.",
2756 targetstorage
=> get_standard_option
('pve-targetstorage'),
2758 description
=> "Wait maximal timeout seconds.",
2761 default => 'max(30, vm memory in GiB)',
2772 my $rpcenv = PVE
::RPCEnvironment
::get
();
2773 my $authuser = $rpcenv->get_user();
2775 my $node = extract_param
($param, 'node');
2776 my $vmid = extract_param
($param, 'vmid');
2777 my $timeout = extract_param
($param, 'timeout');
2778 my $machine = extract_param
($param, 'machine');
2780 my $get_root_param = sub {
2781 my $value = extract_param
($param, $_[0]);
2782 raise_param_exc
({ "$_[0]" => "Only root may use this option." })
2783 if $value && $authuser ne 'root@pam';
2787 my $stateuri = $get_root_param->('stateuri');
2788 my $skiplock = $get_root_param->('skiplock');
2789 my $migratedfrom = $get_root_param->('migratedfrom');
2790 my $migration_type = $get_root_param->('migration_type');
2791 my $migration_network = $get_root_param->('migration_network');
2792 my $targetstorage = $get_root_param->('targetstorage');
2793 my $force_cpu = $get_root_param->('force-cpu');
2797 if ($targetstorage) {
2798 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
2800 $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
2801 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
2805 # read spice ticket from STDIN
2807 my $nbd_protocol_version = 0;
2808 my $replicated_volumes = {};
2809 my $offline_volumes = {};
2810 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
2811 while (defined(my $line = <STDIN
>)) {
2813 if ($line =~ m/^spice_ticket: (.+)$/) {
2815 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2816 $nbd_protocol_version = $1;
2817 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2818 $replicated_volumes->{$1} = 1;
2819 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2820 $offline_volumes->{tpmstate0
} = $1;
2821 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2822 $offline_volumes->{$1} = $2;
2823 } elsif (!$spice_ticket) {
2824 # fallback for old source node
2825 $spice_ticket = $line;
2827 warn "unknown 'start' parameter on STDIN: '$line'\n";
2832 PVE
::Cluster
::check_cfs_quorum
();
2834 my $storecfg = PVE
::Storage
::config
();
2836 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri && $rpcenv->{type
} ne 'ha') {
2840 print "Requesting HA start for VM $vmid\n";
2842 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2843 PVE
::Tools
::run_command
($cmd);
2847 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2854 syslog
('info', "start VM $vmid: $upid\n");
2856 my $migrate_opts = {
2857 migratedfrom
=> $migratedfrom,
2858 spice_ticket
=> $spice_ticket,
2859 network
=> $migration_network,
2860 type
=> $migration_type,
2861 storagemap
=> $storagemap,
2862 nbd_proto_version
=> $nbd_protocol_version,
2863 replicated_volumes
=> $replicated_volumes,
2864 offline_volumes
=> $offline_volumes,
2868 statefile
=> $stateuri,
2869 skiplock
=> $skiplock,
2870 forcemachine
=> $machine,
2871 timeout
=> $timeout,
2872 forcecpu
=> $force_cpu,
2875 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $params, $migrate_opts);
2879 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2883 __PACKAGE__-
>register_method({
2885 path
=> '{vmid}/status/stop',
2889 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
2890 "is akin to pulling the power plug of a running computer and may damage the VM data",
2892 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2895 additionalProperties
=> 0,
2897 node
=> get_standard_option
('pve-node'),
2898 vmid
=> get_standard_option
('pve-vmid',
2899 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2900 skiplock
=> get_standard_option
('skiplock'),
2901 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
2903 description
=> "Wait maximal timeout seconds.",
2909 description
=> "Do not deactivate storage volumes.",
2922 my $rpcenv = PVE
::RPCEnvironment
::get
();
2923 my $authuser = $rpcenv->get_user();
2925 my $node = extract_param
($param, 'node');
2926 my $vmid = extract_param
($param, 'vmid');
2928 my $skiplock = extract_param
($param, 'skiplock');
2929 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2930 if $skiplock && $authuser ne 'root@pam';
2932 my $keepActive = extract_param
($param, 'keepActive');
2933 raise_param_exc
({ keepActive
=> "Only root may use this option." })
2934 if $keepActive && $authuser ne 'root@pam';
2936 my $migratedfrom = extract_param
($param, 'migratedfrom');
2937 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
2938 if $migratedfrom && $authuser ne 'root@pam';
2941 my $storecfg = PVE
::Storage
::config
();
2943 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
2948 print "Requesting HA stop for VM $vmid\n";
2950 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
2951 PVE
::Tools
::run_command
($cmd);
2955 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2961 syslog
('info', "stop VM $vmid: $upid\n");
2963 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
2964 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
2968 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2972 __PACKAGE__-
>register_method({
2974 path
=> '{vmid}/status/reset',
2978 description
=> "Reset virtual machine.",
2980 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2983 additionalProperties
=> 0,
2985 node
=> get_standard_option
('pve-node'),
2986 vmid
=> get_standard_option
('pve-vmid',
2987 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2988 skiplock
=> get_standard_option
('skiplock'),
2997 my $rpcenv = PVE
::RPCEnvironment
::get
();
2999 my $authuser = $rpcenv->get_user();
3001 my $node = extract_param
($param, 'node');
3003 my $vmid = extract_param
($param, 'vmid');
3005 my $skiplock = extract_param
($param, 'skiplock');
3006 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3007 if $skiplock && $authuser ne 'root@pam';
3009 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3014 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
3019 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
3022 __PACKAGE__-
>register_method({
3023 name
=> 'vm_shutdown',
3024 path
=> '{vmid}/status/shutdown',
3028 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3029 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
3031 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3034 additionalProperties
=> 0,
3036 node
=> get_standard_option
('pve-node'),
3037 vmid
=> get_standard_option
('pve-vmid',
3038 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3039 skiplock
=> get_standard_option
('skiplock'),
3041 description
=> "Wait maximal timeout seconds.",
3047 description
=> "Make sure the VM stops.",
3053 description
=> "Do not deactivate storage volumes.",
3066 my $rpcenv = PVE
::RPCEnvironment
::get
();
3067 my $authuser = $rpcenv->get_user();
3069 my $node = extract_param
($param, 'node');
3070 my $vmid = extract_param
($param, 'vmid');
3072 my $skiplock = extract_param
($param, 'skiplock');
3073 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3074 if $skiplock && $authuser ne 'root@pam';
3076 my $keepActive = extract_param
($param, 'keepActive');
3077 raise_param_exc
({ keepActive
=> "Only root may use this option." })
3078 if $keepActive && $authuser ne 'root@pam';
3080 my $storecfg = PVE
::Storage
::config
();
3084 # if vm is paused, do not shutdown (but stop if forceStop = 1)
3085 # otherwise, we will infer a shutdown command, but run into the timeout,
3086 # then when the vm is resumed, it will instantly shutdown
3088 # checking the qmp status here to get feedback to the gui/cli/api
3089 # and the status query should not take too long
3090 if (PVE
::QemuServer
::vm_is_paused
($vmid)) {
3091 if ($param->{forceStop
}) {
3092 warn "VM is paused - stop instead of shutdown\n";
3095 die "VM is paused - cannot shutdown\n";
3099 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
3101 my $timeout = $param->{timeout
} // 60;
3105 print "Requesting HA stop for VM $vmid\n";
3107 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
3108 PVE
::Tools
::run_command
($cmd);
3112 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3119 syslog
('info', "shutdown VM $vmid: $upid\n");
3121 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3122 $shutdown, $param->{forceStop
}, $keepActive);
3126 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3130 __PACKAGE__-
>register_method({
3131 name
=> 'vm_reboot',
3132 path
=> '{vmid}/status/reboot',
3136 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3138 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3141 additionalProperties
=> 0,
3143 node
=> get_standard_option
('pve-node'),
3144 vmid
=> get_standard_option
('pve-vmid',
3145 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3147 description
=> "Wait maximal timeout seconds for the shutdown.",
3160 my $rpcenv = PVE
::RPCEnvironment
::get
();
3161 my $authuser = $rpcenv->get_user();
3163 my $node = extract_param
($param, 'node');
3164 my $vmid = extract_param
($param, 'vmid');
3166 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid);
3168 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3173 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3174 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3178 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3181 __PACKAGE__-
>register_method({
3182 name
=> 'vm_suspend',
3183 path
=> '{vmid}/status/suspend',
3187 description
=> "Suspend virtual machine.",
3189 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3190 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3191 " on the storage for the vmstate.",
3192 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3195 additionalProperties
=> 0,
3197 node
=> get_standard_option
('pve-node'),
3198 vmid
=> get_standard_option
('pve-vmid',
3199 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3200 skiplock
=> get_standard_option
('skiplock'),
3205 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3207 statestorage
=> get_standard_option
('pve-storage-id', {
3208 description
=> "The storage for the VM state",
3209 requires
=> 'todisk',
3211 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3221 my $rpcenv = PVE
::RPCEnvironment
::get
();
3222 my $authuser = $rpcenv->get_user();
3224 my $node = extract_param
($param, 'node');
3225 my $vmid = extract_param
($param, 'vmid');
3227 my $todisk = extract_param
($param, 'todisk') // 0;
3229 my $statestorage = extract_param
($param, 'statestorage');
3231 my $skiplock = extract_param
($param, 'skiplock');
3232 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3233 if $skiplock && $authuser ne 'root@pam';
3235 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3237 die "Cannot suspend HA managed VM to disk\n"
3238 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3240 # early check for storage permission, for better user feedback
3242 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3243 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3245 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3246 for my $key (keys %$conf) {
3247 next if $key !~ /^hostpci\d+/;
3248 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3249 ." possibility to save/restore their internal state\n";
3252 if (!$statestorage) {
3253 # get statestorage from config if none is given
3254 my $storecfg = PVE
::Storage
::config
();
3255 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3258 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3264 syslog
('info', "suspend VM $vmid: $upid\n");
3266 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3271 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3272 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3275 __PACKAGE__-
>register_method({
3276 name
=> 'vm_resume',
3277 path
=> '{vmid}/status/resume',
3281 description
=> "Resume virtual machine.",
3283 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3286 additionalProperties
=> 0,
3288 node
=> get_standard_option
('pve-node'),
3289 vmid
=> get_standard_option
('pve-vmid',
3290 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3291 skiplock
=> get_standard_option
('skiplock'),
3292 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3302 my $rpcenv = PVE
::RPCEnvironment
::get
();
3304 my $authuser = $rpcenv->get_user();
3306 my $node = extract_param
($param, 'node');
3308 my $vmid = extract_param
($param, 'vmid');
3310 my $skiplock = extract_param
($param, 'skiplock');
3311 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3312 if $skiplock && $authuser ne 'root@pam';
3314 # nocheck is used as part of migration when config file might be still
3316 my $nocheck = extract_param
($param, 'nocheck');
3317 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3318 if $nocheck && $authuser ne 'root@pam';
3320 my $to_disk_suspended;
3322 PVE
::QemuConfig-
>lock_config($vmid, sub {
3323 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3324 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3328 die "VM $vmid not running\n"
3329 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3334 syslog
('info', "resume VM $vmid: $upid\n");
3336 if (!$to_disk_suspended) {
3337 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3339 my $storecfg = PVE
::Storage
::config
();
3340 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3346 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3349 __PACKAGE__-
>register_method({
3350 name
=> 'vm_sendkey',
3351 path
=> '{vmid}/sendkey',
3355 description
=> "Send key event to virtual machine.",
3357 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3360 additionalProperties
=> 0,
3362 node
=> get_standard_option
('pve-node'),
3363 vmid
=> get_standard_option
('pve-vmid',
3364 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3365 skiplock
=> get_standard_option
('skiplock'),
3367 description
=> "The key (qemu monitor encoding).",
3372 returns
=> { type
=> 'null'},
3376 my $rpcenv = PVE
::RPCEnvironment
::get
();
3378 my $authuser = $rpcenv->get_user();
3380 my $node = extract_param
($param, 'node');
3382 my $vmid = extract_param
($param, 'vmid');
3384 my $skiplock = extract_param
($param, 'skiplock');
3385 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3386 if $skiplock && $authuser ne 'root@pam';
3388 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3393 __PACKAGE__-
>register_method({
3394 name
=> 'vm_feature',
3395 path
=> '{vmid}/feature',
3399 description
=> "Check if feature for virtual machine is available.",
3401 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3404 additionalProperties
=> 0,
3406 node
=> get_standard_option
('pve-node'),
3407 vmid
=> get_standard_option
('pve-vmid'),
3409 description
=> "Feature to check.",
3411 enum
=> [ 'snapshot', 'clone', 'copy' ],
3413 snapname
=> get_standard_option
('pve-snapshot-name', {
3421 hasFeature
=> { type
=> 'boolean' },
3424 items
=> { type
=> 'string' },
3431 my $node = extract_param
($param, 'node');
3433 my $vmid = extract_param
($param, 'vmid');
3435 my $snapname = extract_param
($param, 'snapname');
3437 my $feature = extract_param
($param, 'feature');
3439 my $running = PVE
::QemuServer
::check_running
($vmid);
3441 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3444 my $snap = $conf->{snapshots
}->{$snapname};
3445 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3448 my $storecfg = PVE
::Storage
::config
();
3450 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3451 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3454 hasFeature
=> $hasFeature,
3455 nodes
=> [ keys %$nodelist ],
3459 __PACKAGE__-
>register_method({
3461 path
=> '{vmid}/clone',
3465 description
=> "Create a copy of virtual machine/template.",
3467 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3468 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3469 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
3472 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3474 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3475 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3480 additionalProperties
=> 0,
3482 node
=> get_standard_option
('pve-node'),
3483 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3484 newid
=> get_standard_option
('pve-vmid', {
3485 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3486 description
=> 'VMID for the clone.' }),
3489 type
=> 'string', format
=> 'dns-name',
3490 description
=> "Set a name for the new VM.",
3495 description
=> "Description for the new VM.",
3499 type
=> 'string', format
=> 'pve-poolid',
3500 description
=> "Add the new VM to the specified pool.",
3502 snapname
=> get_standard_option
('pve-snapshot-name', {
3505 storage
=> get_standard_option
('pve-storage-id', {
3506 description
=> "Target storage for full clone.",
3510 description
=> "Target format for file storage. Only valid for full clone.",
3513 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3518 description
=> "Create a full copy of all disks. This is always done when " .
3519 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3521 target
=> get_standard_option
('pve-node', {
3522 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3526 description
=> "Override I/O bandwidth limit (in KiB/s).",
3530 default => 'clone limit from datacenter or storage config',
3540 my $rpcenv = PVE
::RPCEnvironment
::get
();
3541 my $authuser = $rpcenv->get_user();
3543 my $node = extract_param
($param, 'node');
3544 my $vmid = extract_param
($param, 'vmid');
3545 my $newid = extract_param
($param, 'newid');
3546 my $pool = extract_param
($param, 'pool');
3548 my $snapname = extract_param
($param, 'snapname');
3549 my $storage = extract_param
($param, 'storage');
3550 my $format = extract_param
($param, 'format');
3551 my $target = extract_param
($param, 'target');
3553 my $localnode = PVE
::INotify
::nodename
();
3555 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3559 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3561 my $load_and_check = sub {
3562 $rpcenv->check_pool_exist($pool) if defined($pool);
3563 PVE
::Cluster
::check_node_exists
($target) if $target;
3565 my $storecfg = PVE
::Storage
::config
();
3568 # check if storage is enabled on local node
3569 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3571 # check if storage is available on target node
3572 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3573 # clone only works if target storage is shared
3574 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3575 die "can't clone to non-shared storage '$storage'\n"
3576 if !$scfg->{shared
};
3580 PVE
::Cluster
::check_cfs_quorum
();
3582 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3583 PVE
::QemuConfig-
>check_lock($conf);
3585 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3586 die "unexpected state change\n" if $verify_running != $running;
3588 die "snapshot '$snapname' does not exist\n"
3589 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3591 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3593 die "parameter 'storage' not allowed for linked clones\n"
3594 if defined($storage) && !$full;
3596 die "parameter 'format' not allowed for linked clones\n"
3597 if defined($format) && !$full;
3599 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3601 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3602 PVE
::QemuServer
::check_mapping_access
($rpcenv, $authuser, $oldconf);
3604 PVE
::QemuServer
::check_bridge_access
($rpcenv, $authuser, $oldconf);
3606 die "can't clone VM to node '$target' (VM uses local storage)\n"
3607 if $target && !$sharedvm;
3609 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3610 die "unable to create VM $newid: config file already exists\n"
3613 my $newconf = { lock => 'clone' };
3618 foreach my $opt (keys %$oldconf) {
3619 my $value = $oldconf->{$opt};
3621 # do not copy snapshot related info
3622 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3623 $opt eq 'vmstate' || $opt eq 'snapstate';
3625 # no need to copy unused images, because VMID(owner) changes anyways
3626 next if $opt =~ m/^unused\d+$/;
3628 die "cannot clone TPM state while VM is running\n"
3629 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3631 # always change MAC! address
3632 if ($opt =~ m/^net(\d+)$/) {
3633 my $net = PVE
::QemuServer
::parse_net
($value);
3634 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3635 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3636 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3637 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3638 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3639 die "unable to parse drive options for '$opt'\n" if !$drive;
3640 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3641 $newconf->{$opt} = $value; # simply copy configuration
3643 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3644 die "Full clone feature is not supported for drive '$opt'\n"
3645 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3646 $fullclone->{$opt} = 1;
3648 # not full means clone instead of copy
3649 die "Linked clone feature is not supported for drive '$opt'\n"
3650 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3652 $drives->{$opt} = $drive;
3653 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3654 push @$vollist, $drive->{file
};
3657 # copy everything else
3658 $newconf->{$opt} = $value;
3662 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3666 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3667 my $storecfg = PVE
::Storage
::config
();
3669 # auto generate a new uuid
3670 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3671 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3672 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3673 # auto generate a new vmgenid only if the option was set for template
3674 if ($newconf->{vmgenid
}) {
3675 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3678 delete $newconf->{template
};
3680 if ($param->{name
}) {
3681 $newconf->{name
} = $param->{name
};
3683 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3686 if ($param->{description
}) {
3687 $newconf->{description
} = $param->{description
};
3690 # create empty/temp config - this fails if VM already exists on other node
3691 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3692 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3694 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3696 my $newvollist = [];
3703 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3705 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3707 my $bwlimit = extract_param
($param, 'bwlimit');
3709 my $total_jobs = scalar(keys %{$drives});
3712 foreach my $opt (sort keys %$drives) {
3713 my $drive = $drives->{$opt};
3714 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3715 my $completion = $skipcomplete ?
'skip' : 'complete';
3717 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3718 my $storage_list = [ $src_sid ];
3719 push @$storage_list, $storage if defined($storage);
3720 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3724 running
=> $running,
3727 snapname
=> $snapname,
3733 storage
=> $storage,
3737 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3738 if $opt eq 'efidisk0';
3740 my $newdrive = PVE
::QemuServer
::clone_disk
(
3752 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3754 PVE
::QemuConfig-
>write_config($newid, $newconf);
3758 delete $newconf->{lock};
3760 # do not write pending changes
3761 if (my @changes = keys %{$newconf->{pending
}}) {
3762 my $pending = join(',', @changes);
3763 warn "found pending changes for '$pending', discarding for clone\n";
3764 delete $newconf->{pending
};
3767 PVE
::QemuConfig-
>write_config($newid, $newconf);
3770 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3771 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
3772 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3774 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3775 die "Failed to move config to node '$target' - rename failed: $!\n"
3776 if !rename($conffile, $newconffile);
3779 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3782 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3783 sleep 1; # some storage like rbd need to wait before release volume - really?
3785 foreach my $volid (@$newvollist) {
3786 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3790 PVE
::Firewall
::remove_vmfw_conf
($newid);
3792 unlink $conffile; # avoid races -> last thing before die
3794 die "clone failed: $err";
3800 # Aquire exclusive lock lock for $newid
3801 my $lock_target_vm = sub {
3802 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3805 my $lock_source_vm = sub {
3806 # exclusive lock if VM is running - else shared lock is enough;
3808 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3810 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3814 $load_and_check->(); # early checks before forking/locking
3816 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3819 __PACKAGE__-
>register_method({
3820 name
=> 'move_vm_disk',
3821 path
=> '{vmid}/move_disk',
3825 description
=> "Move volume to different storage or to a different VM.",
3827 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3828 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3829 "a disk to another VM, you need the permissions on the target VM as well.",
3830 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3833 additionalProperties
=> 0,
3835 node
=> get_standard_option
('pve-node'),
3836 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3837 'target-vmid' => get_standard_option
('pve-vmid', {
3838 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3843 description
=> "The disk you want to move.",
3844 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3846 storage
=> get_standard_option
('pve-storage-id', {
3847 description
=> "Target storage.",
3848 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3853 description
=> "Target Format.",
3854 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3859 description
=> "Delete the original disk after successful copy. By default the"
3860 ." original disk is kept as unused disk.",
3866 description
=> 'Prevent changes if current configuration file has different SHA1"
3867 ." digest. This can be used to prevent concurrent modifications.',
3872 description
=> "Override I/O bandwidth limit (in KiB/s).",
3876 default => 'move limit from datacenter or storage config',
3880 description
=> "The config key the disk will be moved to on the target VM"
3881 ." (for example, ide0 or scsi1). Default is the source disk key.",
3882 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3885 'target-digest' => {
3887 description
=> 'Prevent changes if the current config file of the target VM has a"
3888 ." different SHA1 digest. This can be used to detect concurrent modifications.',
3896 description
=> "the task ID.",
3901 my $rpcenv = PVE
::RPCEnvironment
::get
();
3902 my $authuser = $rpcenv->get_user();
3904 my $node = extract_param
($param, 'node');
3905 my $vmid = extract_param
($param, 'vmid');
3906 my $target_vmid = extract_param
($param, 'target-vmid');
3907 my $digest = extract_param
($param, 'digest');
3908 my $target_digest = extract_param
($param, 'target-digest');
3909 my $disk = extract_param
($param, 'disk');
3910 my $target_disk = extract_param
($param, 'target-disk') // $disk;
3911 my $storeid = extract_param
($param, 'storage');
3912 my $format = extract_param
($param, 'format');
3914 my $storecfg = PVE
::Storage
::config
();
3916 my $load_and_check_move = sub {
3917 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3918 PVE
::QemuConfig-
>check_lock($conf);
3920 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
3922 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3924 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3926 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
3927 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3929 my $old_volid = $drive->{file
};
3931 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
3932 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3936 die "you can't move to the same storage with same format\n"
3937 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
3939 # this only checks snapshots because $disk is passed!
3940 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
3946 die "you can't move a disk with snapshots and delete the source\n"
3947 if $snapshotted && $param->{delete};
3949 return ($conf, $drive, $oldstoreid, $snapshotted);
3952 my $move_updatefn = sub {
3953 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3954 my $old_volid = $drive->{file
};
3956 PVE
::Cluster
::log_msg
(
3959 "move disk VM $vmid: move --disk $disk --storage $storeid"
3962 my $running = PVE
::QemuServer
::check_running
($vmid);
3964 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
3966 my $newvollist = [];
3972 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3974 warn "moving disk with snapshots, snapshots will not be moved!\n"
3977 my $bwlimit = extract_param
($param, 'bwlimit');
3978 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
3980 [$oldstoreid, $storeid],
3986 running
=> $running,
3995 storage
=> $storeid,
3999 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
4000 if $disk eq 'efidisk0';
4002 my $newdrive = PVE
::QemuServer
::clone_disk
(
4013 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
4015 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
4017 # convert moved disk to base if part of template
4018 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
4019 if PVE
::QemuConfig-
>is_template($conf);
4021 PVE
::QemuConfig-
>write_config($vmid, $conf);
4023 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
4024 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
4025 eval { mon_cmd
($vmid, "guest-fstrim") };
4029 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4030 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
4036 foreach my $volid (@$newvollist) {
4037 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
4040 die "storage migration failed: $err";
4043 if ($param->{delete}) {
4045 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
4046 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
4052 my $load_and_check_reassign_configs = sub {
4053 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
4055 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
4056 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
4058 my $source_node = $vmlist->{$vmid}->{node
};
4059 my $target_node = $vmlist->{$target_vmid}->{node
};
4061 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4062 if $source_node ne $target_node;
4064 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
4065 PVE
::QemuConfig-
>check_lock($source_conf);
4066 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
4067 PVE
::QemuConfig-
>check_lock($target_conf);
4069 die "Can't move disks from or to template VMs\n"
4070 if ($source_conf->{template
} || $target_conf->{template
});
4073 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
4074 die "VM ${vmid}: $@" if $@;
4077 if ($target_digest) {
4078 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
4079 die "VM ${target_vmid}: $@" if $@;
4082 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4084 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
4085 if $target_conf->{$target_disk};
4087 my $drive = PVE
::QemuServer
::parse_drive
(
4089 $source_conf->{$disk},
4091 die "failed to parse source disk - $@\n" if !$drive;
4093 my $source_volid = $drive->{file
};
4095 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4096 die "CD drive contents can't be moved to another VM\n"
4097 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
4099 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
4100 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4102 die "Can't move disk used by a snapshot to another VM\n"
4103 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
4104 die "Storage does not support moving of this disk to another VM\n"
4105 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
4106 die "Cannot move disk to another VM while the source VM is running - detach first\n"
4107 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
4109 # now re-parse using target disk slot format
4110 if ($target_disk =~ /^unused\d+$/) {
4111 $drive = PVE
::QemuServer
::parse_drive
(
4116 $drive = PVE
::QemuServer
::parse_drive
(
4118 $source_conf->{$disk},
4121 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4123 my $repl_conf = PVE
::ReplicationConfig-
>new();
4124 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4125 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4126 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4127 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4130 return ($source_conf, $target_conf, $drive);
4135 print STDERR
"$msg\n";
4138 my $disk_reassignfn = sub {
4139 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4140 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4141 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4143 my $source_volid = $drive->{file
};
4145 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4146 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4148 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4150 my $new_volid = PVE
::Storage
::rename_volume
(
4156 $drive->{file
} = $new_volid;
4158 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4159 if (defined(delete $boot_order->{$disk})) {
4160 print "removing disk '$disk' from boot order config\n";
4161 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4162 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4165 delete $source_conf->{$disk};
4166 print "removing disk '${disk}' from VM '${vmid}' config\n";
4167 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4169 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4171 if ($target_disk =~ /^unused\d+$/) {
4172 $target_conf->{$target_disk} = $drive_string;
4173 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4178 vmid
=> $target_vmid,
4179 digest
=> $target_digest,
4180 $target_disk => $drive_string,
4186 # remove possible replication snapshots
4187 if (PVE
::Storage
::volume_has_feature
(
4193 PVE
::Replication
::prepare
(
4203 print "Failed to remove replication snapshots on moved disk " .
4204 "'$target_disk'. Manual cleanup could be necessary.\n";
4211 if ($target_vmid && $storeid) {
4212 my $msg = "either set 'storage' or 'target-vmid', but not both";
4213 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4214 } elsif ($target_vmid) {
4215 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4216 if $authuser ne 'root@pam';
4218 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4219 if $vmid eq $target_vmid;
4221 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4222 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4223 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4225 return $rpcenv->fork_worker(
4227 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4231 } elsif ($storeid) {
4232 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4234 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4235 if $disk =~ m/^unused\d+$/;
4237 $load_and_check_move->(); # early checks before forking/locking
4240 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4243 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4245 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4246 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4250 my $check_vm_disks_local = sub {
4251 my ($storecfg, $vmconf, $vmid) = @_;
4253 my $local_disks = {};
4255 # add some more information to the disks e.g. cdrom
4256 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4257 my ($volid, $attr) = @_;
4259 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4261 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4262 return if $scfg->{shared
};
4264 # The shared attr here is just a special case where the vdisk
4265 # is marked as shared manually
4266 return if $attr->{shared
};
4267 return if $attr->{cdrom
} and $volid eq "none";
4269 if (exists $local_disks->{$volid}) {
4270 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4272 $local_disks->{$volid} = $attr;
4273 # ensure volid is present in case it's needed
4274 $local_disks->{$volid}->{volid
} = $volid;
4278 return $local_disks;
4281 __PACKAGE__-
>register_method({
4282 name
=> 'migrate_vm_precondition',
4283 path
=> '{vmid}/migrate',
4287 description
=> "Get preconditions for migration.",
4289 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4292 additionalProperties
=> 0,
4294 node
=> get_standard_option
('pve-node'),
4295 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4296 target
=> get_standard_option
('pve-node', {
4297 description
=> "Target node.",
4298 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4306 running
=> { type
=> 'boolean' },
4310 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4312 not_allowed_nodes
=> {
4315 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4319 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4321 local_resources
=> {
4323 description
=> "List local resources e.g. pci, usb"
4325 'mapped-resources' => {
4327 description
=> "List of mapped resources e.g. pci, usb"
4334 my $rpcenv = PVE
::RPCEnvironment
::get
();
4336 my $authuser = $rpcenv->get_user();
4338 PVE
::Cluster
::check_cfs_quorum
();
4342 my $vmid = extract_param
($param, 'vmid');
4343 my $target = extract_param
($param, 'target');
4344 my $localnode = PVE
::INotify
::nodename
();
4348 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4349 my $storecfg = PVE
::Storage
::config
();
4352 # try to detect errors early
4353 PVE
::QemuConfig-
>check_lock($vmconf);
4355 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4357 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4358 PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4359 delete $missing_mappings_by_node->{$localnode};
4361 # if vm is not running, return target nodes where local storage/mapped devices are available
4362 # for offline migration
4363 if (!$res->{running
}) {
4364 $res->{allowed_nodes
} = [];
4365 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4366 delete $checked_nodes->{$localnode};
4368 foreach my $node (keys %$checked_nodes) {
4369 my $missing_mappings = $missing_mappings_by_node->{$node};
4370 if (scalar($missing_mappings->@*)) {
4371 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4375 if (!defined($checked_nodes->{$node}->{unavailable_storages
})) {
4376 push @{$res->{allowed_nodes
}}, $node;
4380 $res->{not_allowed_nodes
} = $checked_nodes;
4383 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4384 $res->{local_disks
} = [ values %$local_disks ];;
4386 $res->{local_resources
} = $local_resources;
4387 $res->{'mapped-resources'} = $mapped_resources;
4394 __PACKAGE__-
>register_method({
4395 name
=> 'migrate_vm',
4396 path
=> '{vmid}/migrate',
4400 description
=> "Migrate virtual machine. Creates a new migration task.",
4402 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4405 additionalProperties
=> 0,
4407 node
=> get_standard_option
('pve-node'),
4408 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4409 target
=> get_standard_option
('pve-node', {
4410 description
=> "Target node.",
4411 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4415 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4420 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4425 enum
=> ['secure', 'insecure'],
4426 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4429 migration_network
=> {
4430 type
=> 'string', format
=> 'CIDR',
4431 description
=> "CIDR of the (sub) network that is used for migration.",
4434 "with-local-disks" => {
4436 description
=> "Enable live storage migration for local disk",
4439 targetstorage
=> get_standard_option
('pve-targetstorage', {
4440 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4443 description
=> "Override I/O bandwidth limit (in KiB/s).",
4447 default => 'migrate limit from datacenter or storage config',
4453 description
=> "the task ID.",
4458 my $rpcenv = PVE
::RPCEnvironment
::get
();
4459 my $authuser = $rpcenv->get_user();
4461 my $target = extract_param
($param, 'target');
4463 my $localnode = PVE
::INotify
::nodename
();
4464 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4466 PVE
::Cluster
::check_cfs_quorum
();
4468 PVE
::Cluster
::check_node_exists
($target);
4470 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4472 my $vmid = extract_param
($param, 'vmid');
4474 raise_param_exc
({ force
=> "Only root may use this option." })
4475 if $param->{force
} && $authuser ne 'root@pam';
4477 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4478 if $param->{migration_type
} && $authuser ne 'root@pam';
4480 # allow root only until better network permissions are available
4481 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4482 if $param->{migration_network
} && $authuser ne 'root@pam';
4485 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4487 # try to detect errors early
4489 PVE
::QemuConfig-
>check_lock($conf);
4491 if (PVE
::QemuServer
::check_running
($vmid)) {
4492 die "can't migrate running VM without --online\n" if !$param->{online
};
4494 my $repl_conf = PVE
::ReplicationConfig-
>new();
4495 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4496 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4497 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4498 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4499 "target. Use 'force' to override.\n";
4502 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4503 $param->{online
} = 0;
4506 my $storecfg = PVE
::Storage
::config
();
4507 if (my $targetstorage = $param->{targetstorage
}) {
4508 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4509 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4512 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4513 if !defined($storagemap->{identity
});
4515 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4516 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4519 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4520 if $storagemap->{default};
4522 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4523 if $storagemap->{identity
};
4525 $param->{storagemap
} = $storagemap;
4527 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4530 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4535 print "Requesting HA migration for VM $vmid to node $target\n";
4537 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4538 PVE
::Tools
::run_command
($cmd);
4542 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4547 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4551 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4554 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4559 __PACKAGE__-
>register_method({
4560 name
=> 'remote_migrate_vm',
4561 path
=> '{vmid}/remote_migrate',
4565 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4567 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4570 additionalProperties
=> 0,
4572 node
=> get_standard_option
('pve-node'),
4573 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4574 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4575 'target-endpoint' => get_standard_option
('proxmox-remote', {
4576 description
=> "Remote target endpoint",
4580 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4585 description
=> "Delete the original VM and related data after successful migration. By default the original VM is kept on the source cluster in a stopped state.",
4589 'target-storage' => get_standard_option
('pve-targetstorage', {
4590 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4593 'target-bridge' => {
4595 description
=> "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.",
4596 format
=> 'bridge-pair-list',
4599 description
=> "Override I/O bandwidth limit (in KiB/s).",
4603 default => 'migrate limit from datacenter or storage config',
4609 description
=> "the task ID.",
4614 my $rpcenv = PVE
::RPCEnvironment
::get
();
4615 my $authuser = $rpcenv->get_user();
4617 my $source_vmid = extract_param
($param, 'vmid');
4618 my $target_endpoint = extract_param
($param, 'target-endpoint');
4619 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4621 my $delete = extract_param
($param, 'delete') // 0;
4623 PVE
::Cluster
::check_cfs_quorum
();
4626 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4628 PVE
::QemuConfig-
>check_lock($conf);
4630 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4631 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4633 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4635 # TODO: move this as helper somewhere appropriate?
4637 protocol
=> 'https',
4638 host
=> $remote->{host
},
4639 port
=> $remote->{port
} // 8006,
4640 apitoken
=> $remote->{apitoken
},
4644 if ($fp = $remote->{fingerprint
}) {
4645 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4648 print "Establishing API connection with remote at '$remote->{host}'\n";
4650 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4652 if (!defined($fp)) {
4653 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4654 foreach my $cert (@$cert_info) {
4655 my $filename = $cert->{filename
};
4656 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4657 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4659 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4663 my $repl_conf = PVE
::ReplicationConfig-
>new();
4664 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4665 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4667 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4668 die "can't migrate running VM without --online\n" if !$param->{online
};
4671 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4672 $param->{online
} = 0;
4675 my $storecfg = PVE
::Storage
::config
();
4676 my $target_storage = extract_param
($param, 'target-storage');
4677 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4678 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4681 my $target_bridge = extract_param
($param, 'target-bridge');
4682 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4683 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4686 die "remote migration requires explicit storage mapping!\n"
4687 if $storagemap->{identity
};
4689 $param->{storagemap
} = $storagemap;
4690 $param->{bridgemap
} = $bridgemap;
4691 $param->{remote
} = {
4692 conn
=> $conn_args, # re-use fingerprint for tunnel
4693 client
=> $api_client,
4694 vmid
=> $target_vmid,
4696 $param->{migration_type
} = 'websocket';
4697 $param->{'with-local-disks'} = 1;
4698 $param->{delete} = $delete if $delete;
4700 my $cluster_status = $api_client->get("/cluster/status");
4702 foreach my $entry (@$cluster_status) {
4703 next if $entry->{type
} ne 'node';
4704 if ($entry->{local}) {
4705 $target_node = $entry->{name
};
4710 die "couldn't determine endpoint's node name\n"
4711 if !defined($target_node);
4714 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4718 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4721 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4724 __PACKAGE__-
>register_method({
4726 path
=> '{vmid}/monitor',
4730 description
=> "Execute QEMU monitor commands.",
4732 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4733 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4736 additionalProperties
=> 0,
4738 node
=> get_standard_option
('pve-node'),
4739 vmid
=> get_standard_option
('pve-vmid'),
4742 description
=> "The monitor command.",
4746 returns
=> { type
=> 'string'},
4750 my $rpcenv = PVE
::RPCEnvironment
::get
();
4751 my $authuser = $rpcenv->get_user();
4754 my $command = shift;
4755 return $command =~ m/^\s*info(\s+|$)/
4756 || $command =~ m/^\s*help\s*$/;
4759 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4760 if !&$is_ro($param->{command
});
4762 my $vmid = $param->{vmid
};
4764 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4768 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4770 $res = "ERROR: $@" if $@;
4775 __PACKAGE__-
>register_method({
4776 name
=> 'resize_vm',
4777 path
=> '{vmid}/resize',
4781 description
=> "Extend volume size.",
4783 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4786 additionalProperties
=> 0,
4788 node
=> get_standard_option
('pve-node'),
4789 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4790 skiplock
=> get_standard_option
('skiplock'),
4793 description
=> "The disk you want to resize.",
4794 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4798 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4799 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.",
4803 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4811 description
=> "the task ID.",
4816 my $rpcenv = PVE
::RPCEnvironment
::get
();
4818 my $authuser = $rpcenv->get_user();
4820 my $node = extract_param
($param, 'node');
4822 my $vmid = extract_param
($param, 'vmid');
4824 my $digest = extract_param
($param, 'digest');
4826 my $disk = extract_param
($param, 'disk');
4828 my $sizestr = extract_param
($param, 'size');
4830 my $skiplock = extract_param
($param, 'skiplock');
4831 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4832 if $skiplock && $authuser ne 'root@pam';
4834 my $storecfg = PVE
::Storage
::config
();
4836 my $updatefn = sub {
4838 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4840 die "checksum missmatch (file change by other user?)\n"
4841 if $digest && $digest ne $conf->{digest
};
4842 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4844 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4846 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4848 my (undef, undef, undef, undef, undef, undef, $format) =
4849 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4851 my $volid = $drive->{file
};
4853 die "disk '$disk' has no associated volume\n" if !$volid;
4855 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4857 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4859 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4861 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4862 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
4864 die "Could not determine current size of volume '$volid'\n" if !defined($size);
4866 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4867 my ($ext, $newsize, $unit) = ($1, $2, $4);
4870 $newsize = $newsize * 1024;
4871 } elsif ($unit eq 'M') {
4872 $newsize = $newsize * 1024 * 1024;
4873 } elsif ($unit eq 'G') {
4874 $newsize = $newsize * 1024 * 1024 * 1024;
4875 } elsif ($unit eq 'T') {
4876 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4879 $newsize += $size if $ext;
4880 $newsize = int($newsize);
4882 die "shrinking disks is not supported\n" if $newsize < $size;
4884 return if $size == $newsize;
4886 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
4888 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
4890 $drive->{size
} = $newsize;
4891 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
4893 PVE
::QemuConfig-
>write_config($vmid, $conf);
4897 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
4900 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
4903 __PACKAGE__-
>register_method({
4904 name
=> 'snapshot_list',
4905 path
=> '{vmid}/snapshot',
4907 description
=> "List all snapshots.",
4909 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4912 protected
=> 1, # qemu pid files are only readable by root
4914 additionalProperties
=> 0,
4916 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4917 node
=> get_standard_option
('pve-node'),
4926 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
4930 description
=> "Snapshot includes RAM.",
4935 description
=> "Snapshot description.",
4939 description
=> "Snapshot creation time",
4941 renderer
=> 'timestamp',
4945 description
=> "Parent snapshot identifier.",
4951 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
4956 my $vmid = $param->{vmid
};
4958 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4959 my $snaphash = $conf->{snapshots
} || {};
4963 foreach my $name (keys %$snaphash) {
4964 my $d = $snaphash->{$name};
4967 snaptime
=> $d->{snaptime
} || 0,
4968 vmstate
=> $d->{vmstate
} ?
1 : 0,
4969 description
=> $d->{description
} || '',
4971 $item->{parent
} = $d->{parent
} if $d->{parent
};
4972 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
4976 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
4979 digest
=> $conf->{digest
},
4980 running
=> $running,
4981 description
=> "You are here!",
4983 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
4985 push @$res, $current;
4990 __PACKAGE__-
>register_method({
4992 path
=> '{vmid}/snapshot',
4996 description
=> "Snapshot a VM.",
4998 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5001 additionalProperties
=> 0,
5003 node
=> get_standard_option
('pve-node'),
5004 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5005 snapname
=> get_standard_option
('pve-snapshot-name'),
5009 description
=> "Save the vmstate",
5014 description
=> "A textual description or comment.",
5020 description
=> "the task ID.",
5025 my $rpcenv = PVE
::RPCEnvironment
::get
();
5027 my $authuser = $rpcenv->get_user();
5029 my $node = extract_param
($param, 'node');
5031 my $vmid = extract_param
($param, 'vmid');
5033 my $snapname = extract_param
($param, 'snapname');
5035 die "unable to use snapshot name 'current' (reserved name)\n"
5036 if $snapname eq 'current';
5038 die "unable to use snapshot name 'pending' (reserved name)\n"
5039 if lc($snapname) eq 'pending';
5042 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
5043 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
5044 $param->{description
});
5047 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5050 __PACKAGE__-
>register_method({
5051 name
=> 'snapshot_cmd_idx',
5052 path
=> '{vmid}/snapshot/{snapname}',
5059 additionalProperties
=> 0,
5061 vmid
=> get_standard_option
('pve-vmid'),
5062 node
=> get_standard_option
('pve-node'),
5063 snapname
=> get_standard_option
('pve-snapshot-name'),
5072 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
5079 push @$res, { cmd
=> 'rollback' };
5080 push @$res, { cmd
=> 'config' };
5085 __PACKAGE__-
>register_method({
5086 name
=> 'update_snapshot_config',
5087 path
=> '{vmid}/snapshot/{snapname}/config',
5091 description
=> "Update snapshot metadata.",
5093 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5096 additionalProperties
=> 0,
5098 node
=> get_standard_option
('pve-node'),
5099 vmid
=> get_standard_option
('pve-vmid'),
5100 snapname
=> get_standard_option
('pve-snapshot-name'),
5104 description
=> "A textual description or comment.",
5108 returns
=> { type
=> 'null' },
5112 my $rpcenv = PVE
::RPCEnvironment
::get
();
5114 my $authuser = $rpcenv->get_user();
5116 my $vmid = extract_param
($param, 'vmid');
5118 my $snapname = extract_param
($param, 'snapname');
5120 return if !defined($param->{description
});
5122 my $updatefn = sub {
5124 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5126 PVE
::QemuConfig-
>check_lock($conf);
5128 my $snap = $conf->{snapshots
}->{$snapname};
5130 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5132 $snap->{description
} = $param->{description
} if defined($param->{description
});
5134 PVE
::QemuConfig-
>write_config($vmid, $conf);
5137 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5142 __PACKAGE__-
>register_method({
5143 name
=> 'get_snapshot_config',
5144 path
=> '{vmid}/snapshot/{snapname}/config',
5147 description
=> "Get snapshot configuration",
5149 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5152 additionalProperties
=> 0,
5154 node
=> get_standard_option
('pve-node'),
5155 vmid
=> get_standard_option
('pve-vmid'),
5156 snapname
=> get_standard_option
('pve-snapshot-name'),
5159 returns
=> { type
=> "object" },
5163 my $rpcenv = PVE
::RPCEnvironment
::get
();
5165 my $authuser = $rpcenv->get_user();
5167 my $vmid = extract_param
($param, 'vmid');
5169 my $snapname = extract_param
($param, 'snapname');
5171 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5173 my $snap = $conf->{snapshots
}->{$snapname};
5175 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5180 __PACKAGE__-
>register_method({
5182 path
=> '{vmid}/snapshot/{snapname}/rollback',
5186 description
=> "Rollback VM state to specified snapshot.",
5188 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5191 additionalProperties
=> 0,
5193 node
=> get_standard_option
('pve-node'),
5194 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5195 snapname
=> get_standard_option
('pve-snapshot-name'),
5198 description
=> "Whether the VM should get started after rolling back successfully."
5199 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
5207 description
=> "the task ID.",
5212 my $rpcenv = PVE
::RPCEnvironment
::get
();
5214 my $authuser = $rpcenv->get_user();
5216 my $node = extract_param
($param, 'node');
5218 my $vmid = extract_param
($param, 'vmid');
5220 my $snapname = extract_param
($param, 'snapname');
5223 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5224 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5226 if ($param->{start
} && !PVE
::QemuServer
::Helpers
::vm_running_locally
($vmid)) {
5227 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5232 # hold migration lock, this makes sure that nobody create replication snapshots
5233 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5236 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5239 __PACKAGE__-
>register_method({
5240 name
=> 'delsnapshot',
5241 path
=> '{vmid}/snapshot/{snapname}',
5245 description
=> "Delete a VM snapshot.",
5247 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5250 additionalProperties
=> 0,
5252 node
=> get_standard_option
('pve-node'),
5253 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5254 snapname
=> get_standard_option
('pve-snapshot-name'),
5258 description
=> "For removal from config file, even if removing disk snapshots fails.",
5264 description
=> "the task ID.",
5269 my $rpcenv = PVE
::RPCEnvironment
::get
();
5271 my $authuser = $rpcenv->get_user();
5273 my $node = extract_param
($param, 'node');
5275 my $vmid = extract_param
($param, 'vmid');
5277 my $snapname = extract_param
($param, 'snapname');
5280 my $do_delete = sub {
5282 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5283 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5287 if ($param->{force
}) {
5290 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5292 die $err if $lock_obtained;
5293 die "Failed to obtain guest migration lock - replication running?\n";
5298 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5301 __PACKAGE__-
>register_method({
5303 path
=> '{vmid}/template',
5307 description
=> "Create a Template.",
5309 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5310 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5313 additionalProperties
=> 0,
5315 node
=> get_standard_option
('pve-node'),
5316 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5320 description
=> "If you want to convert only 1 disk to base image.",
5321 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5328 description
=> "the task ID.",
5333 my $rpcenv = PVE
::RPCEnvironment
::get
();
5335 my $authuser = $rpcenv->get_user();
5337 my $node = extract_param
($param, 'node');
5339 my $vmid = extract_param
($param, 'vmid');
5341 my $disk = extract_param
($param, 'disk');
5343 my $load_and_check = sub {
5344 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5346 PVE
::QemuConfig-
>check_lock($conf);
5348 die "unable to create template, because VM contains snapshots\n"
5349 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5351 die "you can't convert a template to a template\n"
5352 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5354 die "you can't convert a VM to template if VM is running\n"
5355 if PVE
::QemuServer
::check_running
($vmid);
5360 $load_and_check->();
5363 PVE
::QemuConfig-
>lock_config($vmid, sub {
5364 my $conf = $load_and_check->();
5366 $conf->{template
} = 1;
5367 PVE
::QemuConfig-
>write_config($vmid, $conf);
5369 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5373 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5376 __PACKAGE__-
>register_method({
5377 name
=> 'cloudinit_generated_config_dump',
5378 path
=> '{vmid}/cloudinit/dump',
5381 description
=> "Get automatically generated cloudinit config.",
5383 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5386 additionalProperties
=> 0,
5388 node
=> get_standard_option
('pve-node'),
5389 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5391 description
=> 'Config type.',
5393 enum
=> ['user', 'network', 'meta'],
5403 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5405 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5408 __PACKAGE__-
>register_method({
5410 path
=> '{vmid}/mtunnel',
5413 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5417 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5418 ['perm', '/', [ 'Sys.Incoming' ]],
5420 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5421 " on '/'. Further permission checks happen during the actual migration.",
5424 additionalProperties
=> 0,
5426 node
=> get_standard_option
('pve-node'),
5427 vmid
=> get_standard_option
('pve-vmid'),
5430 format
=> 'pve-storage-id-list',
5432 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5436 format
=> 'pve-bridge-id-list',
5438 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5443 additionalProperties
=> 0,
5445 upid
=> { type
=> 'string' },
5446 ticket
=> { type
=> 'string' },
5447 socket => { type
=> 'string' },
5453 my $rpcenv = PVE
::RPCEnvironment
::get
();
5454 my $authuser = $rpcenv->get_user();
5456 my $node = extract_param
($param, 'node');
5457 my $vmid = extract_param
($param, 'vmid');
5459 my $storages = extract_param
($param, 'storages');
5460 my $bridges = extract_param
($param, 'bridges');
5462 my $nodename = PVE
::INotify
::nodename
();
5464 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5465 if $node ne 'localhost' && $node ne $nodename;
5469 my $storecfg = PVE
::Storage
::config
();
5470 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5471 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5474 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5475 PVE
::Network
::read_bridge_mtu
($bridge);
5478 PVE
::Cluster
::check_cfs_quorum
();
5480 my $lock = 'create';
5481 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5483 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5488 storecfg
=> PVE
::Storage
::config
(),
5493 my $run_locked = sub {
5494 my ($code, $params) = @_;
5495 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5496 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5498 $state->{conf
} = $conf;
5500 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5501 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5503 return $code->($params);
5511 description
=> 'Full VM config, adapted for target cluster/node',
5513 'firewall-config' => {
5515 description
=> 'VM firewall config',
5520 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5523 format
=> 'pve-storage-id',
5527 description
=> 'parsed drive information without volid and format',
5533 description
=> 'params passed to vm_start_nolock',
5537 description
=> 'migrate_opts passed to vm_start_nolock',
5543 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5549 description
=> 'remove VM config and disks, aborting migration',
5553 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5554 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5555 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5558 my $cmd_handlers = {
5560 # compared against other end's version
5561 # bump/reset for breaking changes
5562 # bump/bump for opt-in changes
5564 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5571 # parse and write out VM FW config if given
5572 if (my $fw_conf = $params->{'firewall-config'}) {
5573 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5580 ipset_comments
=> {},
5582 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5584 # TODO: add flag for strict parsing?
5585 # TODO: add import sub that does all this given raw content?
5586 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5587 $vmfw_conf->{vmid
} = $state->{vmid
};
5588 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5590 $state->{cleanup
}->{fw
} = 1;
5593 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5594 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5595 delete $new_conf->{lock};
5596 delete $new_conf->{digest
};
5598 # TODO handle properly?
5599 delete $new_conf->{snapshots
};
5600 delete $new_conf->{parent
};
5601 delete $new_conf->{pending
};
5603 # not handled by update_vm_api
5604 my $vmgenid = delete $new_conf->{vmgenid
};
5605 my $meta = delete $new_conf->{meta
};
5606 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5607 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5609 $new_conf->{vmid
} = $state->{vmid
};
5610 $new_conf->{node
} = $node;
5612 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5615 $update_vm_api->($new_conf, 1);
5618 # revert to locked previous config
5619 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5620 $conf->{lock} = 'create';
5621 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5626 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5627 $conf->{lock} = 'migrate';
5628 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5629 $conf->{meta
} = $meta if defined($meta);
5630 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5631 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5633 $state->{lock} = 'migrate';
5639 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5644 my $format = $params->{format
};
5645 my $storeid = $params->{storage
};
5646 my $drive = $params->{drive
};
5648 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5651 default => $storeid,
5654 my $source_volumes = {
5664 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5665 if (defined($res->{disk
})) {
5666 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5667 return $res->{disk
};
5669 die "failed to allocate NBD disk..\n";
5672 'disk-import' => sub {
5675 $check_storage_access_migrate->(
5683 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5685 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5687 'query-disk-import' => sub {
5690 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5695 my $info = PVE
::QemuServer
::vm_start_nolock
(
5699 $params->{start_params
},
5700 $params->{migrate_opts
},
5704 if ($info->{migrate
}->{proto
} ne 'unix') {
5705 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5706 die "migration over non-UNIX sockets not possible\n";
5709 my $socket = $info->{migrate
}->{addr
};
5710 chown $state->{socket_uid
}, -1, $socket;
5711 $state->{sockets
}->{$socket} = 1;
5713 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5714 foreach my $socket (@$unix_sockets) {
5715 chown $state->{socket_uid
}, -1, $socket;
5716 $state->{sockets
}->{$socket} = 1;
5721 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5722 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5723 warn "fstrim failed: $@\n" if $@;
5728 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5732 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5736 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5737 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5739 die "VM $state->{vmid} not running\n";
5744 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5745 delete $state->{lock};
5751 my $path = $params->{path
};
5753 die "Not allowed to generate ticket for unknown socket '$path'\n"
5754 if !defined($state->{sockets
}->{$path});
5756 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5761 if ($params->{cleanup
}) {
5762 if ($state->{cleanup
}->{fw
}) {
5763 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5766 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5767 print "freeing volume '$volid' as part of cleanup\n";
5768 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5772 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5775 print "switching to exit-mode, waiting for client to disconnect\n";
5782 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5783 unlink $socket_addr;
5785 $state->{socket} = IO
::Socket
::UNIX-
>new(
5786 Type
=> SOCK_STREAM
(),
5787 Local
=> $socket_addr,
5791 $state->{socket_uid
} = getpwnam('www-data')
5792 or die "Failed to resolve user 'www-data' to numeric UID\n";
5793 chown $state->{socket_uid
}, -1, $socket_addr;
5796 print "mtunnel started\n";
5798 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5800 warn "Failed to accept tunnel connection - $@\n";
5802 warn "Removing tunnel socket..\n";
5803 unlink $state->{socket};
5805 warn "Removing temporary VM config..\n";
5807 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5810 die "Exiting mtunnel\n";
5813 $state->{conn
} = $conn;
5815 my $reply_err = sub {
5818 my $reply = JSON
::encode_json
({
5819 success
=> JSON
::false
,
5822 $conn->print("$reply\n");
5826 my $reply_ok = sub {
5829 $res->{success
} = JSON
::true
;
5830 my $reply = JSON
::encode_json
($res);
5831 $conn->print("$reply\n");
5835 while (my $line = <$conn>) {
5838 # untaint, we validate below if needed
5839 ($line) = $line =~ /^(.*)$/;
5840 my $parsed = eval { JSON
::decode_json
($line) };
5842 $reply_err->("failed to parse command - $@");
5846 my $cmd = delete $parsed->{cmd
};
5847 if (!defined($cmd)) {
5848 $reply_err->("'cmd' missing");
5849 } elsif ($state->{exit}) {
5850 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5852 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5853 print "received command '$cmd'\n";
5855 if ($cmd_desc->{$cmd}) {
5856 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5860 my $res = $run_locked->($handler, $parsed);
5863 $reply_err->("failed to handle '$cmd' command - $@")
5866 $reply_err->("unknown command '$cmd' given");
5870 if ($state->{exit}) {
5871 print "mtunnel exited\n";
5873 die "mtunnel exited unexpectedly\n";
5877 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5878 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
5879 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5884 socket => $socket_addr,
5888 __PACKAGE__-
>register_method({
5889 name
=> 'mtunnelwebsocket',
5890 path
=> '{vmid}/mtunnelwebsocket',
5893 description
=> "You need to pass a ticket valid for the selected socket. Tickets can be created via the mtunnel API call, which will check permissions accordingly.",
5894 user
=> 'all', # check inside
5896 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5898 additionalProperties
=> 0,
5900 node
=> get_standard_option
('pve-node'),
5901 vmid
=> get_standard_option
('pve-vmid'),
5904 description
=> "unix socket to forward to",
5908 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5915 port
=> { type
=> 'string', optional
=> 1 },
5916 socket => { type
=> 'string', optional
=> 1 },
5922 my $rpcenv = PVE
::RPCEnvironment
::get
();
5923 my $authuser = $rpcenv->get_user();
5925 my $nodename = PVE
::INotify
::nodename
();
5926 my $node = extract_param
($param, 'node');
5928 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5929 if $node ne 'localhost' && $node ne $nodename;
5931 my $vmid = $param->{vmid
};
5933 PVE
::QemuConfig-
>load_config($vmid);
5935 my $socket = $param->{socket};
5936 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
5938 return { socket => $socket };